Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure bail out on ssr error in static generation #68764

Merged
merged 14 commits into from
Aug 13, 2024
52 changes: 39 additions & 13 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
createHTMLReactServerErrorHandler,
createHTMLErrorHandler,
type DigestedError,
isUserLandError,
} from './create-error-handler'
import {
getShortDynamicParamType,
Expand Down Expand Up @@ -972,7 +973,14 @@ async function renderToHTMLOrFlightImpl(
// prerendering phase and the build.
if (response.digestErrorsMap.size) {
const buildFailingError = response.digestErrorsMap.values().next().value
throw buildFailingError
if (buildFailingError) throw buildFailingError
}
// Pick first userland SSR error, which is also not a RSC error.
if (response.ssrErrors.length) {
const buildFailingError = response.ssrErrors.find((err) =>
isUserLandError(err)
)
if (buildFailingError) throw buildFailingError
}

const options: RenderResultOptions = {
Expand Down Expand Up @@ -1235,24 +1243,31 @@ async function renderToStream(
// and should omit the error reporter in the SSR layer instead
undefined
)
function onServerRenderError(err: DigestedError) {
const renderSource = reactServerErrorsByDigest.has(err.digest)
? 'react-server-components'
: 'server-rendering'
function onHTMLRenderRSCError(err: DigestedError) {
return renderOpts.onInstrumentationRequestError?.(
err,
req,
createErrorContext(ctx, 'react-server-components')
)
}

function onHTMLRenderSSRError(err: DigestedError) {
return renderOpts.onInstrumentationRequestError?.(
err,
req,
createErrorContext(ctx, renderSource)
createErrorContext(ctx, 'server-rendering')
)
}

const allCapturedErrors: Array<unknown> = []
const htmlRendererErrorHandler = createHTMLErrorHandler(
!!renderOpts.dev,
!!renderOpts.nextExport,
reactServerErrorsByDigest,
allCapturedErrors,
silenceLogger,
onServerRenderError
onHTMLRenderRSCError,
onHTMLRenderSSRError
)

let primaryRenderReactServerStream: null | ReadableStream<Uint8Array> = null
Expand Down Expand Up @@ -1571,6 +1586,7 @@ async function renderToStream(
type PrenderToStringResult = {
stream: ReadableStream<Uint8Array>
digestErrorsMap: Map<string, DigestedError>
ssrErrors: Array<unknown>
dynamicTracking?: null | DynamicTrackingState
err?: unknown
}
Expand Down Expand Up @@ -1645,14 +1661,19 @@ async function prerenderToStream(
// and should omit the error reporter in the SSR layer instead
undefined
)
function onServerRenderError(err: DigestedError) {
const renderSource = reactServerErrorsByDigest.has(err.digest)
? 'react-server-components'
: 'server-rendering'
function onHTMLRenderRSCError(err: DigestedError) {
return renderOpts.onInstrumentationRequestError?.(
err,
req,
createErrorContext(ctx, 'react-server-components')
)
}

function onHTMLRenderSSRError(err: DigestedError) {
return renderOpts.onInstrumentationRequestError?.(
err,
req,
createErrorContext(ctx, renderSource)
createErrorContext(ctx, 'server-rendering')
)
}
const allCapturedErrors: Array<unknown> = []
Expand All @@ -1662,7 +1683,8 @@ async function prerenderToStream(
reactServerErrorsByDigest,
allCapturedErrors,
silenceLogger,
onServerRenderError
onHTMLRenderRSCError,
onHTMLRenderSSRError
)

let dynamicTracking: null | DynamicTrackingState = null
Expand Down Expand Up @@ -1786,6 +1808,7 @@ async function prerenderToStream(
// require the same set so we unify the code path here
return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueDynamicPrerender(prelude, {
getServerInsertedHTML,
}),
Expand Down Expand Up @@ -1833,6 +1856,7 @@ async function prerenderToStream(

return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueStaticPrerender(htmlStream, {
inlinedDataStream: createInlinedDataReadableStream(
inlinedReactServerDataStream,
Expand Down Expand Up @@ -1902,6 +1926,7 @@ async function prerenderToStream(
})
return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueFizzStream(htmlStream, {
inlinedDataStream: createInlinedDataReadableStream(
inlinedReactServerDataStream,
Expand Down Expand Up @@ -2040,6 +2065,7 @@ async function prerenderToStream(
// the response in the caller.
err,
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueFizzStream(fizzStream, {
inlinedDataStream: createInlinedDataReadableStream(
// This is intentionally using the readable datastream from the
Expand Down
21 changes: 17 additions & 4 deletions packages/next/src/server/app-render/create-error-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,19 @@ export function createHTMLErrorHandler(
dev: boolean,
isNextExport: boolean,
reactServerErrors: Map<string, DigestedError>,
allCapturedError: Array<unknown>,
allCapturedErrors: Array<unknown>,
silenceLogger: boolean,
onHTMLRenderError: (err: any) => void
onHTMLRenderRSCError: (err: any) => void,
onHTMLRenderSSRError: (err: any) => void
): ErrorHandler {
return (err: any, errorInfo: any) => {
let isRSCError = false
// If the error already has a digest, respect the original digest,
// so it won't get re-generated into another new error.

if (err.digest) {
if (reactServerErrors.has(err.digest)) {
isRSCError = true
// This error is likely an obfuscated error from react-server.
// We recover the original error here.
err = reactServerErrors.get(err.digest)
Expand All @@ -162,7 +165,7 @@ export function createHTMLErrorHandler(
).toString()
}

allCapturedError.push(err)
allCapturedErrors.push(err)

// If the response was closed, we don't need to log the error.
if (isAbortError(err)) return
Expand Down Expand Up @@ -204,10 +207,20 @@ export function createHTMLErrorHandler(
}

if (!silenceLogger) {
onHTMLRenderError(err)
if (isRSCError) {
onHTMLRenderRSCError(err)
} else {
onHTMLRenderSSRError(err)
}
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}
}

return err.digest
}
}

export function isUserLandError(err: any): boolean {
return (
!isAbortError(err) && !isBailoutToCSRError(err) && !isNextRouterError(err)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
5 changes: 5 additions & 0 deletions test/production/app-dir/client-page-error-bailout/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export default function Page() {
throw new Error('client-page-error')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { nextTestSetup } from 'e2e-utils'

describe('app-dir - client-page-error-bailout', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
})

if (skipped) {
return
}

let stderr = ''
beforeAll(() => {
const onLog = (log: string) => {
stderr += log
}

next.on('stderr', onLog)
})

it('should bail out in static generation build', async () => {
await next.build()
expect(stderr).toContain(
'Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error'
)
expect(stderr).toContain('Error: client-page-error')
})
})
Loading