Skip to content

Commit aa7ae42

Browse files
authored
Fix generateMetadata race condition (#63169)
This ensures we properly catch/handle `generateMetadata` errors during eager evaluating of nested `generateMetadata` functions in the tree. Previously if we eager evaluated a child metadata function that threw an error e.g. `notFound()` and but the parent metadata function took longer the thrown error would be an unhandled rejection causing the process to crash depending on the environment. Fixes: NEXT-2588 Closes NEXT-2786
1 parent fb11904 commit aa7ae42

File tree

4 files changed

+68
-6
lines changed

4 files changed

+68
-6
lines changed

packages/next/src/lib/metadata/resolve-metadata.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -620,13 +620,24 @@ function collectMetadataExportPreloading<Data, ResolvedData>(
620620
dynamicMetadataExportFn: DataResolver<Data, ResolvedData>,
621621
resolvers: ((value: ResolvedData) => void)[]
622622
) {
623-
results.push(
624-
dynamicMetadataExportFn(
625-
new Promise<any>((resolve) => {
626-
resolvers.push(resolve)
627-
})
628-
)
623+
const result = dynamicMetadataExportFn(
624+
new Promise<any>((resolve) => {
625+
resolvers.push(resolve)
626+
})
629627
)
628+
629+
if (result instanceof Promise) {
630+
// since we eager execute generateMetadata and
631+
// they can reject at anytime we need to ensure
632+
// we attach the catch handler right away to
633+
// prevent unhandled rejections crashing the process
634+
result.catch((err) => {
635+
return {
636+
__nextError: err,
637+
}
638+
})
639+
}
640+
results.push(result)
630641
}
631642

632643
async function getMetadataFromExport<Data, ResolvedData>(
@@ -681,6 +692,11 @@ async function getMetadataFromExport<Data, ResolvedData>(
681692
resolveParent(currentResolvedMetadata)
682693
metadata =
683694
metadataResult instanceof Promise ? await metadataResult : metadataResult
695+
696+
if (metadata && typeof metadata === 'object' && '__nextError' in metadata) {
697+
// re-throw caught metadata error from preloading
698+
throw metadata['__nextError']
699+
}
684700
} else if (metadataExport !== null && typeof metadataExport === 'object') {
685701
// This metadataExport is the object form
686702
metadata = metadataExport
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ResolvingMetadata } from 'next'
2+
import { notFound } from 'next/navigation'
3+
4+
process.on('unhandledRejection', (rej) => {
5+
console.log('unhandledRejection', rej)
6+
process.exit(1)
7+
})
8+
9+
export const revalidate = 0
10+
11+
export default function Page() {
12+
return notFound()
13+
}
14+
15+
export async function generateMetadata(_, __: ResolvingMetadata) {
16+
return notFound()
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
process.on('unhandledRejection', (rej) => {
2+
console.log('unhandledRejection', rej)
3+
process.exit(1)
4+
})
5+
6+
export const revalidate = 0
7+
8+
export default function Layout({ children }) {
9+
return (
10+
<>
11+
<p>Layout</p>
12+
{children}
13+
</>
14+
)
15+
}
16+
17+
export async function generateMetadata() {
18+
await new Promise((resolve) => {
19+
setTimeout(() => {
20+
process.nextTick(resolve)
21+
}, 2_000)
22+
})
23+
return {}
24+
}

test/e2e/app-dir/metadata/metadata.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,5 +1000,10 @@ createNextDescribe(
10001000
expect(iconHtml).toContain('pages-icon-page')
10011001
expect(ogHtml).toContain('pages-opengraph-image-page')
10021002
})
1003+
1004+
it('should not crash from error thrown during preloading nested generateMetadata', async () => {
1005+
const res = await next.fetch('/dynamic-meta')
1006+
expect(res.status).toBe(404)
1007+
})
10031008
}
10041009
)

0 commit comments

Comments
 (0)