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

fix unhandled runtime error when notFound() triggered in generateMetadata w/ parallel routes #65102

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
7 changes: 4 additions & 3 deletions packages/next/src/lib/metadata/metadata.tsx
Expand Up @@ -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({
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
Expand Down
@@ -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>
}
Expand Up @@ -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')
Expand Down