[Fizz] Cancel suspended fallback tasks when flushing a completed boundary - #37181
Open
SidiEyel wants to merge 1 commit into
Open
[Fizz] Cancel suspended fallback tasks when flushing a completed boundary#37181SidiEyel wants to merge 1 commit into
SidiEyel wants to merge 1 commit into
Conversation
…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
SidiEyel
force-pushed
the
fizz-abort-fallback-on-flush
branch
from
August 2, 2026 00:13
82eca72 to
3708998
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #37178.
When a Suspense boundary completes while it is eligible for outlining,
finishedTaskdeliberately 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
$RCwas emitted for it,onAllReadynever 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), andflushCompletedBoundary, 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.abortTaskSoftis also guarded to only abort tasks whose segment is stillPENDING: aborting can reentrantly trigger a flush (e.g.completeShell→onShellReady→pipe), which can now abort the remaining tasks of the samefallbackAbortableTasksset before the outerforEachreaches them. Without the guard the second abort double-finishes the task and corruptspendingRootTasks/allPendingTasks(caught by the existing "two containers" reentrancy test). This mirrors the status guardfinishAbortedTaskalready uses.How did you test this change?
progressiveChunkSize. It fails onmain(onAllReadynever fires, outer boundary never completes) and passes with this change.NODE_DEVbundle: onmainit reproduces the hang (events: ['onShellReady'], only the inner$RC); with this change it completes (events: ['onShellReady', 'onAllReady'],$RCfor both boundaries, content revealed). Also confirmed the repro against publishedreact-dom@19.2.8and19.3.0-canary-cbb046ab-20260731to 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.