diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index ddc151654b769..bd78cd237640b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -317,11 +317,6 @@ export function parseModelString( } else { const id = parseInt(value.substring(1), 16); const chunk = getChunk(response, id); - if (chunk._status === PENDING) { - throw new Error( - "We didn't expect to see a forward reference. This is a bug in the React Server.", - ); - } return readChunk(chunk); } } 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 4b6995a9edb9d..9de3843764152 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -19,6 +19,7 @@ global.setImmediate = cb => cb(); let act; let clientExports; +let clientModuleError; let webpackMap; let Stream; let React; @@ -26,6 +27,7 @@ let ReactDOMClient; let ReactServerDOMWriter; let ReactServerDOMReader; let Suspense; +let ErrorBoundary; describe('ReactFlightDOM', () => { beforeEach(() => { @@ -33,6 +35,7 @@ describe('ReactFlightDOM', () => { act = require('jest-react').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; + clientModuleError = WebpackMock.clientModuleError; webpackMap = WebpackMock.webpackMap; Stream = require('stream'); @@ -41,6 +44,22 @@ describe('ReactFlightDOM', () => { ReactDOMClient = require('react-dom/client'); ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server'); ReactServerDOMReader = require('react-server-dom-webpack'); + + ErrorBoundary = class extends React.Component { + state = {hasError: false, error: null}; + static getDerivedStateFromError(error) { + return { + hasError: true, + error, + }; + } + render() { + if (this.state.hasError) { + return this.props.fallback(this.state.error); + } + return this.props.children; + } + }; }); function getTestStream() { @@ -319,22 +338,6 @@ describe('ReactFlightDOM', () => { // Client Components - class ErrorBoundary extends React.Component { - state = {hasError: false, error: null}; - static getDerivedStateFromError(error) { - return { - hasError: true, - error, - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback(this.state.error); - } - return this.props.children; - } - } - function MyErrorBoundary({children}) { return (

{e.message}

}> @@ -605,22 +608,6 @@ describe('ReactFlightDOM', () => { it('should be able to complete after aborting and throw the reason client-side', async () => { const reportedErrors = []; - class ErrorBoundary extends React.Component { - state = {hasError: false, error: null}; - static getDerivedStateFromError(error) { - return { - hasError: true, - error, - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback(this.state.error); - } - return this.props.children; - } - } - const {writable, readable} = getTestStream(); const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream(
@@ -661,4 +648,159 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual(['for reasons']); }); + + it('should be able to recover from a direct reference erroring client-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function({prop}) { + return 'This should never render'; + }); + + const ClientReference = clientModuleError(new Error('module init error')); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return res.readRoot(); + } + + await act(async () => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring client-side async', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function({prop}) { + return 'This should never render'; + }); + + let rejectPromise; + const ClientReference = await clientExports( + new Promise((resolve, reject) => { + rejectPromise = reject; + }), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return res.readRoot(); + } + + await act(async () => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + + expect(container.innerHTML).toBe('

(loading)

'); + + await act(async () => { + rejectPromise(new Error('async module init error')); + }); + + expect(container.innerHTML).toBe('

async module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring server-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function({prop}) { + return 'This should never render'; + }); + + // We simulate a bug in the Webpack bundler which causes an error on the server. + for (const id in webpackMap) { + Object.defineProperty(webpackMap, id, { + get: () => { + throw new Error('bug in the bundler'); + }, + }); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return res.readRoot(); + } + + await act(async () => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

bug in the bundler

'); + + expect(reportedErrors).toEqual([]); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 78f78505b6b2a..93d06bf2649a6 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -12,8 +12,12 @@ const Module = require('module'); let webpackModuleIdx = 0; const webpackModules = {}; +const webpackErroredModules = {}; const webpackMap = {}; global.__webpack_require__ = function(id) { + if (webpackErroredModules[id]) { + throw webpackErroredModules[id]; + } return webpackModules[id]; }; @@ -36,6 +40,27 @@ Module._extensions['.client.js'] = previousLoader; exports.webpackMap = webpackMap; exports.webpackModules = webpackModules; +exports.clientModuleError = function clientModuleError(moduleError) { + const idx = '' + webpackModuleIdx++; + webpackErroredModules[idx] = moduleError; + const path = url.pathToFileURL(idx).href; + webpackMap[path] = { + '': { + id: idx, + chunks: [], + name: '', + }, + '*': { + id: idx, + chunks: [], + name: '*', + }, + }; + const mod = {exports: {}}; + nodeLoader(mod, idx); + return mod.exports; +}; + exports.clientExports = function clientExports(moduleExports) { const idx = '' + webpackModuleIdx++; webpackModules[idx] = moduleExports; @@ -53,17 +78,20 @@ exports.clientExports = function clientExports(moduleExports) { }, }; if (typeof moduleExports.then === 'function') { - moduleExports.then(asyncModuleExports => { - for (const name in asyncModuleExports) { - webpackMap[path] = { - [name]: { - id: idx, - chunks: [], - name: name, - }, - }; - } - }); + moduleExports.then( + asyncModuleExports => { + for (const name in asyncModuleExports) { + webpackMap[path] = { + [name]: { + id: idx, + chunks: [], + name: name, + }, + }; + } + }, + () => {}, + ); } for (const name in moduleExports) { webpackMap[path] = {