Skip to content

[Fizz] Cancel suspended fallback tasks when flushing a completed boundary - #37181

Open
SidiEyel wants to merge 1 commit into
react:mainfrom
SidiEyel:fizz-abort-fallback-on-flush
Open

[Fizz] Cancel suspended fallback tasks when flushing a completed boundary#37181
SidiEyel wants to merge 1 commit into
react:mainfrom
SidiEyel:fizz-abort-fallback-on-flush

Conversation

@SidiEyel

@SidiEyel SidiEyel commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Fixes #37178.

When a Suspense boundary completes while it is eligible for outlining, finishedTask deliberately keeps its fallback tasks alive, because the fallback may still need to be written if the boundary is outlined during flush:

https://github.com/facebook/react/blob/3a717e42438afac81020cdec297dadb5613a4304/packages/react-server/src/ReactFizzServer.js#L5198-L5200

But nothing cancelled those tasks once the boundary's content actually flushed. If the fallback itself suspended (and never resolved), the abandoned fallback task kept the parent boundary's pending task count from draining: the parent boundary never completed, no $RC was emitted for it, onAllReady never fired, and the request hung with the content sitting unreferenced in the stream — with no error surfaced anywhere. Small boundaries are unaffected because the non-outlineable path cancels fallbacks at completion time, which is why #34694's test (tiny content) passes while the same shape with >500 bytes of content hangs.

This cancels the fallback tasks at the two places where the decision to show the content over the fallback becomes final:

  • flushSegment, when a completed boundary is inlined (the fallback is never written), and
  • flushCompletedBoundary, when the completion instruction is written (the outlined fallback has already been written and is about to be replaced).

Both places already mutate row/task state during flush (finishSuspenseListRow), and the flush loops explicitly support new completed boundaries being scheduled mid-loop, so the parent boundary completing reentrantly is handled by the existing machinery.

abortTaskSoft is also guarded to only abort tasks whose segment is still PENDING: aborting can reentrantly trigger a flush (e.g. completeShellonShellReadypipe), which can now abort the remaining tasks of the same fallbackAbortableTasks set before the outer forEach reaches them. Without the guard the second abort double-finishes the task and corrupts pendingRootTasks/allPendingTasks (caught by the existing "two containers" reentrancy test). This mirrors the status guard finishAbortedTask already uses.

How did you test this change?

  • Added a regression test next to [Fizz] Detatch boundary after flushing segment with boundary #34694's test: same nested-boundary shape with a suspending-forever fallback, but with content large enough to be outlining-eligible and a small progressiveChunkSize. It fails on main (onAllReady never fires, outer boundary never completes) and passes with this change.
  • Ran the repro script from Bug: outer Suspense boundary never completes when an inner boundary's suspending fallback is abandoned after the shell flushed (Fizz streaming, no error surfaced) #37178 against a locally built NODE_DEV bundle: on main it reproduces the hang (events: ['onShellReady'], only the inner $RC); with this change it completes (events: ['onShellReady', 'onAllReady'], $RC for both boundaries, content revealed). Also confirmed the repro against published react-dom@19.2.8 and 19.3.0-canary-cbb046ab-20260731 to verify the bug exists as reported.
  • yarn test Fizz — 14 suites, 333 tests pass (including the "two containers" reentrancy test).
  • yarn test ReactDOMFizzServer --prod — 217 tests pass.
  • yarn test ReactDOMServerIntegration — 1602 tests pass.
  • yarn linc, yarn flow dom-node, yarn prettier — clean.

@meta-cla meta-cla Bot added the CLA Signed label Aug 2, 2026
…dary

When a boundary completes while being eligible for outlining, its fallback
tasks are deliberately kept alive because the fallback may still be written
when the boundary is outlined during flush. But nothing cancelled those
tasks after the boundary's content actually flushed, so a fallback that
itself suspended kept the parent boundary's pending task count from ever
draining: the parent boundary never completed, no completion instruction
was emitted for it, and onAllReady never fired, hanging the request with
the content sitting unreferenced in the stream.

Cancel the fallback tasks at the two places where the decision to show the
content becomes final: when the completed boundary is inlined in
flushSegment and when its completion instruction is written in
flushCompletedBoundary. Also guard abortTaskSoft against tasks that have
already finished, since aborting can reentrantly trigger a flush (e.g. via
onShellReady -> pipe) which can now abort remaining tasks of the same set
before the outer forEach reaches them.

Fixes react#37178
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant