Skip to content

Commit d9c8d57

Browse files
authored
Avoid dereferencing before caching (#2793)
1 parent 0d615e3 commit d9c8d57

File tree

8 files changed

+56008
-15
lines changed

8 files changed

+56008
-15
lines changed

.changeset/khaki-zebras-deliver.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
'gitbook': patch
4+
---
5+
6+
Do not dereference before caching OpenAPI spec.

packages/gitbook/src/lib/openapi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function fetchOpenAPIBlock(
4949

5050
const fetcher: OpenAPIFetcher = {
5151
fetch: cache({
52-
name: 'openapi.fetch.v3',
52+
name: 'openapi.fetch.v4',
5353
get: async (url: string, options: CacheFunctionOptions) => {
5454
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
5555
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:

packages/react-openapi/src/fetchOpenAPIOperation.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { toJSON, fromJSON } from 'flatted';
22

3-
import { OpenAPICustomSpecProperties } from './parser';
4-
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
3+
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
4+
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
55
import { noReference } from './utils';
6-
import { parseDescriptions } from './parser/markdown';
6+
import { dereference } from '@scalar/openapi-parser';
77

88
export interface OpenAPIFetcher {
99
/**
@@ -44,7 +44,8 @@ export async function fetchOpenAPIOperation(
4444
},
4545
fetcher: OpenAPIFetcher,
4646
): Promise<OpenAPIOperationData | null> {
47-
const schema = await fetcher.fetch(input.url);
47+
const refSchema = await fetcher.fetch(input.url);
48+
const schema = await memoDereferenceSchema(refSchema, input.url);
4849

4950
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
5051

@@ -89,6 +90,38 @@ export async function fetchOpenAPIOperation(
8990
};
9091
}
9192

93+
const dereferenceSchemaCache = new WeakMap<OpenAPI.Document, Promise<OpenAPI.Document>>();
94+
95+
/**
96+
* Memoized version of `dereferenceSchema`.
97+
*/
98+
function memoDereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
99+
if (dereferenceSchemaCache.has(schema)) {
100+
return dereferenceSchemaCache.get(schema) as Promise<T>;
101+
}
102+
103+
const promise = dereferenceSchema(schema, url);
104+
dereferenceSchemaCache.set(schema, promise);
105+
return promise;
106+
}
107+
108+
/**
109+
* Dereference an OpenAPI schema.
110+
*/
111+
async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
112+
const derefResult = await dereference(schema);
113+
114+
if (!derefResult.schema) {
115+
throw new OpenAPIParseError(
116+
'Failed to dereference OpenAPI document',
117+
url,
118+
'failed-dereference',
119+
);
120+
}
121+
122+
return derefResult.schema as T;
123+
}
124+
92125
/**
93126
* Get a path object from its path.
94127
*/

packages/react-openapi/src/parser/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export class OpenAPIParseError extends Error {
44
constructor(
55
message: string,
66
public readonly url: string,
7-
public readonly code?: 'invalid-spec' | 'v2-spec',
7+
public readonly code?: 'invalid-spec' | 'v2-spec' | 'failed-dereference',
88
) {
99
super(message);
1010
}

packages/react-openapi/src/parser/fixtures/spec-example.json

Lines changed: 55942 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { expect, it } from 'bun:test';
2+
import { readFile } from 'node:fs/promises';
3+
import { parseOpenAPI } from '.';
4+
5+
it('should parse and not give a recursive structure', async () => {
6+
const schema = await parseOpenAPI({
7+
value: await readFile(new URL('./fixtures/spec-example.json', import.meta.url), 'utf-8'),
8+
url: 'https://example.com',
9+
parseMarkdown: async (input) => input,
10+
});
11+
12+
JSON.stringify(schema);
13+
expect(schema.openapi).toBe('3.0.0');
14+
});

packages/react-openapi/src/parser/traverse.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ export function traverse(
1515
return specification;
1616
}
1717

18-
seen.add(specification);
19-
2018
for (const [key, value] of Object.entries(specification)) {
2119
const currentPath = [...path, key];
2220
if (Array.isArray(value)) {

packages/react-openapi/src/parser/v3.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAPICustomSpecProperties } from './types';
2-
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
2+
import { AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
33
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
44
import { OpenAPIParseError } from './error';
55
import { parseDescriptions } from './markdown';
@@ -18,27 +18,27 @@ export async function parseOpenAPIV3(input: {
1818
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
1919
> {
2020
const { value, url } = input;
21-
const result = await dereference(value);
21+
const result = await validate(value);
2222

2323
// Spec is invalid, we stop here.
24-
if (!result.schema) {
24+
if (!result.specification) {
2525
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
2626
}
2727

2828
if (result.version === '2.0') {
2929
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
3030
}
3131

32-
const schema = await parseDescriptions({
33-
specification: result.schema,
32+
const specification = await parseDescriptions({
33+
specification: result.specification,
3434
parseMarkdown: input.parseMarkdown,
3535
});
3636

3737
switch (result.version) {
3838
case '3.0':
39-
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
39+
return specification as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
4040
case '3.1':
4141
default:
42-
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
42+
return specification as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
4343
}
4444
}

0 commit comments

Comments
 (0)