From 9baecbf02b2b53746f259b77293288aa9d8968f8 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 12 Aug 2025 16:46:56 -0700 Subject: [PATCH] [Fizz] Avoid hanging when suspending after aborting while rendering (#34192) This fixes an edge case where you abort the render while rendering a component that ends up Suspending. It technically only applied if you were deep enough to be inside `renderNode` and was not susceptible to hanging if the abort + suspending component was being tried inside retryRenderTask/retryReplaytask. The fix is to preempt the thenable checks in renderNode and check if the request is aborting and if so just bubble up to the task handler. The reason this hung before is a new task would get scheduled after we had aborted every other task (minus the currently rendering one). This led to a situation where the task count would not hit zero. --- .../src/__tests__/ReactDOMFizzServer-test.js | 54 +++++++++++++++++-- packages/react-server/src/ReactFizzServer.js | 8 ++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 22d2279578f..8442fa3c131 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -94,9 +94,7 @@ describe('ReactDOMFizzServer', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server'); - if (__EXPERIMENTAL__) { - ReactDOMFizzStatic = require('react-dom/static'); - } + ReactDOMFizzStatic = require('react-dom/static'); Stream = require('stream'); Suspense = React.Suspense; use = React.use; @@ -10784,4 +10782,54 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue // Instead we assert that we never emitted the fallback of the Suspense boundary around the body. expect(streamedContent).not.toContain(randomTag); }); + + it('should be able to Suspend after aborting in the same component without hanging the render', async () => { + const controller = new AbortController(); + + const promise1 = new Promise(() => {}); + function AbortAndSuspend() { + controller.abort('boom'); + return React.use(promise1); + } + + function App() { + return ( + + + + {/* + The particular code path that was problematic required the Suspend to happen in renderNode + rather than retryRenderTask so we render the aborting function inside a host component + intentionally here + */} +
+ +
+
+ + + ); + } + + const errors = []; + await act(async () => { + const result = await ReactDOMFizzStatic.prerenderToNodeStream(, { + signal: controller.signal, + onError(e) { + errors.push(e); + }, + }); + + result.prelude.pipe(writable); + }); + + expect(errors).toEqual(['boom']); + + expect(getVisibleChildren(document)).toEqual( + + + loading... + , + ); + }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index d619385ec7d..8681d03b229 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -4155,7 +4155,9 @@ function renderNode( getSuspendedThenable() : thrownValue; - if (typeof x === 'object' && x !== null) { + if (request.status === ABORTING) { + // We are aborting so we can just bubble up to the task by falling through + } else if (typeof x === 'object' && x !== null) { // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); @@ -4254,7 +4256,9 @@ function renderNode( getSuspendedThenable() : thrownValue; - if (typeof x === 'object' && x !== null) { + if (request.status === ABORTING) { + // We are aborting so we can just bubble up to the task by falling through + } else if (typeof x === 'object' && x !== null) { // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any);