Skip to content

Improve OpenAPI parsing errors #3555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tough-beers-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"gitbook": patch
---

Improve OpenAPI parsing errors
34 changes: 16 additions & 18 deletions packages/gitbook/src/lib/openapi/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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');
Expand All @@ -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) {
Expand Down
19 changes: 9 additions & 10 deletions packages/gitbook/src/lib/openapi/resolveOpenAPIOperationBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 };
}
Expand Down
4 changes: 3 additions & 1 deletion packages/openapi-parser/src/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ describe('#parseOpenAPI', () => {
});
} catch (error) {
if (error instanceof OpenAPIParseError) {
expect(error.message).toContain('Invalid OpenAPI document');
expect(error.message).toContain(
'Can’t find supported Swagger/OpenAPI version in the provided document, version must be a string.'
);
}
}
});
Expand Down
15 changes: 14 additions & 1 deletion packages/openapi-parser/src/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'Can’t 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',
Expand All @@ -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,
Expand Down