From f1589a3d946c56ba14e46c0e5a90733918385f75 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 12 Aug 2025 15:13:41 -0700 Subject: [PATCH] [Fizz] Avoid hanging when suspending after aborting while rendering 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 22d2279578ff0..8442fa3c1316d 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 d619385ec7db3..8681d03b22907 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);