Skip to content

Commit c33ca65

Browse files
committed
feat: add timeout option
1 parent 003fff0 commit c33ca65

File tree

7 files changed

+290
-0
lines changed

7 files changed

+290
-0
lines changed

lib/dereference.ts

+15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type $Refs from "./refs.js";
66
import type { DereferenceOptions, ParserOptions } from "./options.js";
77
import type { JSONSchema } from "./types";
88
import type $RefParser from "./index";
9+
import { TimeoutError } from "./util/errors";
910

1011
export default dereference;
1112

@@ -20,6 +21,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
2021
parser: $RefParser<S, O>,
2122
options: O,
2223
) {
24+
const start = Date.now();
2325
// console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
2426
const dereferenced = crawl<S, O>(
2527
parser.schema,
@@ -30,6 +32,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
3032
new Map(),
3133
parser.$refs,
3234
options,
35+
start,
3336
);
3437
parser.$refs.circular = dereferenced.circular;
3538
parser.schema = dereferenced.value;
@@ -46,6 +49,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
4649
* @param dereferencedCache - An map of all the dereferenced objects
4750
* @param $refs
4851
* @param options
52+
* @param startTime - The time when the dereferencing started
4953
* @returns
5054
*/
5155
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
@@ -57,13 +61,19 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
5761
dereferencedCache: any,
5862
$refs: $Refs<S, O>,
5963
options: O,
64+
startTime: number,
6065
) {
6166
let dereferenced;
6267
const result = {
6368
value: obj,
6469
circular: false,
6570
};
6671

72+
if (options && options.timeoutMs) {
73+
if (Date.now() - startTime > options.timeoutMs) {
74+
throw new TimeoutError(options.timeoutMs);
75+
}
76+
}
6777
const derefOptions = (options.dereference || {}) as DereferenceOptions;
6878
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
6979

@@ -82,6 +92,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
8292
dereferencedCache,
8393
$refs,
8494
options,
95+
startTime,
8596
);
8697
result.circular = dereferenced.circular;
8798
result.value = dereferenced.value;
@@ -107,6 +118,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
107118
dereferencedCache,
108119
$refs,
109120
options,
121+
startTime,
110122
);
111123
circular = dereferenced.circular;
112124
// Avoid pointless mutations; breaks frozen objects to no profit
@@ -125,6 +137,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
125137
dereferencedCache,
126138
$refs,
127139
options,
140+
startTime,
128141
);
129142
circular = dereferenced.circular;
130143
// Avoid pointless mutations; breaks frozen objects to no profit
@@ -170,6 +183,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
170183
dereferencedCache: any,
171184
$refs: $Refs<S, O>,
172185
options: O,
186+
startTime: number,
173187
) {
174188
const isExternalRef = $Ref.isExternal$Ref($ref);
175189
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
@@ -224,6 +238,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
224238
dereferencedCache,
225239
$refs,
226240
options,
241+
startTime,
227242
);
228243
circular = dereferenced.circular;
229244
dereferencedValue = dereferenced.value;

lib/options.ts

+6
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ export interface $RefParserOptions<S extends object = JSONSchema> {
101101
* Default: `true` due to mutating the input being the default behavior historically
102102
*/
103103
mutateInputSchema?: boolean;
104+
105+
/**
106+
* The maximum amount of time (in milliseconds) that JSON Schema $Ref Parser will spend dereferencing a single schema.
107+
* It will throw a timeout error if the operation takes longer than this.
108+
*/
109+
timeoutMs?: number;
104110
}
105111

106112
export const getJsonSchemaRefParserDefaultOptions = () => {

lib/util/errors.ts

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type JSONParserErrorType =
99
| "EUNKNOWN"
1010
| "EPARSER"
1111
| "EUNMATCHEDPARSER"
12+
| "ETIMEOUT"
1213
| "ERESOLVER"
1314
| "EUNMATCHEDRESOLVER"
1415
| "EMISSINGPOINTER"
@@ -127,6 +128,14 @@ export class MissingPointerError extends JSONParserError {
127128
}
128129
}
129130

131+
export class TimeoutError extends JSONParserError {
132+
code = "ETIMEOUT" as JSONParserErrorType;
133+
name = "TimeoutError";
134+
constructor(timeout: number) {
135+
super(`Dereferencing timeout reached: ${timeout}ms`);
136+
}
137+
}
138+
130139
export class InvalidPointerError extends JSONParserError {
131140
code = "EUNMATCHEDRESOLVER" as JSONParserErrorType;
132141
name = "InvalidPointerError";
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
title: name
2+
type: object
3+
required:
4+
- first
5+
- last
6+
properties:
7+
first:
8+
$ref: ../definitions/required-string.yaml
9+
last:
10+
$ref: ./required-string.yaml
11+
middle:
12+
type:
13+
$ref: "#/properties/first/type"
14+
minLength:
15+
$ref: "#/properties/first/minLength"
16+
prefix:
17+
$ref: "#/properties/last"
18+
minLength: 3
19+
suffix:
20+
type: string
21+
$ref: "#/properties/prefix"
22+
maxLength: 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
title: requiredString
2+
type: string
3+
minLength: 1

test/specs/timeout/timeout.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, it } from "vitest";
2+
import { expect } from "vitest";
3+
import $RefParser from "../../../lib/index.js";
4+
import path from "../../utils/path";
5+
import helper from "../../utils/helper";
6+
import { TimeoutError } from "../../../lib/util/errors";
7+
8+
describe("Timeouts", () => {
9+
it("should throw error when timeout is reached", async () => {
10+
try {
11+
const parser = new $RefParser();
12+
await parser.dereference(path.rel("test/specs/timeout/timeout.yaml"), {
13+
timeoutMs: 0.01,
14+
});
15+
helper.shouldNotGetCalled();
16+
} catch (err) {
17+
expect(err).to.be.an.instanceOf(TimeoutError);
18+
// @ts-expect-error TS(2571): Object is of type 'unknown'.
19+
expect(err.message).to.contain("Dereferencing timeout reached");
20+
}
21+
});
22+
});

0 commit comments

Comments
 (0)