[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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@SidiEyelSidiEyel 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?

…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.
Fixesreact#37178

@dianatofficialdianatofficial left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice improvement. Clean separation of concerns.

@github-actions

github-actionsBot commented Sep 1, 2026

Copy link
Copy Markdown

The build for this commit did not complete, so there is no size report. See the workflow run for details.

Generated by sizebot against 3708998

@SidiEyel

Copy link
Copy Markdown
Author

Gentle ping on this one. I re-checked against current main: the bug is still present — #37178 is still open, abortTaskSoft still isn't called from flushSegment's inline path or from flushCompletedBoundary, so an outlined boundary whose suspending fallback is abandoned still keeps the parent boundary from completing. The branch is behind main now; happy to rebase onto the latest whenever that's useful for review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@SidiEyel@dianatofficial