diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 7614ba3e8dfbc..51a5604bd5554 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -54,13 +54,18 @@ const ReactNoopFlightServer = ReactFlightServer({ }, }); -function render(model: ReactModel): Destination { +type Options = { + onError?: (error: mixed) => void, +}; + +function render(model: ReactModel, options?: Options): Destination { const destination: Destination = []; const bundlerConfig = undefined; const request = ReactNoopFlightServer.createRequest( model, destination, bundlerConfig, + options ? options.onError : undefined, ); ReactNoopFlightServer.startWork(request); return destination; diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js index 3334d34d453da..56769e4255394 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js @@ -15,12 +15,22 @@ import type { import {createRequest, startWork} from 'react-server/src/ReactFlightServer'; +type Options = { + onError?: (error: mixed) => void, +}; + function render( model: ReactModel, destination: Destination, config: BundlerConfig, + options?: Options, ): void { - const request = createRequest(model, destination, config); + const request = createRequest( + model, + destination, + config, + options ? options.onError : undefined, + ); startWork(request); } diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index 1aa02e00d54b5..7ebcaf6b1e8d5 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -26,6 +26,7 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; import { emitRow, resolveModuleMetaData as resolveModuleMetaDataImpl, + close, } from 'ReactFlightDOMRelayServerIntegration'; export type { @@ -146,4 +147,8 @@ export function writeChunk(destination: Destination, chunk: Chunk): boolean { export function completeWriting(destination: Destination) {} -export {close} from 'ReactFlightDOMRelayServerIntegration'; +export {close}; + +export function closeWithError(destination: Destination, error: mixed): void { + close(destination); +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index bbbf5f18ea867..accd749a56d54 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -16,14 +16,24 @@ import { startFlowing, } from 'react-server/src/ReactFlightServer'; +type Options = { + onError?: (error: mixed) => void, +}; + function renderToReadableStream( model: ReactModel, webpackMap: BundlerConfig, + options?: Options, ): ReadableStream { let request; return new ReadableStream({ start(controller) { - request = createRequest(model, controller, webpackMap); + request = createRequest( + model, + controller, + webpackMap, + options ? options.onError : undefined, + ); startWork(request); }, pull(controller) { diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index 312c18b094846..dc78c503aaf39 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -21,12 +21,22 @@ function createDrainHandler(destination, request) { return () => startFlowing(request); } +type Options = { + onError?: (error: mixed) => void, +}; + function pipeToNodeWritable( model: ReactModel, destination: Writable, webpackMap: BundlerConfig, + options?: Options, ): void { - const request = createRequest(model, destination, webpackMap); + const request = createRequest( + model, + destination, + webpackMap, + options ? options.onError : undefined, + ); destination.on('drain', createDrainHandler(destination, request)); startWork(request); } 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 a481d6ebbcd62..a768b0cf0bb64 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -256,6 +256,7 @@ describe('ReactFlightDOM', () => { // @gate experimental it('should progressively reveal server components', async () => { + let reportedErrors = []; const {Suspense} = React; // Client Components @@ -374,7 +375,11 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - ReactServerDOMWriter.pipeToNodeWritable(model, writable, webpackMap); + ReactServerDOMWriter.pipeToNodeWritable(model, writable, webpackMap, { + onError(x) { + reportedErrors.push(x); + }, + }); const response = ReactServerDOMReader.createFromReadableStream(readable); const container = document.createElement('div'); @@ -407,9 +412,12 @@ describe('ReactFlightDOM', () => { '
(loading games)
', ); + expect(reportedErrors).toEqual([]); + + const theError = new Error('Game over'); // Let's *fail* loading games. await act(async () => { - rejectGames(new Error('Game over')); + rejectGames(theError); }); expect(container.innerHTML).toBe( 'Game over
', // TODO: should not have message in prod. ); + expect(reportedErrors).toEqual([theError]); + reportedErrors = []; + // We can now show the sidebar. await act(async () => { resolvePhotos(); @@ -439,6 +450,8 @@ describe('ReactFlightDOM', () => { 'Game over
', // TODO: should not have message in prod. ); + + expect(reportedErrors).toEqual([]); }); // @gate experimental diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index ecea013654f39..8bf0cdf8b41ef 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -25,6 +25,7 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; import { emitRow, + close, resolveModuleMetaData as resolveModuleMetaDataImpl, } from 'ReactFlightNativeRelayServerIntegration'; @@ -146,4 +147,8 @@ export function writeChunk(destination: Destination, chunk: Chunk): boolean { export function completeWriting(destination: Destination) {} -export {close} from 'ReactFlightNativeRelayServerIntegration'; +export {close}; + +export function closeWithError(destination: Destination, error: mixed): void { + close(destination); +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b80798cd66f4b..2a09e21c89a00 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -24,6 +24,7 @@ import { completeWriting, flushBuffered, close, + closeWithError, processModelChunk, processModuleChunk, processSymbolChunk, @@ -83,16 +84,20 @@ export type Request = { completedErrorChunks: Array