Skip to content

Commit ad7c756

Browse files
authored
[Cache Components] allow using headers() in runtime prefetches (#83838)
This PR allows usage of `headers()` in runtime prefetches and private caches. The implementation is basically analogous to `cookies()`. Closes NAR-353
1 parent 9d596b1 commit ad7c756

File tree

15 files changed

+289
-109
lines changed

15 files changed

+289
-109
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ async function generateRuntimePrefetchResult(
676676
prerenderResumeDataCache,
677677
renderResumeDataCache,
678678
rootParams,
679+
requestStore.headers,
679680
requestStore.cookies,
680681
requestStore.draftMode
681682
)
@@ -686,6 +687,7 @@ async function generateRuntimePrefetchResult(
686687
prerenderResumeDataCache,
687688
renderResumeDataCache,
688689
rootParams,
690+
requestStore.headers,
689691
requestStore.cookies,
690692
requestStore.draftMode,
691693
onError
@@ -707,6 +709,7 @@ async function prospectiveRuntimeServerPrerender(
707709
prerenderResumeDataCache: PrerenderResumeDataCache | null,
708710
renderResumeDataCache: RenderResumeDataCache | null,
709711
rootParams: Params,
712+
headers: PrerenderStoreModernRuntime['headers'],
710713
cookies: PrerenderStoreModernRuntime['cookies'],
711714
draftMode: PrerenderStoreModernRuntime['draftMode']
712715
) {
@@ -757,6 +760,7 @@ async function prospectiveRuntimeServerPrerender(
757760
// We only need task sequencing in the final prerender.
758761
runtimeStagePromise: null,
759762
// These are not present in regular prerenders, but allowed in a runtime prerender.
763+
headers,
760764
cookies,
761765
draftMode,
762766
}
@@ -842,6 +846,7 @@ async function finalRuntimeServerPrerender(
842846
prerenderResumeDataCache: PrerenderResumeDataCache | null,
843847
renderResumeDataCache: RenderResumeDataCache | null,
844848
rootParams: Params,
849+
headers: PrerenderStoreModernRuntime['headers'],
845850
cookies: PrerenderStoreModernRuntime['cookies'],
846851
draftMode: PrerenderStoreModernRuntime['draftMode'],
847852
onError: (err: unknown) => string | undefined
@@ -892,6 +897,7 @@ async function finalRuntimeServerPrerender(
892897
// Used to separate the "Static" stage from the "Runtime" stage.
893898
runtimeStagePromise,
894899
// These are not present in regular prerenders, but allowed in a runtime prerender.
900+
headers,
895901
cookies,
896902
draftMode,
897903
}

packages/next/src/server/app-render/work-unit-async-storage.external.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export interface PrerenderStoreModernRuntime
120120
*/
121121
readonly runtimeStagePromise: Promise<void> | null
122122

123+
readonly headers: RequestStore['headers']
123124
readonly cookies: RequestStore['cookies']
124125
readonly draftMode: RequestStore['draftMode']
125126
}
@@ -294,10 +295,7 @@ export interface PrivateUseCacheStore extends CommonUseCacheStore {
294295
*/
295296
readonly runtimeStagePromise: Promise<void> | null
296297

297-
/**
298-
* As opposed to the public cache store, the private cache store is allowed to
299-
* access the request cookies.
300-
*/
298+
readonly headers: ReadonlyHeaders
301299
readonly cookies: ReadonlyRequestCookies
302300

303301
/**

packages/next/src/server/request/cookies.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ export function cookies(): Promise<ReadonlyRequestCookies> {
126126
makeUntrackedCookies(workUnitStore.cookies)
127127
)
128128
case 'private-cache':
129+
// Private caches are delayed until the runtime stage in use-cache-wrapper,
130+
// so we don't need an additional delay here.
129131
if (process.env.__NEXT_CACHE_COMPONENTS) {
130132
return makeUntrackedCookies(workUnitStore.cookies)
131133
}
132-
133134
return makeUntrackedExoticCookies(workUnitStore.cookies)
134135
case 'request':
135136
trackDynamicDataInDynamicRender(workUnitStore)

packages/next/src/server/request/headers.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type PrerenderStoreModern,
1313
} from '../app-render/work-unit-async-storage.external'
1414
import {
15+
delayUntilRuntimeStage,
1516
postponeWithTracking,
1617
throwToInterruptStaticGeneration,
1718
trackDynamicDataInDynamicRender,
@@ -92,20 +93,13 @@ export function headers(): Promise<ReadonlyHeaders> {
9293
workStore.invalidDynamicUsageError ??= error
9394
throw error
9495
}
95-
case 'private-cache': {
96-
const error = new Error(
97-
`Route ${workStore.route} used "headers" inside "use cache: private". Accessing "headers" inside a private cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`
98-
)
99-
Error.captureStackTrace(error, headers)
100-
workStore.invalidDynamicUsageError ??= error
101-
throw error
102-
}
10396
case 'unstable-cache':
10497
throw new Error(
10598
`Route ${workStore.route} used "headers" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
10699
)
107100
case 'prerender':
108101
case 'prerender-client':
102+
case 'private-cache':
109103
case 'prerender-runtime':
110104
case 'prerender-ppr':
111105
case 'prerender-legacy':
@@ -125,7 +119,6 @@ export function headers(): Promise<ReadonlyHeaders> {
125119
if (workUnitStore) {
126120
switch (workUnitStore.type) {
127121
case 'prerender':
128-
case 'prerender-runtime':
129122
return makeHangingHeaders(workStore, workUnitStore)
130123
case 'prerender-client':
131124
const exportName = '`headers`'
@@ -152,6 +145,18 @@ export function headers(): Promise<ReadonlyHeaders> {
152145
workStore,
153146
workUnitStore
154147
)
148+
case 'prerender-runtime':
149+
return delayUntilRuntimeStage(
150+
workUnitStore,
151+
makeUntrackedHeaders(workUnitStore.headers)
152+
)
153+
case 'private-cache':
154+
// Private caches are delayed until the runtime stage in use-cache-wrapper,
155+
// so we don't need an additional delay here.
156+
if (process.env.__NEXT_CACHE_COMPONENTS) {
157+
return makeUntrackedHeaders(workUnitStore.headers)
158+
}
159+
return makeUntrackedExoticHeaders(workUnitStore.headers)
155160
case 'request':
156161
trackDynamicDataInDynamicRender(workUnitStore)
157162

packages/next/src/server/use-cache/use-cache-wrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ function createUseCacheStore(
211211
outerWorkUnitStore
212212
),
213213
rootParams: outerWorkUnitStore.rootParams,
214+
headers: outerWorkUnitStore.headers,
214215
cookies: outerWorkUnitStore.cookies,
215216
}
216217
} else {

test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3406,63 +3406,6 @@ describe('Cache Components Errors', () => {
34063406
})
34073407
}
34083408
})
3409-
3410-
describe('with `headers()`', () => {
3411-
if (isNextDev) {
3412-
it('should show a redbox error', async () => {
3413-
const browser = await next.browser('/use-cache-private-headers')
3414-
3415-
if (isTurbopack) {
3416-
await expect(browser).toDisplayRedbox(`
3417-
{
3418-
"description": "Route /use-cache-private-headers used "headers" inside "use cache: private". Accessing "headers" inside a private cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
3419-
"environmentLabel": null,
3420-
"label": "Runtime Error",
3421-
"source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
3422-
> 25 | await headers()
3423-
| ^",
3424-
"stack": [
3425-
"Private app/use-cache-private-headers/page.tsx (25:18)",
3426-
],
3427-
}
3428-
`)
3429-
} else {
3430-
await expect(browser).toDisplayRedbox(`
3431-
{
3432-
"description": "Route /use-cache-private-headers used "headers" inside "use cache: private". Accessing "headers" inside a private cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
3433-
"environmentLabel": null,
3434-
"label": "Runtime Error",
3435-
"source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
3436-
> 25 | await headers()
3437-
| ^",
3438-
"stack": [
3439-
"Private app/use-cache-private-headers/page.tsx (25:18)",
3440-
],
3441-
}
3442-
`)
3443-
}
3444-
})
3445-
} else {
3446-
// TODO: With prefetch sentinels this should yield a build error.
3447-
it('should not fail the build and show no runtime error (caught in userland)', async () => {
3448-
await prerender('/use-cache-private-headers')
3449-
await next.start({ skipBuild: true })
3450-
cliOutputLength = next.cliOutput.length
3451-
3452-
const browser = await next.browser('/use-cache-private-headers', {
3453-
pushErrorAsConsoleLog: true,
3454-
})
3455-
3456-
expect(await browser.elementById('private').text()).toBe('Private')
3457-
3458-
expect(await browser.log()).not.toContainEqual(
3459-
expect.objectContaining({ source: 'error' })
3460-
)
3461-
3462-
expect(next.cliOutput.slice(cliOutputLength)).not.toInclude('Error')
3463-
})
3464-
}
3465-
})
34663409
})
34673410

34683411
describe('Sync IO - Current Time - Date()', () => {

test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/layout.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.

test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/page.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export default async function Page() {
2323
prefetch={true}
2424
/>
2525
</li>
26+
<li>
27+
<DebugLinkAccordion
28+
href="/errors/sync-io-after-runtime-api/headers"
29+
prefetch={true}
30+
/>
31+
</li>
2632
<li>
2733
<DebugLinkAccordion
2834
href="/errors/sync-io-after-runtime-api/dynamic-params/123"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { headers } from 'next/headers'
2+
import { Suspense } from 'react'
3+
import { DebugRenderKind } from '../../../../shared'
4+
5+
export default async function Page() {
6+
return (
7+
<main>
8+
<DebugRenderKind />
9+
<p id="intro">
10+
This page performs sync IO after a headers() call, so we should only see
11+
the error in a runtime prefetch
12+
</p>
13+
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 1...</div>}>
14+
<RuntimePrefetchable />
15+
</Suspense>
16+
</main>
17+
)
18+
}
19+
20+
async function RuntimePrefetchable() {
21+
await headers()
22+
return <div id="timestamp">Timestamp: {Date.now()}</div>
23+
}

0 commit comments

Comments
 (0)