Skip to content

Commit 3f4db01

Browse files
lubieowocegnoffunstubbable
authored
[Cache Components] Discriminate static shell validation errors by type (#85747)
Prior to this change any "hole" in a prerender that would block the shell was considered an error and you would be presented with a very generic message explaining all the different ways you could have failed this validation check. With this change we use a new technique to validate the static shell which can now tell the difference between waiting on uncached data or runtime data. It also improves the heuristics around generateMetadata and generateViewport errors. Added new error pages for runtime sync IO and ensure we only validate sync IO after runtime data if the page will be validating runtime prefetches. Restored the validation on HMR update so you can get feedback after saving a new file. --- We've also discovered that hanging inputs are not handled correctly. Fixing this is non-trivial and will be done in a follow-up, so for now, we're disabling the failing tests. --------- Co-authored-by: Josh Story <[email protected]> Co-authored-by: Hendrik Liebau <[email protected]>
1 parent 95a0744 commit 3f4db01

File tree

36 files changed

+2976
-1197
lines changed

36 files changed

+2976
-1197
lines changed

errors/next-prerender-dynamic-metadata.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
---
2-
title: Cannot access Request information or uncached data in `generateMetadata()` in an otherwise entirely static route
2+
title: Cannot access Runtime data or uncached data in `generateMetadata()` or file-based Metadata in an otherwise entirely static route
33
---
44

55
## Why This Error Occurred
66

7-
When `cacheComponents` is enabled, Next.js requires that `generateMetadata()` not depend on uncached data or Request data unless some other part of the page also has similar requirements. The reason for this is that while you normally control your intention for what is allowed to be dynamic by adding or removing Suspense boundaries in your Layout and Page components you are not in control of rendering metadata itself.
7+
When `cacheComponents` is enabled, Next.js requires that Document Metadata not depend on uncached data or Runtime data (`cookies()`, `headers()`, `params`, `searchParams`) unless some other part of the page also has similar requirements.
88

9-
The heuristic Next.js uses to understand your intent with `generateMetadata()` is to look at the data requirements of the rest of the route. If other components depend on Request data or uncached data, then we allow `generateMetadata()` to have similar data requirements. If the rest of your page has no dependence on this type of data, we require that `generateMetadata()` also not have this type of data dependence.
9+
Next.js determines if a page is entirely static or partially static by looking at whether any part of the page cannot be prerendered.
10+
11+
Typically you control the which parts of a page can be prerendered by adding `"use cache"` to Components or data functions and by avoiding Runtime data like `cookies()` or `searchParams`. However, Metadata can be defined in functions (`generateMetadata()`) defined far from your Page content. Additionally Metadata can implicitly depend on Runtime data when using file-based Metadata such as an icon inside a route with a dynamic param. It would be easy for Metadata to accidentally make an otherwise entirely static page have a dynamic component.
12+
13+
To prevent anwanted partially dynamic pages, Next.js expects pages that are otherwise entirely prerenderable to also have prerenderable Metadata.
1014

1115
## Possible Ways to Fix It
1216

@@ -141,7 +145,7 @@ export default function Page() {
141145
}
142146
```
143147

144-
Note: The reason to structure this `DynamicMarker` as a self-contained Suspense boundary is to avoid blocking the actual content of the page from being prerendered. When Partial Prerendering is enabled alongside `cacheComponents`, the static shell will still contain all of the prerenderable content, and only the metadata will stream in dynamically.
148+
Note: The reason to structure this `DynamicMarker` as a self-contained Suspense boundary is to avoid blocking the actual content of the page from being prerendered.
145149

146150
## Useful Links
147151

errors/next-prerender-dynamic-viewport.mdx

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,61 @@
11
---
2-
title: Cannot access Request information or uncached data in `generateViewport()`
2+
title: Cannot access Runtime data or uncached data in `generateViewport()`
33
---
44

55
## Why This Error Occurred
66

7-
When `cacheComponents` is enabled, Next.js requires that `generateViewport()` not depend on uncached data or Request data unless you explicitly opt into having a fully dynamic page. If you encountered this error, it means that `generateViewport` depends on one of these types of data and you have not specifically indicated that the affected route should be entirely dynamic.
7+
When `cacheComponents` is enabled, Next.js requires that `generateViewport()` not depend on uncached data or Runtime data (`cookies()`, `headers()`, `params`, `searchParams`) unless you explicitly opt into having a fully dynamic page. If you encountered this error, it means that `generateViewport` depends on one of these types of data and you have not specifically indicated that blocking navigations are acceptable.
88

99
## Possible Ways to Fix It
1010

1111
To fix this issue, you must first determine your goal for the affected route.
1212

13-
Normally, the way you indicate to Next.js that you want to allow reading Request data or uncached external data is by performing this data access inside a component with an ancestor Suspense boundary. With Viewport, however, you aren't directly in control of wrapping the location where this metadata will be rendered, and even if you could wrap it in a Suspense boundary, it would not be correct to render it with a fallback. This is because this metadata is critical to properly loading resources such as images and must be part of the initial App Shell (the initial HTML containing the document head as well as the first paintable UI).
13+
Normally, Next.js ensures every page can produce an initial UI that allows the page to start loading even before uncached data and Runtime data is available. This is accomplished by defining prerenderable UI with Suspense. However viewport metadata is not able to be deferred until after the page loads because it affects initial page load UI.
14+
15+
Ideally, you update `generateViewport` so it does not depend on any uncached data or Runtime data. This allows navigations to appear instant.
16+
17+
However if this is not possibl you can instruct Next.js to allow all navigations to be potentially blocking by wrapping your document `<body>` in a Suspense boundary.
18+
19+
### Caching External Data
20+
21+
When external data is cached, Next.js can prerender with it, which ensures that the App Shell always has the complete viewport metadata available. Consider using `"use cache"` to mark the function producing the external data as cacheable.
22+
23+
Before:
24+
25+
```jsx filename="app/.../layout.tsx"
26+
import { db } from './db'
27+
28+
export async function generateViewport() {
29+
const { width, initialScale } = await db.query('viewport-size')
30+
return {
31+
width,
32+
initialScale,
33+
}
34+
}
35+
36+
export default async function Layout({ children }) {
37+
return ...
38+
}
39+
```
40+
41+
After:
42+
43+
```jsx filename="app/.../layout.tsx"
44+
import { db } from './db'
45+
46+
export async function generateViewport() {
47+
"use cache"
48+
const { width, initialScale } = await db.query('viewport-size')
49+
return {
50+
width,
51+
initialScale,
52+
}
53+
}
54+
55+
export default async function Layout({ children }) {
56+
return ...
57+
}
58+
```
1459

1560
### If you must access Request Data or your external data is uncacheable
1661

@@ -61,47 +106,6 @@ export default function RootLayout({ children }) {
61106
}
62107
```
63108

64-
### Caching External Data
65-
66-
When external data is cached, Next.js can prerender with it, which ensures that the App Shell always has the complete viewport metadata available. Consider using `"use cache"` to mark the function producing the external data as cacheable.
67-
68-
Before:
69-
70-
```jsx filename="app/.../layout.tsx"
71-
import { db } from './db'
72-
73-
export async function generateViewport() {
74-
const { width, initialScale } = await db.query('viewport-size')
75-
return {
76-
width,
77-
initialScale,
78-
}
79-
}
80-
81-
export default async function Layout({ children }) {
82-
return ...
83-
}
84-
```
85-
86-
After:
87-
88-
```jsx filename="app/.../layout.tsx"
89-
import { db } from './db'
90-
91-
export async function generateViewport() {
92-
"use cache"
93-
const { width, initialScale } = await db.query('viewport-size')
94-
return {
95-
width,
96-
initialScale,
97-
}
98-
}
99-
100-
export default async function Layout({ children }) {
101-
return ...
102-
}
103-
```
104-
105109
## Useful Links
106110

107111
- [`generateViewport()`](/docs/app/api-reference/functions/generate-viewport)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
title: Cannot access `crypto.getRandomValue()`, `crypto.randomUUID()`, or another web or node crypto API that generates random values synchronously before other uncached data or `connection()` in a Server Component
3+
---
4+
5+
## Why This Error Occurred
6+
7+
An API that produces a random value synchronously from the Web Crypto API or from Node's `crypto` package was used in a Server Component before accessing other uncached data through APIs like `fetch()` and native database drivers, or the `connection()` API. While typically random crypto values can be guarded behind Runtime data like `cookies()`, `headers()`, `params`, and `searchParams`, this particular route is configured for Runtime Prefetching which makes these APIs available as part of the prefetch request. Accessing random values synchronously without preceding it with uncached data or `await connection()` interferes with the framework's ability to produce a correct prefetch result.
8+
9+
## Possible Ways to Fix It
10+
11+
If the random crypto value is appropriate to be prefetched consider moving it into a Cache Component or Cache Function with the `"use cache"` directive.
12+
13+
If the random crypto value is intended to be generated on every user navigation consider whether an async API exists that achieves the same result. If not consider whether you can move the random crypto value generation later, behind other existing uncached data or Request data access. If there is no way to do this you can always precede the random crypto value generation with Request data access by using `await connection()`.
14+
15+
### Cache the token value
16+
17+
If you are generating a token to talk to a database that itself should be cached move the token generation inside the `"use cache"`.
18+
19+
Before:
20+
21+
```jsx filename="app/page.js"
22+
export const unstable_prefetch = {
23+
mode: 'runtime',
24+
samples: [...],
25+
}
26+
27+
async function getCachedData(token: string, userId: string) {
28+
"use cache"
29+
return db.query(token, userId, ...)
30+
}
31+
32+
export default async function Page({ params }) {
33+
const { userId } = await params
34+
const token = crypto.randomUUID()
35+
const data = await getCachedData(token, userId);
36+
return ...
37+
}
38+
```
39+
40+
After:
41+
42+
```jsx filename="app/page.js"
43+
export const unstable_prefetch = {
44+
mode: 'runtime',
45+
samples: [...],
46+
}
47+
48+
async function getCachedData(userId: string) {
49+
"use cache"
50+
const token = crypto.randomUUID()
51+
return db.query(token, userId, ...)
52+
}
53+
54+
export default async function Page({ params }) {
55+
const { userId } = await params
56+
const data = await getCachedData(userId);
57+
return ...
58+
}
59+
```
60+
61+
### Use an async API at request-time
62+
63+
If you require this random value to be unique per Request and an async version of the API exists switch to it instead. Also ensure that there is a parent Suspense boundary that defines a fallback UI Next.js can use while rendering this component on each Request.
64+
65+
Before:
66+
67+
```jsx filename="app/page.js"
68+
export const unstable_prefetch = {
69+
mode: 'runtime',
70+
samples: [...],
71+
}
72+
73+
import { generateKeySync } from 'node:crypto'
74+
75+
export default async function Page({ params }) {
76+
const { dataId } = await params
77+
const data = await fetchData(dataId)
78+
const key = generateKeySync('hmac', { ... })
79+
const digestedData = await digestDataWithKey(data, key);
80+
return ...
81+
}
82+
```
83+
84+
After:
85+
86+
```jsx filename="app/page.js"
87+
export const unstable_prefetch = {
88+
mode: 'runtime',
89+
samples: [...],
90+
}
91+
92+
import { generateKey } from 'node:crypto'
93+
94+
export default async function Page({ params }) {
95+
const { dataId } = await params
96+
const data = await fetchData(dataId)
97+
const key = await new Promise(resolve => generateKey('hmac', { ... }, key => resolve(key)))
98+
const digestedData = await digestDataWithKey(data, key);
99+
return ...
100+
}
101+
```
102+
103+
### Use `await connection()` at request-time
104+
105+
If you require this random value to be unique per Request and an async version of the API does not exist, call `await connection()`. Also ensure that there is a parent Suspense boundary that defines a fallback UI Next.js can use while rendering this component on each Request.
106+
107+
Before:
108+
109+
```jsx filename="app/page.js"
110+
export const unstable_prefetch = {
111+
mode: 'runtime',
112+
samples: [...],
113+
}
114+
115+
export default async function Page({ params }) {
116+
const { sessionId } = await params
117+
const uuid = crypto.randomUUID()
118+
return <RequestId sessionId={sessionId} id={uuid} />
119+
}
120+
```
121+
122+
After:
123+
124+
```jsx filename="app/page.js"
125+
export const unstable_prefetch = {
126+
mode: 'runtime',
127+
samples: [...],
128+
}
129+
130+
import { connection } from 'next/server'
131+
132+
export default async function Page({ params }) {
133+
await connection()
134+
const { sessionId } = await params
135+
const uuid = crypto.randomUUID()
136+
return <RequestId sessionId={sessionId} id={uuid} />
137+
}
138+
```
139+
140+
## Useful Links
141+
142+
- [`connection` function](/docs/app/api-reference/functions/connection)
143+
- [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
144+
- [Node Crypto API](https://nodejs.org/docs/latest/api/crypto.html)
145+
- [`Suspense` React API](https://react.dev/reference/react/Suspense)

0 commit comments

Comments
 (0)