diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index e00da0b8f1b05..0788f216e5f30 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -6131,6 +6131,10 @@ export function emitEarlyPreloads( if (onHeaders) { const headers = renderState.headers; if (headers) { + // Even if onHeaders throws we don't want to call this again so + // we drop the headers state from this point onwards. + renderState.headers = null; + let linkHeader = headers.preconnects; if (headers.fontPreloads) { if (linkHeader) { @@ -6205,7 +6209,6 @@ export function emitEarlyPreloads( // it React will not provide any headers onHeaders({}); } - renderState.headers = null; return; } } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 6ed04b64a50e1..874183bb4d541 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3841,6 +3841,30 @@ describe('ReactDOMFizzServer', () => { }); }); + it('logs an error if onHeaders throws but continues the render', async () => { + const errors = []; + function onError(error) { + errors.push(error.message); + } + + function onHeaders(x) { + throw new Error('bad onHeaders'); + } + + let pipe; + await act(() => { + ({pipe} = renderToPipeableStream(
hello
, {onHeaders, onError})); + }); + + expect(errors).toEqual(['bad onHeaders']); + + await act(() => { + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual(
hello
); + }); + describe('error escaping', () => { it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => { window.__outlet = {}; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 70d8de5c19ff2..7482c7adaac8c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -1420,6 +1420,28 @@ describe('ReactDOMFizzStaticBrowser', () => { ); }); + // @gate experimental + it('logs an error if onHeaders throws but continues the prerender', async () => { + const errors = []; + function onError(error) { + errors.push(error.message); + } + + function onHeaders(x) { + throw new Error('bad onHeaders'); + } + + const prerendered = await ReactDOMFizzStatic.prerender(
hello
, { + onHeaders, + onError, + }); + expect(prerendered.postponed).toBe(null); + expect(errors).toEqual(['bad onHeaders']); + + await readIntoContainer(prerendered.prelude); + expect(getVisibleChildren(container)).toEqual(
hello
); + }); + // @gate enablePostpone it('does not bootstrap again in a resume if it bootstraps', async () => { let prerendering = true; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 675b8816a7a0b..8909fccc691a6 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -3220,6 +3220,22 @@ function abortTask(task: Task, request: Request, error: mixed): void { } } +function safelyEmitEarlyPreloads( + request: Request, + shellComplete: boolean, +): void { + try { + emitEarlyPreloads( + request.renderState, + request.resumableState, + shellComplete, + ); + } catch (error) { + // We assume preloads are optimistic and thus non-fatal if errored. + logRecoverableError(request, error); + } +} + // I extracted this function out because we want to ensure we consistently emit preloads before // transitioning to the next request stage and this transition can happen in multiple places in this // implementation. @@ -3232,11 +3248,7 @@ function completeShell(request: Request) { // we should only be calling completeShell when the shell is complete so we // just use a literal here const shellComplete = true; - emitEarlyPreloads( - request.renderState, - request.resumableState, - shellComplete, - ); + safelyEmitEarlyPreloads(request, shellComplete); } // We have completed the shell so the shell can't error anymore. request.onShellError = noop; @@ -3259,7 +3271,7 @@ function completeAll(request: Request) { : // Prerender Request, we use the state of the root segment request.completedRootSegment === null || request.completedRootSegment.status !== POSTPONED; - emitEarlyPreloads(request.renderState, request.resumableState, shellComplete); + safelyEmitEarlyPreloads(request, shellComplete); const onAllReady = request.onAllReady; onAllReady(); } @@ -4124,7 +4136,7 @@ export function startWork(request: Request): void { function enqueueEarlyPreloadsAfterInitialWork(request: Request) { const shellComplete = request.pendingRootTasks === 0; - emitEarlyPreloads(request.renderState, request.resumableState, shellComplete); + safelyEmitEarlyPreloads(request, shellComplete); } function enqueueFlush(request: Request): void { @@ -4168,7 +4180,7 @@ export function prepareForStartFlowingIfBeforeAllReady(request: Request) { request.completedRootSegment === null ? request.pendingRootTasks === 0 : request.completedRootSegment.status !== POSTPONED; - emitEarlyPreloads(request.renderState, request.resumableState, shellComplete); + safelyEmitEarlyPreloads(request, shellComplete); } export function startFlowing(request: Request, destination: Destination): void {