Improve OpenAPI parsing errors (#3555)

This commit is contained in:
Greg Bergé
2025-08-13 13:38:14 +02:00
committed by GitHub
parent d655d3eece
commit 42c17f5c74
5 changed files with 48 additions and 30 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"gitbook": patch
---
Improve OpenAPI parsing errors
+16 -18
View File
@@ -36,25 +36,28 @@ export async function fetchOpenAPIFilesystem(
return { filesystem: null, specUrl: null };
}
const filesystem = await (() => {
const result = await (() => {
// If the reference is a new OpenAPI reference, we return it.
if (ref.kind === 'openapi') {
assert(resolved.openAPIFilesystem);
return resolved.openAPIFilesystem;
}
// For legacy blocks ("swagger"), we need to fetch the file system.
return fetchFilesystem(resolved.href, context.space.id);
})();
if ('error' in filesystem) {
throw new OpenAPIParseError(filesystem.error.message, { code: filesystem.error.code });
if ('error' in result) {
throw new OpenAPIParseError(result.error.message, { code: result.error.code });
}
return {
filesystem,
specUrl: resolved.href,
};
return { filesystem: result, specUrl: resolved.href };
}
const fetchFilesystem = async (
/**
* Fetch the filesystem from the URL.
* It's used for legacy "swagger" blocks.
*/
async function fetchFilesystem(
url: string,
spaceId: string
): Promise<
@@ -65,11 +68,11 @@ const fetchFilesystem = async (
message: string;
};
}
> => {
> {
'use cache';
try {
cacheTag(getCacheTag({ tag: 'space', space: spaceId }));
return await fetchFilesystemUncached(url);
return await fetchFilesystemNoCache(url);
} catch (error) {
// To avoid hammering the file with requests, we cache the error for around a minute.
cacheLife('minutes');
@@ -86,21 +89,16 @@ const fetchFilesystem = async (
console.error('Unknown error while fetching OpenAPI file:', error);
return { error: { code: 'invalid' as const, message: 'Unknown error' } };
}
};
}
async function fetchFilesystemUncached(
url: string,
options?: {
signal?: AbortSignal;
}
) {
async function fetchFilesystemNoCache(url: string) {
console.log(url);
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
// https://github.com/cloudflare/workerd/issues/1957
const response = await fetch(new URL(url), {
...noCacheFetchOptions,
cache: 'no-store',
signal: options?.signal,
});
if (!response.ok) {
@@ -9,10 +9,7 @@ import type {
type ResolveOpenAPIOperationBlockResult = ResolveOpenAPIBlockResult<OpenAPIOperationData>;
const weakmap = new WeakMap<
AnyOpenAPIOperationsBlock,
Promise<ResolveOpenAPIOperationBlockResult>
>();
const cache = new WeakMap<AnyOpenAPIOperationsBlock, Promise<ResolveOpenAPIOperationBlockResult>>();
/**
* Cache the result of resolving an OpenAPI block.
@@ -21,22 +18,24 @@ const weakmap = new WeakMap<
export function resolveOpenAPIOperationBlock(
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
): Promise<ResolveOpenAPIOperationBlockResult> {
if (weakmap.has(args.block)) {
return weakmap.get(args.block)!;
const inCache = cache.get(args.block);
if (inCache) {
return inCache;
}
const result = baseResolveOpenAPIOperationBlock(args);
weakmap.set(args.block, result);
return result;
const promise = resolveOpenAPIOperationBlockNoCache(args);
cache.set(args.block, promise);
return promise;
}
/**
* Resolve OpenAPI operation block.
*/
async function baseResolveOpenAPIOperationBlock(
async function resolveOpenAPIOperationBlockNoCache(
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
): Promise<ResolveOpenAPIOperationBlockResult> {
const { context, block } = args;
if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null };
}
+3 -1
View File
@@ -39,7 +39,9 @@ describe('#parseOpenAPI', () => {
});
} catch (error) {
if (error instanceof OpenAPIParseError) {
expect(error.message).toContain('Invalid OpenAPI document');
expect(error.message).toContain(
'Cant find supported Swagger/OpenAPI version in the provided document, version must be a string.'
);
}
}
});
+14 -1
View File
@@ -12,6 +12,19 @@ export async function parseOpenAPIV3(input: ParseOpenAPIInput): Promise<ParseOpe
const { value, rootURL, options = {} } = input;
const result = await validate(value);
// If there is no version, we consider it invalid instantely.
if (!result.version) {
throw new OpenAPIParseError(
'Cant find supported Swagger/OpenAPI version in the provided document, version must be a string.',
{
code: 'invalid',
rootURL,
errors: result.errors,
}
);
}
// If the version is 2.0, we throw an error to trigger the upgrade.
if (result.version === '2.0') {
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
code: 'parse-v2-in-v3',
@@ -21,7 +34,7 @@ export async function parseOpenAPIV3(input: ParseOpenAPIInput): Promise<ParseOpe
// We don't rely on `result.invalid` because it's too strict.
// If we succeed in parsing a schema, then we consider it valid.
if (!result.specification || !result.version) {
if (!result.specification) {
throw new OpenAPIParseError('Invalid OpenAPI document', {
code: 'invalid',
rootURL,