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 77a2241734d3e..718ddf6c5716c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2554,4 +2554,100 @@ describe('ReactFlightDOM', () => { , ); }); + + it('can error synchronously after aborting in a synchronous Component', async () => { + const rejectError = new Error('bam!'); + const rejectedPromise = Promise.reject(rejectError); + rejectedPromise.catch(() => {}); + rejectedPromise.status = 'rejected'; + rejectedPromise.reason = rejectError; + + const resolvedValue =

hello world

; + const resolvedPromise = Promise.resolve(resolvedValue); + resolvedPromise.status = 'fulfilled'; + resolvedPromise.value = resolvedValue; + + function App() { + return ( +
+ loading...

}> + +
+ loading too...

}> + {rejectedPromise} +
+ loading three...

}> + {resolvedPromise} +
+
+ ); + } + + const abortRef = {current: null}; + + // This test is specifically asserting that this works with Sync Server Component + function ComponentThatAborts() { + abortRef.current(); + throw new Error('boom'); + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + + await serverAct(() => { + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + { + onError(e) { + console.error(e); + }, + }, + ); + abortRef.current = abort; + pipe(flightWritable); + }); + + assertConsoleErrorDev([ + 'The render was aborted by the server without a reason.', + 'bam!', + ]); + + 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.', + 'bam!', + ]); + + expect(shellErrors).toEqual([]); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(getMeaningfulChildren(container)).toEqual( +
+

loading...

+

loading too...

+

hello world

+
, + ); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 10ceee8075bd3..6c9536d95acf7 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -382,8 +382,6 @@ export type Request = { didWarnForKey: null | WeakSet, }; -const AbortSigil = {}; - const { TaintRegistryObjects, TaintRegistryValues, @@ -594,6 +592,8 @@ function serializeThenable( const digest = logRecoverableError(request, x, null); emitErrorChunk(request, newTask.id, digest, x); } + newTask.status = ERRORED; + request.abortableTasks.delete(newTask); return newTask.id; } default: { @@ -650,10 +650,10 @@ function serializeThenable( logPostpone(request, postponeInstance.message, newTask); emitPostponeChunk(request, newTask.id, postponeInstance); } else { - newTask.status = ERRORED; const digest = logRecoverableError(request, reason, newTask); emitErrorChunk(request, newTask.id, digest, reason); } + newTask.status = ERRORED; request.abortableTasks.delete(newTask); enqueueFlush(request); }, @@ -1114,7 +1114,8 @@ function renderFunctionComponent( // 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. - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } if ( @@ -1616,7 +1617,8 @@ function renderElement( // lazy initializers are user code and could abort during render // we don't wan to return any value resolved from the lazy initializer // if it aborts so we interrupt rendering here - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } return renderElement( request, @@ -2183,7 +2185,7 @@ function renderModel( } } - if (thrownValue === AbortSigil) { + if (request.status === ABORTING) { task.status = ABORTED; const errorId: number = (request.fatalError: any); if (wasReactNode) { @@ -2357,7 +2359,8 @@ function renderModelDestructive( // lazy initializers are user code and could abort during render // we don't wan to return any value resolved from the lazy initializer // if it aborts so we interrupt rendering here - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } if (__DEV__) { const debugInfo: ?ReactDebugInfo = lazy._debugInfo; @@ -3690,7 +3693,7 @@ function retryTask(request: Request, task: Task): void { } } - if (x === AbortSigil) { + if (request.status === ABORTING) { request.abortableTasks.delete(task); task.status = ABORTED; const errorId: number = (request.fatalError: any); @@ -3909,7 +3912,9 @@ export function stopFlowing(request: Request): void { // This is called to early terminate a request. It creates an error at all pending tasks. export function abort(request: Request, reason: mixed): void { try { - request.status = ABORTING; + if (request.status === OPEN) { + request.status = ABORTING; + } const abortableTasks = request.abortableTasks; // We have tasks to abort. We'll emit one error row and then emit a reference // to that row from every row that's still remaining.