Avoid dereferencing before caching (#2793)

This commit is contained in:
Greg Bergé
2025-01-30 17:15:19 +01:00
committed by GitHub
parent 0d615e3888
commit d9c8d57e8e
8 changed files with 56008 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Do not dereference before caching OpenAPI spec.
+1 -1
View File
@@ -49,7 +49,7 @@ export async function fetchOpenAPIBlock(
const fetcher: OpenAPIFetcher = {
fetch: cache({
name: 'openapi.fetch.v3',
name: 'openapi.fetch.v4',
get: async (url: string, options: CacheFunctionOptions) => {
// 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:
@@ -1,9 +1,9 @@
import { toJSON, fromJSON } from 'flatted';
import { OpenAPICustomSpecProperties } from './parser';
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
import { noReference } from './utils';
import { parseDescriptions } from './parser/markdown';
import { dereference } from '@scalar/openapi-parser';
export interface OpenAPIFetcher {
/**
@@ -44,7 +44,8 @@ export async function fetchOpenAPIOperation(
},
fetcher: OpenAPIFetcher,
): Promise<OpenAPIOperationData | null> {
const schema = await fetcher.fetch(input.url);
const refSchema = await fetcher.fetch(input.url);
const schema = await memoDereferenceSchema(refSchema, input.url);
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
@@ -89,6 +90,38 @@ export async function fetchOpenAPIOperation(
};
}
const dereferenceSchemaCache = new WeakMap<OpenAPI.Document, Promise<OpenAPI.Document>>();
/**
* Memoized version of `dereferenceSchema`.
*/
function memoDereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
if (dereferenceSchemaCache.has(schema)) {
return dereferenceSchemaCache.get(schema) as Promise<T>;
}
const promise = dereferenceSchema(schema, url);
dereferenceSchemaCache.set(schema, promise);
return promise;
}
/**
* Dereference an OpenAPI schema.
*/
async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
const derefResult = await dereference(schema);
if (!derefResult.schema) {
throw new OpenAPIParseError(
'Failed to dereference OpenAPI document',
url,
'failed-dereference',
);
}
return derefResult.schema as T;
}
/**
* Get a path object from its path.
*/
+1 -1
View File
@@ -4,7 +4,7 @@ export class OpenAPIParseError extends Error {
constructor(
message: string,
public readonly url: string,
public readonly code?: 'invalid-spec' | 'v2-spec',
public readonly code?: 'invalid-spec' | 'v2-spec' | 'failed-dereference',
) {
super(message);
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
import { expect, it } from 'bun:test';
import { readFile } from 'node:fs/promises';
import { parseOpenAPI } from '.';
it('should parse and not give a recursive structure', async () => {
const schema = await parseOpenAPI({
value: await readFile(new URL('./fixtures/spec-example.json', import.meta.url), 'utf-8'),
url: 'https://example.com',
parseMarkdown: async (input) => input,
});
JSON.stringify(schema);
expect(schema.openapi).toBe('3.0.0');
});
@@ -15,8 +15,6 @@ export function traverse(
return specification;
}
seen.add(specification);
for (const [key, value] of Object.entries(specification)) {
const currentPath = [...path, key];
if (Array.isArray(value)) {
+7 -7
View File
@@ -1,5 +1,5 @@
import { OpenAPICustomSpecProperties } from './types';
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
import { AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
import { OpenAPIParseError } from './error';
import { parseDescriptions } from './markdown';
@@ -18,10 +18,10 @@ export async function parseOpenAPIV3(input: {
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
> {
const { value, url } = input;
const result = await dereference(value);
const result = await validate(value);
// Spec is invalid, we stop here.
if (!result.schema) {
if (!result.specification) {
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
}
@@ -29,16 +29,16 @@ export async function parseOpenAPIV3(input: {
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
}
const schema = await parseDescriptions({
specification: result.schema,
const specification = await parseDescriptions({
specification: result.specification,
parseMarkdown: input.parseMarkdown,
});
switch (result.version) {
case '3.0':
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
return specification as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
case '3.1':
default:
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
return specification as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
}
}