Catch layout error in global-error (#52654)

When there's a runtime error showing in root layout (server components), it should be able to catch by `global-error`.

For server components, we caught it and gonna render the error fallback components (either not-found or error page), and the response status is `200`, and since we'll display error dev overlay in developmenet mode so we only render `global-error` for production.

So that you can catch more errors with `global-error` and maybe do potential error tracking on client side.

Follow up of #52573
Closes NEXT-1442

minor refactor: move `appUsingSizeAdjust` into `Metadata` component so that we can just tune the flag as prop
This commit is contained in:
Jiachi Liu 2023-07-14 02:43:40 +02:00 committed by GitHub
parent 3cde104d64
commit 79227ee74a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 79 deletions

View file

@ -25,11 +25,13 @@ export async function MetadataTree({
pathname,
searchParams,
getDynamicParamFromSegment,
appUsingSizeAdjust,
}: {
tree: LoaderTree
pathname: string
searchParams: { [key: string]: any }
getDynamicParamFromSegment: GetDynamicParamFromSegment
appUsingSizeAdjust: boolean
}) {
const metadataContext = {
pathname,
@ -56,6 +58,8 @@ export async function MetadataTree({
IconsMetadata({ icons: metadata.icons }),
])
if (appUsingSizeAdjust) elements.push(<meta name="next-size-adjust" />)
return (
<>
{elements.map((el, index) => {

View file

@ -17,7 +17,10 @@ import type { RequestAsyncStorage } from '../../client/components/request-async-
import React from 'react'
import { NotFound as DefaultNotFound } from '../../client/components/error'
import { createServerComponentRenderer } from './create-server-components-renderer'
import {
createServerComponentRenderer,
ErrorHtml,
} from './create-server-components-renderer'
import { ParsedUrlQuery } from 'querystring'
import { NextParsedUrlQuery } from '../request-meta'
@ -195,7 +198,7 @@ export async function renderToHTMLOrFlight(
serverActionsBodySizeLimit,
} = renderOpts
const appUsingSizeAdjust = nextFontManifest?.appUsingSizeAdjust
const appUsingSizeAdjust = !!nextFontManifest?.appUsingSizeAdjust
const clientReferenceManifest = renderOpts.clientReferenceManifest!
@ -1216,8 +1219,8 @@ export async function renderToHTMLOrFlight(
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
{appUsingSizeAdjust ? <meta name="next-size-adjust" /> : null}
</>
),
injectedCSS: new Set(),
@ -1259,12 +1262,12 @@ export async function renderToHTMLOrFlight(
/** GlobalError can be either the default error boundary or the overwritten app/global-error.js **/
ComponentMod.GlobalError as typeof import('../../client/components/error-boundary').GlobalError
let serverComponentsInlinedTransformStream: TransformStream<
const serverComponentsInlinedTransformStream: TransformStream<
Uint8Array,
Uint8Array
> = new TransformStream()
let serverErrorComponentsInlinedTransformStream: TransformStream<
const serverErrorComponentsInlinedTransformStream: TransformStream<
Uint8Array,
Uint8Array
> = new TransformStream()
@ -1367,6 +1370,7 @@ export async function renderToHTMLOrFlight(
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
)
@ -1384,22 +1388,15 @@ export async function renderToHTMLOrFlight(
assetPrefix={assetPrefix}
initialCanonicalUrl={pathname}
initialTree={initialTree}
initialHead={
<>
{createMetadata(loaderTree)}
{appUsingSizeAdjust ? <meta name="next-size-adjust" /> : null}
</>
}
initialHead={<>{createMetadata(loaderTree)}</>}
globalErrorComponent={GlobalError}
notFound={
NotFound ? (
<html id="__next_error__">
<body>
{createMetadata(loaderTree)}
{notFoundStyles}
<NotFound />
</body>
</html>
<ErrorHtml>
{createMetadata(loaderTree)}
{notFoundStyles}
<NotFound />
</ErrorHtml>
) : undefined
}
asNotFound={props.asNotFound}
@ -1480,16 +1477,16 @@ export async function renderToHTMLOrFlight(
let polyfillsFlushed = false
let flushedErrorMetaTagsUntilIndex = 0
const getServerInsertedHTML = () => {
const getServerInsertedHTML = (serverCapturedErrors: Error[]) => {
// Loop through all the errors that have been captured but not yet
// flushed.
const errorMetaTags = []
for (
;
flushedErrorMetaTagsUntilIndex < allCapturedErrors.length;
flushedErrorMetaTagsUntilIndex < serverCapturedErrors.length;
flushedErrorMetaTagsUntilIndex++
) {
const error = allCapturedErrors[flushedErrorMetaTagsUntilIndex]
const error = serverCapturedErrors[flushedErrorMetaTagsUntilIndex]
if (isNotFoundError(error)) {
errorMetaTags.push(
<meta name="robots" content="noindex" key={error.digest} />
@ -1571,7 +1568,8 @@ export async function renderToHTMLOrFlight(
dataStream: serverComponentsInlinedTransformStream.readable,
generateStaticHTML:
staticGenerationStore.isStaticGeneration || generateStaticHTML,
getServerInsertedHTML,
getServerInsertedHTML: () =>
getServerInsertedHTML(allCapturedErrors),
serverInsertedHTMLToHead: true,
...validateRootLayout,
})
@ -1610,23 +1608,6 @@ export async function renderToHTMLOrFlight(
res.setHeader('Location', getURLFromRedirectError(err))
}
const defaultErrorComponent = (
<html id="__next_error__">
<head>
{/* @ts-expect-error allow to use async server component */}
<MetadataTree
key={requestId}
tree={emptyLoaderTree}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
/>
{appUsingSizeAdjust ? <meta name="next-size-adjust" /> : null}
</head>
<body></body>
</html>
)
const use404Error = res.statusCode === 404
const useDefaultError = res.statusCode < 400 || res.statusCode === 307
@ -1643,48 +1624,45 @@ export async function renderToHTMLOrFlight(
? interopDefault(await rootLayoutModule())
: null
const serverErrorElement = useDefaultError
? defaultErrorComponent
: React.createElement(
createServerComponentRenderer(
async () => {
// only pass plain object to client
return (
<>
{/* @ts-expect-error allow to use async server component */}
<MetadataTree
key={requestId}
tree={emptyLoaderTree}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={
getDynamicParamFromSegment
}
/>
{use404Error ? (
const serverErrorElement = (
<ErrorHtml
head={
// @ts-expect-error allow to use async server component
<MetadataTree
key={requestId}
tree={emptyLoaderTree}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
}
>
{useDefaultError
? null
: React.createElement(
createServerComponentRenderer(
async () => {
return (
<>
<RootLayout params={{}}>
{notFoundStyles}
<NotFound />
</RootLayout>
{use404Error ? (
<RootLayout params={{}}>
{notFoundStyles}
<meta name="robots" content="noindex" />
<NotFound />
</RootLayout>
) : undefined}
</>
) : (
<GlobalError
error={{
message: err?.message,
digest: err?.digest,
}}
/>
)}
</>
)
},
ComponentMod,
serverErrorComponentsRenderOpts,
serverComponentsErrorHandler,
nonce
)
},
ComponentMod,
serverErrorComponentsRenderOpts,
serverComponentsErrorHandler,
nonce
)
)
)}
</ErrorHtml>
)
const renderStream = await renderToInitialStream({
ReactDOMServer: require('react-dom/server.edge'),
@ -1713,7 +1691,7 @@ export async function renderToHTMLOrFlight(
: serverErrorComponentsInlinedTransformStream
).readable,
generateStaticHTML: staticGenerationStore.isStaticGeneration,
getServerInsertedHTML,
getServerInsertedHTML: () => getServerInsertedHTML([]),
serverInsertedHTMLToHead: true,
...validateRootLayout,
})

View file

@ -75,3 +75,18 @@ export function createServerComponentRenderer<Props>(
return use(response)
}
}
export function ErrorHtml({
head,
children,
}: {
head?: React.ReactNode
children?: React.ReactNode
}) {
return (
<html id="__next_error__">
<head>{head}</head>
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,14 @@
'use client'
export default function GlobalError({ error }) {
return (
<html>
<head></head>
<body>
<h1>Global Error</h1>
<p id="error">{`Global error: ${error?.message}`}</p>
{error?.digest && <p id="digest">{error?.digest}</p>}
</body>
</html>
)
}

View file

@ -0,0 +1,5 @@
export default function layout() {
throw new Error('Global error: layout error')
}
export const revalidate = 0

View file

@ -0,0 +1,3 @@
export default function page() {
return <div>Page</div>
}

View file

@ -0,0 +1,30 @@
import { getRedboxHeader, hasRedbox } from 'next-test-utils'
import { createNextDescribe } from 'e2e-utils'
async function testDev(browser, errorRegex) {
expect(await hasRedbox(browser, true)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(errorRegex)
}
createNextDescribe(
'app dir - global error - layout error',
{
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev }) => {
it('should render global error for error in server components', async () => {
const browser = await next.browser('/')
if (isNextDev) {
await testDev(browser, /Global error: layout error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
}
})
}
)