Skip to content

fix: render flows whose nodes have no position instead of crashing the canvas - #14258

Merged
erichare merged 2 commits into
release-1.12.0from
fix/position-less-flow-canvas-crash
Jul 25, 2026
Merged

fix: render flows whose nodes have no position instead of crashing the canvas#14258
erichare merged 2 commits into
release-1.12.0from
fix/position-less-flow-canvas-crash

Conversation

@erichare

@erichare erichare commented Jul 25, 2026

Copy link
Copy Markdown
Member

Fixes #14257.

Problem

Any flow whose data.nodes lack a position crashes the canvas with a full-page "Sorry, we found an unexpected error!" when opened at /flow/{id}.

The frontend already intends to handle this — needsLayout() + getLayoutedNodes() exist precisely to lay out position-less flows — but the fallback is broken in two independent ways, so it never recovers.

1. getLayoutedNodes passes id: undefined to ELK. Edges are mapped with id: e.id, and ports are derived with id: e.sourceHandle / id: e.targetHandle. When a flow's edges have none of those, ELK rejects the graph with JsonImportException: Id must be a string or an integer: 'null' — so the layout fails for exactly the flows that need it. Bisected against a real payload, both id sources must be fixed; repairing either alone still throws:

baseline (as shipped): THREW -> JsonImportException: Id must be a string or an integer: 'null'.
edge ids fixed only:   THREW -> JsonImportException: Id must be a string or an integer: 'null'.
ports filtered only:   THREW -> JsonImportException: Id must be a string or an integer: 'null'.
both fixed:            OK -> ChatInput(12,12) PromptComponent(416,140) ...

2. The layout is applied too late — the actual root cause of the crash. processFlows iterates with forEach(async (flow) => { … await processDataFromFlow(…) }); forEach discards the returned promise, so processFlows returns before the layout resolves. useApplyFlowToCanvas then calls setCurrentFlow() immediately → resetFlow() → position-less nodes reach the store → React Flow's adoptUserNodes calls getNodePositionWithOrigin, which dereferences node.position.x unguarded. This crashes even when ELK succeeds, so fixing the ids alone is not sufficient.

Changes

  • layoutUtils.ts — drop ports whose handle is absent (their edges fall back to the always-present node-id port) and give id-less edges a stable synthetic id, so ELK accepts the graph. Wrap elk.layout so a failure returns a deterministic grid via the new exported getFallbackGridPositions instead of leaving nodes unpositioned.
  • reactflowUtils.tsprocessDataFromFlow seeds deterministic positions synchronously before awaiting ELK, so no node can reach React Flow without one regardless of the async timing. needsLayout now also rejects non-numeric and NaN coordinates, not just an absent position.

Testing

New src/frontend/src/utils/__tests__/layoutUtils.test.ts (10 cases) covering the fallback grid, the tightened needsLayout, ELK layout with handle-less edges, and the synchronous guarantee through the real processFlows.

  • 7 of the 10 fail on unpatched code, all 10 pass with the fix.
  • Full src/utils suite green: 422 tests / 30 suites.
  • tsc --noEmit clean; biome check clean.

Verified end to end against all five GET /api/v1/starter-projects/ payloads (4–9 nodes each): before the fix every node was unpositioned and ELK threw; after, each renders with a real layered layout.

Note on scope

This stops the crash and produces a correct layout from getLayoutedNodes. It does not change processFlows' fire-and-forget signature, so on the useApplyFlowToCanvas path the canvas renders the synchronously-seeded grid rather than the ELK result — resetFlow captures the node array by reference before the async layout rebinds data.nodes:

store-captured array at capture time: [{"x":0,"y":0},{"x":464,"y":0}]
flow.data.nodes after ELK settles:    [{"x":12,"y":12},{"x":790,"y":12}]
what the store STILL holds:           [{"x":0,"y":0},{"x":464,"y":0}]

Threading await through processFlows touches 8 call sites, so I kept it out of this PR to keep the crash fix small and reviewable. Happy to follow up with it if you'd like.

Adjacent data-quality bugs (not fixed here)

