diff --git a/lib/dereference.ts b/lib/dereference.ts index 928613b4..c51cd107 100644 --- a/lib/dereference.ts +++ b/lib/dereference.ts @@ -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; @@ -20,6 +21,7 @@ function dereference = parser: $RefParser, options: O, ) { + const start = Date.now(); // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path); const dereferenced = crawl( parser.schema, @@ -30,6 +32,7 @@ function dereference = new Map(), parser.$refs, options, + start, ); parser.$refs.circular = dereferenced.circular; parser.schema = dereferenced.value; @@ -46,6 +49,7 @@ function dereference = * @param dereferencedCache - An map of all the dereferenced objects * @param $refs * @param options + * @param startTime - The time when the dereferencing started * @returns */ function crawl = ParserOptions>( @@ -57,6 +61,7 @@ function crawl = Parse dereferencedCache: any, $refs: $Refs, options: O, + startTime: number, ) { let dereferenced; const result = { @@ -64,6 +69,11 @@ function crawl = Parse 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); @@ -82,6 +92,7 @@ function crawl = Parse dereferencedCache, $refs, options, + startTime, ); result.circular = dereferenced.circular; result.value = dereferenced.value; @@ -107,6 +118,7 @@ function crawl = Parse dereferencedCache, $refs, options, + startTime, ); circular = dereferenced.circular; // Avoid pointless mutations; breaks frozen objects to no profit @@ -125,6 +137,7 @@ function crawl = Parse dereferencedCache, $refs, options, + startTime, ); circular = dereferenced.circular; // Avoid pointless mutations; breaks frozen objects to no profit @@ -170,6 +183,7 @@ function dereference$Ref, options: O, + startTime: number, ) { const isExternalRef = $Ref.isExternal$Ref($ref); const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root"; @@ -224,6 +238,7 @@ function dereference$Ref { * 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 = () => { diff --git a/lib/util/errors.ts b/lib/util/errors.ts index 35d77307..85ed8212 100644 --- a/lib/util/errors.ts +++ b/lib/util/errors.ts @@ -9,6 +9,7 @@ export type JSONParserErrorType = | "EUNKNOWN" | "EPARSER" | "EUNMATCHEDPARSER" + | "ETIMEOUT" | "ERESOLVER" | "EUNMATCHEDRESOLVER" | "EMISSINGPOINTER" @@ -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"; diff --git a/test/specs/timeout/definitions/name.yaml b/test/specs/timeout/definitions/name.yaml new file mode 100644 index 00000000..02eeba43 --- /dev/null +++ b/test/specs/timeout/definitions/name.yaml @@ -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 diff --git a/test/specs/timeout/definitions/required-string.yaml b/test/specs/timeout/definitions/required-string.yaml new file mode 100644 index 00000000..3970e6ba --- /dev/null +++ b/test/specs/timeout/definitions/required-string.yaml @@ -0,0 +1,3 @@ +title: requiredString +type: string +minLength: 1 diff --git a/test/specs/timeout/timeout.spec.ts b/test/specs/timeout/timeout.spec.ts new file mode 100644 index 00000000..538c96bf --- /dev/null +++ b/test/specs/timeout/timeout.spec.ts @@ -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"); + } + }); +}); diff --git a/test/specs/timeout/timeout.yaml b/test/specs/timeout/timeout.yaml new file mode 100644 index 00000000..ce4c3406 --- /dev/null +++ b/test/specs/timeout/timeout.yaml @@ -0,0 +1,213 @@ +title: Deep Schema +type: object +properties: + name: + $ref: "#/definitions/name" + level1: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level2: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level3: + type: object + required: + - name + properties: + name: + $ref: "#/definitions/name" + level4: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level5: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level6: + type: object + required: + - name + properties: + name: + $ref: "#/definitions/name" + level7: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level8: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level9: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level10: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level11: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level12: + type: object + required: + - name + properties: + name: + $ref: "#/definitions/name" + level13: + type: object + required: + - name + properties: + name: + $ref: "#/definitions/name" + level14: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level15: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level16: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level17: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level18: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level19: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level20: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level21: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level22: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level23: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level24: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level25: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level26: + type: object + required: + - name + properties: + name: + $ref: "#/definitions/name" + level27: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level28: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level29: + type: object + required: + - name + properties: + name: + $ref: definitions/name.yaml + level30: + $ref: "#" +definitions: + name: + $ref: definitions/name.yaml