Skip to content

fix(cli): gracefully handle circular $ref errors in OpenAPI specs#15064

Open
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1776275480-fix-circular-ref-error
Open

fix(cli): gracefully handle circular $ref errors in OpenAPI specs#15064
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1776275480-fix-circular-ref-error

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Apr 15, 2026

Description

Closes #14422

Gracefully handle the "Self-referencing circular pointer" error thrown by @redocly/openapi-core when bundling OpenAPI specs that contain circular $ref references. Instead of failing the entire publish/generation pipeline, the CLI now logs a clear warning with the offending file path and circular $ref breadcrumbs, then continues processing with the unbundled document.

Changes Made

  • Error handling in parseOpenAPI.ts: Wrapped the bundle() call in a try-catch. On "Self-referencing circular pointer" errors, falls back to the unbundled document and continues processing.
  • Circular ref detection (findCircularRefs.ts): New DFS-based utility that walks the OpenAPI document, builds a schema-level adjacency graph from $ref targets, and detects cycles. Normalizes source nodes to their containing schema ancestor so both direct and indirect circular references are detected.
  • Warning output: Logs a clear warning with file path and cycle chain:
    Circular $ref detected in /path/to/openapi.yaml.
    The following references form a cycle:
      - at components.schemas.PlantCategory ($ref: #/components/schemas/PlantCategoryAlias)
        cycle: components.schemas.PlantCategory -> components.schemas.PlantCategoryAlias -> components.schemas.PlantCategory
    Continuing without full reference resolution.
    
  • Test: Added test fixture with circular $ref chain and test case verifying the full pipeline (workspace loading → IR → FDR) completes successfully despite circular refs.
  • Changelog entry: Added unreleased changelog for the fix.
  • Updated README.md generator (if applicable) — N/A

Testing

  • Unit tests added/updated — test fixture + test case in openapi-from-flag.test.ts
  • Manual testing completed — verified fixture triggers redocly error and pipeline recovers

Link to Devin session: https://app.devin.ai/sessions/3d60d563398f4f06b4d7328bbf50d3d8


Open with Devin

Co-Authored-By: kenny <kenny@buildwithfern.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Copy Markdown
Contributor

🌱 Seed Test Selector

Select languages to run seed tests for:

  • Python
  • TypeScript
  • Java
  • Go
  • Ruby
  • C#
  • PHP
  • Swift
  • Rust
  • OpenAPI

How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR.

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-15T21:03:37Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 96s 146s 94s -2s (-2.1%)
go-sdk square 113s 145s 111s -2s (-1.8%)
java-sdk square 184s 188s 162s -22s (-12.0%)
php-sdk square 90s 128s 90s +0s (+0.0%)
python-sdk square 115s 155s 116s +1s (+0.9%)
ruby-sdk-v2 square 118s 142s 116s -2s (-1.7%)
rust-sdk square 96s 96s 94s -2s (-2.1%)
swift-sdk square 89s 132s 86s -3s (-3.4%)
ts-sdk square 103s 140s 106s +3s (+2.9%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-15T21:03:37Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-04-15 21:33 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

Docs Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-15T21:03:37Z).

Fixture main PR Delta
docs 319.8s 284.7s (35 versions) -35.1s (-11.0%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-04-15T21:03:37Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-04-15 21:34 UTC

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +36 to +43
// The source of the $ref is the parent of the "$ref" key
const sourceParent = sourcePath.endsWith(".$ref") ? sourcePath.slice(0, -5) : sourcePath;
const sourcePointer = dotPathToPointer(sourceParent);

if (!graph.has(sourcePointer)) {
graph.set(sourcePointer, new Set());
}
graph.get(sourcePointer)?.add(dotPathToPointer(targetDotPath));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 Cycle detection graph uses mismatched node granularity, failing to detect most indirect circular refs

The findCircularRefs function builds a graph where source nodes are at the $ref location level (e.g., /components/schemas/Foo/properties/bar) but target nodes are at the referenced schema level (e.g., /components/schemas/Bar). These different granularities mean edges never form cycles for the most common circular reference pattern in OpenAPI (indirect references through properties).

Concrete example showing the failure

Given a typical circular ref:

Foo:
  properties:
    bar:
      $ref: '#/components/schemas/Bar'
Bar:
  properties:
    foo:
      $ref: '#/components/schemas/Foo'

The graph gets edges:

  • /components/schemas/Foo/properties/bar/components/schemas/Bar
  • /components/schemas/Bar/properties/foo/components/schemas/Foo

Node /components/schemas/Bar has no outgoing edge (only /components/schemas/Bar/properties/foo does), so the DFS never finds a cycle. The function falls through to the fallback message "(unable to determine exact location...)" instead of providing the precise cycle information.

Only direct self-references (e.g., Foo: $ref: '#/.../Foo') or schema-level $ref cycles (where the $ref is the direct child of the schema) are detected.

Prompt for agents
The graph-building logic in findCircularRefs uses the exact $ref location as the source node (e.g. /components/schemas/Foo/properties/bar) but the $ref target as the destination (e.g. /components/schemas/Bar). Since these are at different hierarchy levels, the DFS never finds cycles through properties.

To fix this, the source node should be normalized to the same level as the target. For OpenAPI circular refs, this typically means extracting the schema-level ancestor path from the $ref location. For example, if a $ref lives at components.schemas.Foo.properties.bar.$ref, the source node should be /components/schemas/Foo (not /components/schemas/Foo/properties/bar).

One approach: for each $ref, find the prefix that corresponds to a target path seen elsewhere (like /components/schemas/X) and use that as the source node. Another approach: build the graph specifically at the components/schemas level by matching source paths against the target format (e.g., anything under /components/schemas/X maps to source node /components/schemas/X).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in the next push. The graph now normalizes source nodes to their schema-level ancestor (e.g., components.schemas.Foo.properties.barcomponents.schemas.Foo) using a findSchemaAncestor() helper, so indirect cycles through properties are correctly detected. Also added support for definitions.X (Swagger 2.0) paths.

devin-ai-integration bot and others added 2 commits April 15, 2026 21:10
Co-Authored-By: kenny <kenny@buildwithfern.com>
Co-Authored-By: kenny <kenny@buildwithfern.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Output file/location of Self-referencing circular pointers

0 participants