Two more issues visible in the same /api/v1/starter-projects/ response, noted in #14257 — both look unintended and are backend-side:

  • name is always null (Graph.dump() only sets it when self.flow_name is truthy).
  • endpoint_name is the literal string "None" (str(endpoint_name) on None in lfx/graph/graph/base.py).

Let me know if you'd like those in scope and I'll add them.

Summary by CodeRabbit

  • Bug Fixes
    • Improved automatic layout handling for graphs with missing edge IDs or connection handles.
    • Added a reliable grid fallback when automatic layout cannot be completed.
    • Prevented crashes caused by missing, invalid, or NaN node positions.
    • Ensured nodes receive valid positions immediately while layout processing completes.
    • Added regression coverage for layout behavior across edge-less and handle-less graphs.

…e canvas

Flows whose `data.nodes` lack a `position` crashed the canvas with a full-page
error boundary. The auto-layout fallback that exists for exactly this case
(`needsLayout` + `getLayoutedNodes`) was broken in two independent ways.

1. `getLayoutedNodes` passed `id: undefined` to ELK — for edges (`id: e.id`)
   and for ports derived from absent handles (`id: e.sourceHandle`). ELK
   rejected the graph with `JsonImportException: Id must be a string or an
   integer: 'null'`, so the layout failed for precisely the flows that needed
   it. Both id sources had to be fixed; repairing either alone still threw.
   Handle-less ports are now dropped (their edges fall back to the
   always-present node-id port) and edges without an id get a stable synthetic
   one. A failed layout now falls back to a deterministic grid rather than
   leaving nodes unpositioned.

2. `processFlows` iterates with `forEach(async …)`, which discards the returned
   promise, so it returns before the layout resolves and `useApplyFlowToCanvas`
   hands position-less nodes straight to React Flow — whose
   `getNodePositionWithOrigin` dereferences `node.position.x` unguarded. This
   crashed even when ELK succeeded. `processDataFromFlow` now seeds
   deterministic positions synchronously before awaiting ELK, so no node can
   reach React Flow without one.

`needsLayout` also now rejects non-numeric and NaN coordinates, not just an
absent `position`.

A payload with this shape is served by `GET /api/v1/starter-projects/`, whose
nodes have no `position` and whose edges have no `id`/`sourceHandle`/
`targetHandle`; POSTing one to `/api/v1/flows/` and opening it reproduced the
crash. Position-less flows can also arrive from hand-written JSON, older
exports, and third-party tooling.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bcf69db-8f98-4d8d-b50e-cb678f5812b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Frontend auto-layout now validates node positions, handles missing ELK edge and port identifiers, falls back to deterministic grid placement on layout failure, and synchronously seeds positions before asynchronous layout completes.

Changes

Auto-layout resilience

Layer / File(s) Summary
Fallback grid positioning
src/frontend/src/utils/layoutUtils.ts, src/frontend/src/utils/__tests__/layoutUtils.test.ts
Adds deterministic grid positions with unique numeric coordinates and tests empty and populated node lists.
ELK graph resilience
src/frontend/src/utils/layoutUtils.ts, src/frontend/src/utils/__tests__/layoutUtils.test.ts
Filters ports without handles, supplies fallback edge IDs, catches ELK failures, and tests handle-less and edge-free graphs.
Synchronous flow positioning
src/frontend/src/utils/reactflowUtils.ts, src/frontend/src/utils/__tests__/layoutUtils.test.ts
Validates both position coordinates, seeds fallback positions before awaiting layout, and tests immediate positioning during flow processing.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FlowProcessor
  participant PositionFallback
  participant LayoutEngine
  participant ReactFlow
  FlowProcessor->>PositionFallback: seed positions for invalid nodes
  PositionFallback-->>FlowProcessor: numeric grid positions
  FlowProcessor->>ReactFlow: provide immediately positioned nodes
  FlowProcessor->>LayoutEngine: compute asynchronous layout
  LayoutEngine-->>FlowProcessor: return laid-out nodes or failure
