Skip to content

fix(parser): validate large API definitions without exhausting the heap - #1226

Open
GoTo225 wants to merge 2 commits into
readmeio:mainfrom
GoTo225:fix/parser-large-spec-validation
Open

GoTo225 wants to merge 2 commits into
readmeio:mainfrom
GoTo225:fix/parser-large-spec-validation

Conversation

@GoTo225

@GoTo225 GoTo225 commented Sep 24, 2026

Copy link
Copy Markdown

Fixes #1225

🧰 Changes

Validating a large API definition with schema errors crashed the process with a heap out-of-memory error (see #1225 for the analysis and a reproduction). This PR changes three things in packages/parser:

1. Skip code frames for large definitions (src/validators/schema.ts)

Code frames are rendered by better-ajv-errors, which pretty-prints the whole dereferenced definition and parses it into a JSON AST. For definitions of LARGE_SPEC_SIZE_CAP (5,000,000 characters) or more, schema errors are now reported as plain messages instead: <instancePath> <ajv message>, plus the offending property for additionalProperties/unevaluatedProperties errors (whose Ajv message doesn't name it), e.g.

/paths/~1pets/get/parameters/1 must NOT have unevaluated properties (allowEmptyValue)
  • The existing cap of 20 errors plus additionalErrors is unchanged.
  • The size is measured once. A definition whose JSON.stringify throws (it exceeds the maximum string length) now counts as large; previously it counted as small and went straight into the formatter.

2. New validate.errors.codeFrames option (src/types.ts, README)

codeFrames: false returns the same plain messages for definitions of any size. The default is true, so small definitions keep today's output. Above the size threshold code frames are always disabled; the README documents why.

3. Linear reduceAjvErrors (src/lib/reduceAjvErrors.ts)

Every recorded instancePath now marks itself and its ancestors in a Set, so each error is checked in O(depth) instead of against every recorded error. This also fixes the ancestor check: it used instancePath.includes(…), a substring test, so /parameters/1 was dropped once /parameters/10 was recorded although they are siblings. Only real ancestors are dropped now, so such definitions can report more (correct) errors than before.

⚠️ Behaviour changes

  • Definitions of 5,000,000+ characters with schema errors get plain messages instead of code frames. This is visible in the existing large-file-memory-leak test, whose expected message changes from 4xx is not expected to be here! to must NOT have additional properties (4xx); the error count (20 + 1,016) is unchanged.
  • Sibling paths that share a prefix are no longer collapsed (see 3).

🧬 Testing

  • New test/lib/reduceAjvErrors.test.ts: lineage reduction, one error per path, prefix siblings (fails on main), $ref/oneOf noise, 200,000 errors within 5 s (does not finish on main).
  • New test/specs/code-frames-option: code frames by default, plain messages with codeFrames: false, plain messages when the definition cannot be stringified.
  • large-file-memory-leak: updated expectation, plus a test that codeFrames: true cannot force code frames on a large definition.
  • Reproduction from validate() exhausts the heap on large API definitions with schema errors #1225, --max-old-space-size=4096:
Definition main This PR
79 MB 66.8 s, 1.6 GB heap used, code frames 6.0 s, 0.2 GB, plain messages
157 MB heap out of memory after 4 min 14.0 s, 0.5 GB, plain messages

Both return the same 20 errors plus additionalErrors.

Schema errors are formatted with better-ajv-errors, which pretty-prints the
whole dereferenced API definition and parses it into a JSON AST to render
code frames. For large definitions this needs many times their size in
memory and crashes the process, or fails with "Invalid string length" once
the pretty-printed string exceeds the maximum string length, hiding every
real validation error.

- Skip code frames for definitions of 5,000,000 characters or more (the
  existing large spec threshold), including definitions that cannot be
  stringified at all, and report plain `<JSON pointer> <message>` errors.
- Add a `validate.errors.codeFrames` option to disable code frames for any
  definition. Code frames stay the default.
- Make reduceAjvErrors linear instead of quadratic, and only drop errors of
  actual ancestor paths: the substring check also dropped errors of sibling
  paths that share a prefix (`/parameters/1` and `/parameters/10`).
@GoTo225
GoTo225 requested a review from erunion as a code owner September 24, 2026 13:22
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2290839

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@readme/openapi-parser Minor
jest-expect-openapi Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Optimizes validation error handling for large API definitions.

The PR appears safe to merge.

Reviews (2) · Last reviewed commit: "fix(parser): name empty property names i..."

Comment thread packages/parser/src/validators/schema.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validate() exhausts the heap on large API definitions with schema errors

1 participant