Skip to content

fix: prevent OOM when loading multi-file OpenAPI specs#5

Merged
laurence79 merged 1 commit intomainfrom
fix/multi-file-ref-oom
Apr 2, 2026
Merged

fix: prevent OOM when loading multi-file OpenAPI specs#5
laurence79 merged 1 commit intomainfrom
fix/multi-file-ref-oom

Conversation

@laurence79
Copy link
Copy Markdown
Owner

@laurence79 laurence79 commented Apr 2, 2026

Summary

  • Fixes a JavaScript heap out of memory crash when generating code from OpenAPI specs that use cross-file $ref references (e.g. models.yaml#/components/schemas/Foo)
  • The document loader's inlining logic re-inlined the same external schemas under new pointer paths for every reference encountered, creating an exponentially growing document structure
  • Adds deduplication by canonical reference key, deep-clones external schemas before insertion, and walks only the cloned subtree

Root cause

When the loader encounters a cross-file $ref, it copies the referenced schema into the root document at #/components/schemas/<title> and recursively walks the copy to resolve nested refs. However:

  1. No deduplication: the walked array tracked pointer paths in the root document, not canonical external schema identities. The same external schema was inlined and walked once per unique pointer path where it was referenced.
  2. Shared object references: refNode was inserted by reference (not cloned), so the cached external document was mutated by subsequent walks.
  3. Walking from the root: AsyncJsonWalker.walk(doc, walkFn, newRef.pointer) could discover sibling schemas added by previous inlines, causing cascading re-processing.

With ~70 shared schemas referenced from ~35 endpoints, this created nested components/schemas trees (e.g. /components/schemas/CompanyStatus/components/schemas/ApproverList/components/schemas/...) growing without bound until OOM at ~4GB.

Fix

  • Added inlinedRefs Set to track filename#pointer pairs that have already been inlined — each external schema is copied and walked exactly once
  • JSON.parse(JSON.stringify(refNode)) deep-clones the node before inserting, preventing mutation of the cached source document
  • Walk the cloned subtree directly rather than from a pointer path within doc

Testing

  • Added load.spec.ts with 4 tests covering multi-file $ref resolution
  • Added test fixtures (models.yaml + multi-file-api.yaml) exercising cross-file refs from multiple endpoints
  • Verified against a production spec with 70+ schemas and 35+ endpoints — completes in <1s (previously OOM'd at 4GB)
  • Existing v2 (swagger.json + user.json) example still generates correctly

Note

Medium Risk
Touches core OpenAPI document-loading/inlining logic; mistakes could break $ref rewriting or schema placement for multi-file specs, though changes are scoped and covered by new tests.

Overview
Prevents runaway growth/OOM when loading multi-file OpenAPI specs by deduplicating cross-file $ref inlining and avoiding repeated reinsertion of the same external schema.

load() now tracks already-inlined canonical refs, deep-clones referenced nodes before inserting into the root document, and walks only the cloned subtree; new fixtures and load.spec.ts tests assert schemas are inlined once, $refs are rewritten locally, and no nested components trees are produced.

Written by Cursor Bugbot for commit 9bb3be9. This will update automatically on new commits. Configure here.

The document loader's cross-file $ref inlining created an exponentially
growing document structure. Each inlined schema triggered a recursive
walk that re-inlined all transitively referenced schemas under new
pointer paths, causing the document to balloon until the process ran
out of memory.

This was triggered in practice by OpenAPI specs with ~70 shared schemas
in a models.yaml file referenced from multiple endpoints.

The fix:
- Track already-inlined external refs by canonical key to avoid
  re-inlining the same schema multiple times
- Deep-clone external schema nodes before inserting into the root
  document to prevent mutations to the cached source file
- Walk the cloned subtree directly instead of walking from a pointer
  path within the root document
@laurence79 laurence79 force-pushed the fix/multi-file-ref-oom branch from 128a85e to 9bb3be9 Compare April 2, 2026 16:48
@laurence79 laurence79 merged commit 43c5395 into main Apr 2, 2026
2 checks passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

jsonPointer.set(doc, newRef.pointer, cloned);

await AsyncJsonWalker.walk(cloned, walkFn, newRef.pointer);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplication skips inlining when target path differs

Low Severity

node.$ref = newRef.$ is set unconditionally (line 202), but jsonPointer.set(doc, newRef.pointer, cloned) only runs for the first encounter of a given canonicalKey. Since newRef depends on componentKeyForPointer(ptr), if the same external schema is referenced from contexts producing different component keys (e.g., one resolving to schemas, another to parameters), the second occurrence's $ref will point to a path that was never populated in doc, creating a dangling reference.

Additional Locations (1)
Fix in Cursor Fix in Web

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.

1 participant