Loading

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Quality And Coverage ⚠️ Warning Good happy-path and sync regression coverage, but no test mocks ELK rejection or checks non-finite positions like Infinity. Add one test that rejects elk.layout and asserts fallback grid positions, plus a needsLayout case for Infinity/non-finite coordinates.
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: rendering position-less flows without crashing the canvas.
Linked Issues check ✅ Passed The changes address both linked defects by fixing ELK input handling and seeding positions before async layout completes.
Out of Scope Changes check ✅ Passed The PR stays focused on the reported layout crash and supporting tests, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Test Coverage For New Implementations ✅ Passed PASS: Added src/frontend/src/utils/tests/layoutUtils.test.ts, a properly named frontend test suite covering the new layout fallback, needsLayout, and processFlows regressions.
Test File Naming And Structure ✅ Passed layoutUtils.test.ts is a Jest unit test with .test.ts naming, clear describe/it blocks, descriptive names, and positive/negative/error coverage matching repo conventions.
Excessive Mock Usage Warning ✅ Passed The new test suite uses real code paths and lightweight helpers only; no jest.mock/spyOn/vi.fn or heavy stubbing appears in the touched tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/position-less-flow-canvas-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/utils/__tests__/layoutUtils.test.ts`:
- Around line 83-104: Extend the “getLayoutedNodes with handle-less edges” tests
with a focused error-case test that mocks or spies on elk.layout to reject, then
verifies getLayoutedNodes still returns all nodes with numeric grid positions.
Preserve the existing successful-layout assertions and use the module’s
established ELK dependency injection or mocking approach.

In `@src/frontend/src/utils/reactflowUtils.ts`:
- Around line 615-618: Update the coordinate validation predicate in
reactflowUtils.ts at lines 615-618 to use Number.isFinite for both
node.position.x and node.position.y, preserving the existing layout-required
behavior for invalid coordinates. In
src/frontend/src/utils/__tests__/layoutUtils.test.ts at lines 77-80, add an
assertion verifying that a node with x: Infinity requires layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a769df13-0176-4d06-b7a2-4a4f0003cc11

📥 Commits

Reviewing files that changed from the base of the PR and between 720cabc and 9702b33.

📒 Files selected for processing (3)
  • src/frontend/src/utils/__tests__/layoutUtils.test.ts
  • src/frontend/src/utils/layoutUtils.ts
  • src/frontend/src/utils/reactflowUtils.ts

Comment thread src/frontend/src/utils/__tests__/layoutUtils.test.ts
Comment thread src/frontend/src/utils/reactflowUtils.ts Outdated
Addresses review feedback on #14258.

`needsLayout` used a typeof + NaN check, which let `Infinity` through: it is a
number and is not NaN, so a node with an infinite coordinate skipped the
synchronous seeding and could still reach React Flow as an unusable position.
`Number.isFinite` does not coerce, so it subsumes the typeof check while also
rejecting NaN and ±Infinity.

Also adds the missing error-path coverage for the ELK fallback: the previous
tests only exercised successful layouts, never the catch branch. The new test
mocks elk.layout to reject and asserts getLayoutedNodes resolves to the
deterministic grid rather than propagating.

Both tests fail without their corresponding fix.
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 47%
47.15% (67584/143333) 70.09% (9546/13619) 45.92% (1566/3410)

Unit Test Results

Tests Skipped Failures Errors Time
5387 0 💤 0 ❌ 0 🔥 21m 3s ⏱️

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.27%. Comparing base (8c56958) to head (0287bc8).
⚠️ Report is 2 commits behind head on release-1.12.0.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                @@
##           release-1.12.0   #14258     +/-   ##
=================================================
  Coverage           61.26%   61.27%             
=================================================
  Files                2347     2341      -6     
  Lines              238587   236643   -1944     
  Branches            35590    33294   -2296     
=================================================
- Hits               146180   145005   -1175     
+ Misses              90585    89875    -710     
+ Partials             1822     1763     -59     
Flag Coverage Δ
frontend 59.74% <100.00%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/frontend/src/utils/layoutUtils.ts 100.00% <100.00%> (+73.75%) ⬆️
src/frontend/src/utils/reactflowUtils.ts 58.09% <100.00%> (+2.06%) ⬆️

... and 185 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erichare
erichare merged commit edfe9dc into release-1.12.0 Jul 25, 2026
194 of 196 checks passed
@erichare
erichare deleted the fix/position-less-flow-canvas-crash branch July 25, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flows with nodes missing position crash the canvas; the auto-layout fallback is broken two ways

1 participant