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

Catch layout error in global-error #52654

Merged
merged 11 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) => {
Expand Down
136 changes: 57 additions & 79 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -195,7 +198,7 @@ export async function renderToHTMLOrFlight(
serverActionsBodySizeLimit,
} = renderOpts

const appUsingSizeAdjust = nextFontManifest?.appUsingSizeAdjust
const appUsingSizeAdjust = !!nextFontManifest?.appUsingSizeAdjust

const clientReferenceManifest = renderOpts.clientReferenceManifest!

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -1367,6 +1370,7 @@ export async function renderToHTMLOrFlight(
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
)

Expand All @@ -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}
Expand Down Expand Up @@ -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} />
Expand Down Expand Up @@ -1571,7 +1568,8 @@ export async function renderToHTMLOrFlight(
dataStream: serverComponentsInlinedTransformStream.readable,
generateStaticHTML:
staticGenerationStore.isStaticGeneration || generateStaticHTML,
getServerInsertedHTML,
getServerInsertedHTML: () =>
getServerInsertedHTML(allCapturedErrors),
serverInsertedHTMLToHead: true,
...validateRootLayout,
})
Expand Down Expand Up @@ -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

Expand All @@ -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'),
Expand Down Expand Up @@ -1713,7 +1691,7 @@ export async function renderToHTMLOrFlight(
: serverErrorComponentsInlinedTransformStream
).readable,
generateStaticHTML: staticGenerationStore.isStaticGeneration,
getServerInsertedHTML,
getServerInsertedHTML: () => getServerInsertedHTML([]),
serverInsertedHTMLToHead: true,
...validateRootLayout,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)
}
14 changes: 14 additions & 0 deletions test/e2e/app-dir/global-error/layout-error/app/global-error.js
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions test/e2e/app-dir/global-error/layout-error/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function layout() {
throw new Error('Global error: layout error')
}

export const revalidate = 0
3 changes: 3 additions & 0 deletions test/e2e/app-dir/global-error/layout-error/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function page() {
return <div>Page</div>
}
30 changes: 30 additions & 0 deletions test/e2e/app-dir/global-error/layout-error/index.test.ts
Original file line number Diff line number Diff line change
@@ -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+/)
}
})
}
)