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:
parent
3cde104d64
commit
79227ee74a
13 changed files with 128 additions and 79 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
<ErrorHtml>
|
||||
{createMetadata(loaderTree)}
|
||||
{notFoundStyles}
|
||||
<NotFound />
|
||||
</body>
|
||||
</html>
|
||||
</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,39 +1624,34 @@ 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 */}
|
||||
const serverErrorElement = (
|
||||
<ErrorHtml
|
||||
head={
|
||||
// @ts-expect-error allow to use async server component
|
||||
<MetadataTree
|
||||
key={requestId}
|
||||
tree={emptyLoaderTree}
|
||||
pathname={pathname}
|
||||
searchParams={providedSearchParams}
|
||||
getDynamicParamFromSegment={
|
||||
getDynamicParamFromSegment
|
||||
}
|
||||
getDynamicParamFromSegment={getDynamicParamFromSegment}
|
||||
appUsingSizeAdjust={appUsingSizeAdjust}
|
||||
/>
|
||||
{use404Error ? (
|
||||
}
|
||||
>
|
||||
{useDefaultError
|
||||
? null
|
||||
: React.createElement(
|
||||
createServerComponentRenderer(
|
||||
async () => {
|
||||
return (
|
||||
<>
|
||||
{use404Error ? (
|
||||
<RootLayout params={{}}>
|
||||
{notFoundStyles}
|
||||
<meta name="robots" content="noindex" />
|
||||
<NotFound />
|
||||
</RootLayout>
|
||||
</>
|
||||
) : (
|
||||
<GlobalError
|
||||
error={{
|
||||
message: err?.message,
|
||||
digest: err?.digest,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : undefined}
|
||||
</>
|
||||
)
|
||||
},
|
||||
|
@ -1684,6 +1660,8 @@ export async function renderToHTMLOrFlight(
|
|||
serverComponentsErrorHandler,
|
||||
nonce
|
||||
)
|
||||
)}
|
||||
</ErrorHtml>
|
||||
)
|
||||
|
||||
const renderStream = await renderToInitialStream({
|
||||
|
@ -1713,7 +1691,7 @@ export async function renderToHTMLOrFlight(
|
|||
: serverErrorComponentsInlinedTransformStream
|
||||
).readable,
|
||||
generateStaticHTML: staticGenerationStore.isStaticGeneration,
|
||||
getServerInsertedHTML,
|
||||
getServerInsertedHTML: () => getServerInsertedHTML([]),
|
||||
serverInsertedHTMLToHead: true,
|
||||
...validateRootLayout,
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
5
test/e2e/app-dir/global-error/layout-error/app/layout.js
Normal file
5
test/e2e/app-dir/global-error/layout-error/app/layout.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default function layout() {
|
||||
throw new Error('Global error: layout error')
|
||||
}
|
||||
|
||||
export const revalidate = 0
|
3
test/e2e/app-dir/global-error/layout-error/app/page.js
Normal file
3
test/e2e/app-dir/global-error/layout-error/app/page.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function page() {
|
||||
return <div>Page</div>
|
||||
}
|
30
test/e2e/app-dir/global-error/layout-error/index.test.ts
Normal file
30
test/e2e/app-dir/global-error/layout-error/index.test.ts
Normal 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+/)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
Loading…
Reference in a new issue