fix unhandled runtime error when notFound() triggered in generateMetadata w/ parallel routes (#65102)
### What When a page throws a `notFound()` error in `generateMetadata`, and the page contains parallel route(s), an unhandled runtime error would be thrown rather than displaying the not found page. ### Why We use the `<MetadataOutlet />` component to throw any errors caught during metadata resolution once the metadata is rendered by React so that it can be caught by an error boundary, so that it doesn't throw during metadata resolution. A promise is tracked & resolved with an error once the metadata tree is rendered. Once the promise resolves, the outlet component will throw. However, every `__PAGE__` segment that would be rendered as part of the page the user is on will render this `<MetadataOutlet />` component. We only need a single outlet per segment as only a single error needs to be thrown & caught. ### How This will only render a `MetadataOutlet` for the first parallel route that is encountered at each segment depth, as we only need a single handler to throw the error. Fixes #65013 Closes NEXT-3222
This commit is contained in:
parent
15f7418fd6
commit
5c9b06256a
14 changed files with 104 additions and 5 deletions
|
@ -96,9 +96,10 @@ export function createMetadataComponents({
|
|||
resolve(undefined)
|
||||
} else {
|
||||
error = resolvedError
|
||||
// If the error triggers in initial metadata resolving, re-resolve with proper error type.
|
||||
// They'll be saved for flight data, when hydrates, it will replaces the SSR'd metadata with this.
|
||||
// for not-found error: resolve not-found metadata
|
||||
// If a not-found error is triggered during metadata resolution, we want to capture the metadata
|
||||
// for the not-found route instead of whatever triggered the error. For all error types, we resolve an
|
||||
// error, which will cause the outlet to throw it so it'll be handled by an error boundary
|
||||
// (either an actual error, or an internal error that renders UI such as the NotFoundBoundary).
|
||||
if (!errorType && isNotFoundError(resolvedError)) {
|
||||
const [notFoundMetadataError, notFoundMetadata, notFoundViewport] =
|
||||
await resolveMetadata({
|
||||
|
|
|
@ -364,7 +364,8 @@ async function createComponentTreeInternal({
|
|||
const parallelRouteMap = await Promise.all(
|
||||
Object.keys(parallelRoutes).map(
|
||||
async (
|
||||
parallelRouteKey
|
||||
parallelRouteKey,
|
||||
parallelRouteIndex
|
||||
): Promise<[string, React.ReactNode, CacheNodeSeedData | null]> => {
|
||||
const isChildrenRouteKey = parallelRouteKey === 'children'
|
||||
const currentSegmentPath: FlightSegmentPath = firstItem
|
||||
|
@ -443,7 +444,11 @@ async function createComponentTreeInternal({
|
|||
injectedJS: injectedJSWithCurrentLayout,
|
||||
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
|
||||
asNotFound,
|
||||
metadataOutlet,
|
||||
// The metadataOutlet is responsible for throwing any errors that were caught during metadata resolution.
|
||||
// We only want to render an outlet once per segment, as otherwise the error will be triggered
|
||||
// multiple times causing an uncaught error.
|
||||
metadataOutlet:
|
||||
parallelRouteIndex === 0 ? metadataOutlet : undefined,
|
||||
ctx,
|
||||
missingSlots,
|
||||
})
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>@bar slot</div>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>@bar slot</div>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
|
||||
export function generateMetadata() {
|
||||
notFound()
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <div>@bar slot</div>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>@foo slot</div>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export function generateMetadata() {
|
||||
return {
|
||||
title: 'Create Next App',
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <div>@foo slot</div>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>@foo slot</div>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
|
||||
export function generateMetadata() {
|
||||
notFound()
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <div>@foobar slot</div>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Layout(props: { foobar: React.ReactNode }) {
|
||||
return <>{props.foobar}</>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>Custom Not Found!</div>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
|
||||
export function generateMetadata() {
|
||||
notFound()
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <h1>Hello from Page</h1>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export function generateMetadata() {
|
||||
return {
|
||||
title: 'Create Next App',
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <h1>Hello from Page</h1>
|
||||
}
|
|
@ -57,6 +57,36 @@ describe('parallel-route-not-found', () => {
|
|||
expect(warnings.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle `notFound()` in generateMetadata on a page that also renders a parallel route', async () => {
|
||||
const browser = await next.browser('/not-found-metadata/page-error')
|
||||
|
||||
// The page's `generateMetadata` function threw a `notFound()` error,
|
||||
// so we should see the not found page.
|
||||
expect(await browser.elementByCss('body').text()).toMatch(
|
||||
/This page could not be found/
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle `notFound()` in a slot', async () => {
|
||||
const browser = await next.browser('/not-found-metadata/slot-error')
|
||||
|
||||
// The page's `generateMetadata` function threw a `notFound()` error,
|
||||
// so we should see the not found page.
|
||||
expect(await browser.elementByCss('body').text()).toMatch(
|
||||
/This page could not be found/
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle `notFound()` in a slot with no `children` slot', async () => {
|
||||
const browser = await next.browser('/not-found-metadata/no-page')
|
||||
|
||||
// The page's `generateMetadata` function threw a `notFound()` error,
|
||||
// so we should see the not found page.
|
||||
expect(await browser.elementByCss('body').text()).toMatch(
|
||||
/This page could not be found/
|
||||
)
|
||||
})
|
||||
|
||||
if (isNextDev) {
|
||||
it('should not log any warnings for a regular not found page', async () => {
|
||||
const browser = await next.browser('/this-page-doesnt-exist')
|
||||
|
|
Loading…
Reference in a new issue