mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 02:53:29 +00:00
Improve OpenAPI parsing errors (#3555)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/openapi-parser": patch
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve OpenAPI parsing errors
|
||||||
@@ -36,25 +36,28 @@ export async function fetchOpenAPIFilesystem(
|
|||||||
return { filesystem: null, specUrl: null };
|
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') {
|
if (ref.kind === 'openapi') {
|
||||||
assert(resolved.openAPIFilesystem);
|
assert(resolved.openAPIFilesystem);
|
||||||
return resolved.openAPIFilesystem;
|
return resolved.openAPIFilesystem;
|
||||||
}
|
}
|
||||||
|
// For legacy blocks ("swagger"), we need to fetch the file system.
|
||||||
return fetchFilesystem(resolved.href, context.space.id);
|
return fetchFilesystem(resolved.href, context.space.id);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if ('error' in filesystem) {
|
if ('error' in result) {
|
||||||
throw new OpenAPIParseError(filesystem.error.message, { code: filesystem.error.code });
|
throw new OpenAPIParseError(result.error.message, { code: result.error.code });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { filesystem: result, specUrl: resolved.href };
|
||||||
filesystem,
|
|
||||||
specUrl: resolved.href,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFilesystem = async (
|
/**
|
||||||
|
* Fetch the filesystem from the URL.
|
||||||
|
* It's used for legacy "swagger" blocks.
|
||||||
|
*/
|
||||||
|
async function fetchFilesystem(
|
||||||
url: string,
|
url: string,
|
||||||
spaceId: string
|
spaceId: string
|
||||||
): Promise<
|
): Promise<
|
||||||
@@ -65,11 +68,11 @@ const fetchFilesystem = async (
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
> => {
|
> {
|
||||||
'use cache';
|
'use cache';
|
||||||
try {
|
try {
|
||||||
cacheTag(getCacheTag({ tag: 'space', space: spaceId }));
|
cacheTag(getCacheTag({ tag: 'space', space: spaceId }));
|
||||||
return await fetchFilesystemUncached(url);
|
return await fetchFilesystemNoCache(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// To avoid hammering the file with requests, we cache the error for around a minute.
|
// To avoid hammering the file with requests, we cache the error for around a minute.
|
||||||
cacheLife('minutes');
|
cacheLife('minutes');
|
||||||
@@ -86,21 +89,16 @@ const fetchFilesystem = async (
|
|||||||
console.error('Unknown error while fetching OpenAPI file:', error);
|
console.error('Unknown error while fetching OpenAPI file:', error);
|
||||||
return { error: { code: 'invalid' as const, message: 'Unknown error' } };
|
return { error: { code: 'invalid' as const, message: 'Unknown error' } };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
async function fetchFilesystemUncached(
|
async function fetchFilesystemNoCache(url: string) {
|
||||||
url: string,
|
console.log(url);
|
||||||
options?: {
|
|
||||||
signal?: AbortSignal;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
// 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:
|
// 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
|
// https://github.com/cloudflare/workerd/issues/1957
|
||||||
const response = await fetch(new URL(url), {
|
const response = await fetch(new URL(url), {
|
||||||
...noCacheFetchOptions,
|
...noCacheFetchOptions,
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
signal: options?.signal,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import type {
|
|||||||
|
|
||||||
type ResolveOpenAPIOperationBlockResult = ResolveOpenAPIBlockResult<OpenAPIOperationData>;
|
type ResolveOpenAPIOperationBlockResult = ResolveOpenAPIBlockResult<OpenAPIOperationData>;
|
||||||
|
|
||||||
const weakmap = new WeakMap<
|
const cache = new WeakMap<AnyOpenAPIOperationsBlock, Promise<ResolveOpenAPIOperationBlockResult>>();
|
||||||
AnyOpenAPIOperationsBlock,
|
|
||||||
Promise<ResolveOpenAPIOperationBlockResult>
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache the result of resolving an OpenAPI block.
|
* Cache the result of resolving an OpenAPI block.
|
||||||
@@ -21,22 +18,24 @@ const weakmap = new WeakMap<
|
|||||||
export function resolveOpenAPIOperationBlock(
|
export function resolveOpenAPIOperationBlock(
|
||||||
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
|
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
|
||||||
): Promise<ResolveOpenAPIOperationBlockResult> {
|
): Promise<ResolveOpenAPIOperationBlockResult> {
|
||||||
if (weakmap.has(args.block)) {
|
const inCache = cache.get(args.block);
|
||||||
return weakmap.get(args.block)!;
|
if (inCache) {
|
||||||
|
return inCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = baseResolveOpenAPIOperationBlock(args);
|
const promise = resolveOpenAPIOperationBlockNoCache(args);
|
||||||
weakmap.set(args.block, result);
|
cache.set(args.block, promise);
|
||||||
return result;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve OpenAPI operation block.
|
* Resolve OpenAPI operation block.
|
||||||
*/
|
*/
|
||||||
async function baseResolveOpenAPIOperationBlock(
|
async function resolveOpenAPIOperationBlockNoCache(
|
||||||
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
|
args: ResolveOpenAPIBlockArgs<AnyOpenAPIOperationsBlock>
|
||||||
): Promise<ResolveOpenAPIOperationBlockResult> {
|
): Promise<ResolveOpenAPIOperationBlockResult> {
|
||||||
const { context, block } = args;
|
const { context, block } = args;
|
||||||
|
|
||||||
if (!block.data.path || !block.data.method) {
|
if (!block.data.path || !block.data.method) {
|
||||||
return { data: null, specUrl: null };
|
return { data: null, specUrl: null };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ describe('#parseOpenAPI', () => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpenAPIParseError) {
|
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.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ export async function parseOpenAPIV3(input: ParseOpenAPIInput): Promise<ParseOpe
|
|||||||
const { value, rootURL, options = {} } = input;
|
const { value, rootURL, options = {} } = input;
|
||||||
const result = await validate(value);
|
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') {
|
if (result.version === '2.0') {
|
||||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
|
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
|
||||||
code: 'parse-v2-in-v3',
|
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.
|
// 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 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', {
|
throw new OpenAPIParseError('Invalid OpenAPI document', {
|
||||||
code: 'invalid',
|
code: 'invalid',
|
||||||
rootURL,
|
rootURL,
|
||||||
|
|||||||
Reference in New Issue
Block a user