From e50f36948c9905ed2d8803f680af5f326f29a7ad Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 13 Aug 2024 10:03:54 -0700 Subject: [PATCH] [Flight] erroring after abort should not result in unhandled rejection When I implemented the ability to abort synchronoulsy in flight I made it possible for erroring async server components to cause an unhandled rejection error. In the current implementation if you abort during the synchronous phase of a Function Component and then throw an error in the synchronous phase React will not attach any promise handlers because it short circuits the thenable treatment and throws an AbortSigil instead. This change updates the rendering logic to ignore the rejecting component. --- .../src/__tests__/ReactFlightDOM-test.js | 69 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 31 +++++---- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index cd0bb994a7aa6..77a2241734d3e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2485,4 +2485,73 @@ describe('ReactFlightDOM', () => { , ); }); + + it('can error synchronously after aborting without an unhandled rejection error', async () => { + function App() { + return ( +
+ loading...

}> + +
+
+ ); + } + + const abortRef = {current: null}; + + async function ComponentThatAborts() { + abortRef.current(); + throw new Error('boom'); + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + + await serverAct(() => { + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + abortRef.current = abort; + pipe(flightWritable); + }); + + assertConsoleErrorDev([ + 'The render was aborted by the server without a reason.', + ]); + + const response = + ReactServerDOMClient.createFromReadableStream(flightReadable); + + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + function ClientApp() { + return use(response); + } + + const shellErrors = []; + await serverAct(async () => { + ReactDOMFizzServer.renderToPipeableStream( + React.createElement(ClientApp), + { + onShellError(error) { + shellErrors.push(error.message); + }, + }, + ).pipe(fizzWritable); + }); + assertConsoleErrorDev([ + 'The render was aborted by the server without a reason.', + ]); + + expect(shellErrors).toEqual([]); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(getMeaningfulChildren(container)).toEqual( +
+

loading...

+
, + ); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f680a3d97e1fc..10ceee8075bd3 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -997,6 +997,8 @@ function callWithDebugContextInDEV( } } +const voidHandler = () => {}; + function renderFunctionComponent( request: Request, task: Task, @@ -1101,6 +1103,14 @@ function renderFunctionComponent( } if (request.status === ABORTING) { + if ( + typeof result === 'object' && + result !== null && + typeof result.then === 'function' && + !isClientReference(result) + ) { + result.then(voidHandler, voidHandler); + } // If we aborted during rendering we should interrupt the render but // we don't need to provide an error because the renderer will encode // the abort error as the reason. @@ -1120,18 +1130,15 @@ function renderFunctionComponent( // If the thenable resolves to an element, then it was in a static position, // the return value of a Server Component. That doesn't need further validation // of keys. The Server Component itself would have had a key. - thenable.then( - resolvedValue => { - if ( - typeof resolvedValue === 'object' && - resolvedValue !== null && - resolvedValue.$$typeof === REACT_ELEMENT_TYPE - ) { - resolvedValue._store.validated = 1; - } - }, - () => {}, - ); + thenable.then(resolvedValue => { + if ( + typeof resolvedValue === 'object' && + resolvedValue !== null && + resolvedValue.$$typeof === REACT_ELEMENT_TYPE + ) { + resolvedValue._store.validated = 1; + } + }, voidHandler); } if (thenable.status === 'fulfilled') { return thenable.value;