Skip to content

Commit

Permalink
feat: add timeout option
Browse files Browse the repository at this point in the history
  • Loading branch information
jonluca committed Apr 21, 2024
1 parent 003fff0 commit c33ca65
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 0 deletions.
15 changes: 15 additions & 0 deletions lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type $Refs from "./refs.js";
import type { DereferenceOptions, ParserOptions } from "./options.js";
import type { JSONSchema } from "./types";
import type $RefParser from "./index";
import { TimeoutError } from "./util/errors";

export default dereference;

Expand All @@ -20,6 +21,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
parser: $RefParser<S, O>,
options: O,
) {
const start = Date.now();
// console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
const dereferenced = crawl<S, O>(
parser.schema,
Expand All @@ -30,6 +32,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
new Map(),
parser.$refs,
options,
start,
);
parser.$refs.circular = dereferenced.circular;
parser.schema = dereferenced.value;
Expand All @@ -46,6 +49,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
* @param dereferencedCache - An map of all the dereferenced objects
* @param $refs
* @param options
* @param startTime - The time when the dereferencing started
* @returns
*/
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
Expand All @@ -57,13 +61,19 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
dereferencedCache: any,
$refs: $Refs<S, O>,
options: O,
startTime: number,
) {
let dereferenced;
const result = {
value: obj,
circular: false,
};

if (options && options.timeoutMs) {
if (Date.now() - startTime > options.timeoutMs) {
throw new TimeoutError(options.timeoutMs);
}
}
const derefOptions = (options.dereference || {}) as DereferenceOptions;
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);

Expand All @@ -82,6 +92,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
dereferencedCache,
$refs,
options,
startTime,
);
result.circular = dereferenced.circular;
result.value = dereferenced.value;
Expand All @@ -107,6 +118,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
Expand All @@ -125,6 +137,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
Expand Down Expand Up @@ -170,6 +183,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
dereferencedCache: any,
$refs: $Refs<S, O>,
options: O,
startTime: number,
) {
const isExternalRef = $Ref.isExternal$Ref($ref);
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
Expand Down Expand Up @@ -224,6 +238,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
dereferencedValue = dereferenced.value;
Expand Down
6 changes: 6 additions & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ export interface $RefParserOptions<S extends object = JSONSchema> {
* Default: `true` due to mutating the input being the default behavior historically
*/
mutateInputSchema?: boolean;

/**
* The maximum amount of time (in milliseconds) that JSON Schema $Ref Parser will spend dereferencing a single schema.
* It will throw a timeout error if the operation takes longer than this.
*/
timeoutMs?: number;
}

export const getJsonSchemaRefParserDefaultOptions = () => {
Expand Down
9 changes: 9 additions & 0 deletions lib/util/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type JSONParserErrorType =
| "EUNKNOWN"
| "EPARSER"
| "EUNMATCHEDPARSER"
| "ETIMEOUT"
| "ERESOLVER"
| "EUNMATCHEDRESOLVER"
| "EMISSINGPOINTER"
Expand Down Expand Up @@ -127,6 +128,14 @@ export class MissingPointerError extends JSONParserError {
}
}

export class TimeoutError extends JSONParserError {
code = "ETIMEOUT" as JSONParserErrorType;
name = "TimeoutError";
constructor(timeout: number) {
super(`Dereferencing timeout reached: ${timeout}ms`);
}
}

export class InvalidPointerError extends JSONParserError {
code = "EUNMATCHEDRESOLVER" as JSONParserErrorType;
name = "InvalidPointerError";
Expand Down
22 changes: 22 additions & 0 deletions test/specs/timeout/definitions/name.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
title: name
type: object
required:
- first
- last
properties:
first:
$ref: ../definitions/required-string.yaml
last:
$ref: ./required-string.yaml
middle:
type:
$ref: "#/properties/first/type"
minLength:
$ref: "#/properties/first/minLength"
prefix:
$ref: "#/properties/last"
minLength: 3
suffix:
type: string
$ref: "#/properties/prefix"
maxLength: 3
3 changes: 3 additions & 0 deletions test/specs/timeout/definitions/required-string.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
title: requiredString
type: string
minLength: 1
22 changes: 22 additions & 0 deletions test/specs/timeout/timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it } from "vitest";
import { expect } from "vitest";
import $RefParser from "../../../lib/index.js";
import path from "../../utils/path";
import helper from "../../utils/helper";
import { TimeoutError } from "../../../lib/util/errors";

describe("Timeouts", () => {
it("should throw error when timeout is reached", async () => {
try {
const parser = new $RefParser();
await parser.dereference(path.rel("test/specs/timeout/timeout.yaml"), {
timeoutMs: 0.01,
});
helper.shouldNotGetCalled();
} catch (err) {
expect(err).to.be.an.instanceOf(TimeoutError);
// @ts-expect-error TS(2571): Object is of type 'unknown'.
expect(err.message).to.contain("Dereferencing timeout reached");
}
});
});
Loading

0 comments on commit c33ca65

Please sign in to comment.