Skip to content

Commit 8bba0d9

Browse files
committed
allow using headers() in runtime prefetches
1 parent f982260 commit 8bba0d9

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
@@ -675,6 +675,7 @@ async function generateRuntimePrefetchResult(
675675
prerenderResumeDataCache,
676676
renderResumeDataCache,
677677
rootParams,
678+
requestStore.headers,
678679
requestStore.cookies,
679680
requestStore.draftMode
680681
)
@@ -685,6 +686,7 @@ async function generateRuntimePrefetchResult(
685686
prerenderResumeDataCache,
686687
renderResumeDataCache,
687688
rootParams,
689+
requestStore.headers,
688690
requestStore.cookies,
689691
requestStore.draftMode,
690692
onError
@@ -706,6 +708,7 @@ async function prospectiveRuntimeServerPrerender(
706708
prerenderResumeDataCache: PrerenderResumeDataCache | null,
707709
renderResumeDataCache: RenderResumeDataCache | null,
708710
rootParams: Params,
711+
headers: PrerenderStoreModernRuntime['headers'],
709712
cookies: PrerenderStoreModernRuntime['cookies'],
710713
draftMode: PrerenderStoreModernRuntime['draftMode']
711714
) {
@@ -756,6 +759,7 @@ async function prospectiveRuntimeServerPrerender(
756759
// We only need task sequencing in the final prerender.
757760
runtimeStagePromise: null,
758761
// These are not present in regular prerenders, but allowed in a runtime prerender.
762+
headers,
759763
cookies,
760764
draftMode,
761765
}
@@ -841,6 +845,7 @@ async function finalRuntimeServerPrerender(
841845
prerenderResumeDataCache: PrerenderResumeDataCache | null,
842846
renderResumeDataCache: RenderResumeDataCache | null,
843847
rootParams: Params,
848+
headers: PrerenderStoreModernRuntime['headers'],
844849
cookies: PrerenderStoreModernRuntime['cookies'],
845850
draftMode: PrerenderStoreModernRuntime['draftMode'],
846851
onError: (err: unknown) => string | undefined
@@ -891,6 +896,7 @@ async function finalRuntimeServerPrerender(
891896
// Used to separate the "Static" stage from the "Runtime" stage.
892897
runtimeStagePromise,
893898
// These are not present in regular prerenders, but allowed in a runtime prerender.
899+
headers,
894900
cookies,
895901
draftMode,
896902
}

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
@@ -3409,63 +3409,6 @@ describe('Cache Components Errors', () => {
34093409
})
34103410
}
34113411
})
3412-
3413-
describe('with `headers()`', () => {
3414-
if (isNextDev) {
3415-
it('should show a redbox error', async () => {
3416-
const browser = await next.browser('/use-cache-private-headers')
3417-
3418-
if (isTurbopack) {
3419-
await expect(browser).toDisplayRedbox(`
3420-
{
3421-
"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",
3422-
"environmentLabel": null,
3423-
"label": "Runtime Error",
3424-
"source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
3425-
> 25 | await headers()
3426-
| ^",
3427-
"stack": [
3428-
"Private app/use-cache-private-headers/page.tsx (25:18)",
3429-
],
3430-
}
3431-
`)
3432-
} else {
3433-
await expect(browser).toDisplayRedbox(`
3434-
{
3435-
"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",
3436-
"environmentLabel": null,
3437-
"label": "Runtime Error",
3438-
"source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
3439-
> 25 | await headers()
3440-
| ^",
3441-
"stack": [
3442-
"Private app/use-cache-private-headers/page.tsx (25:18)",
3443-
],
3444-
}
3445-
`)
3446-
}
3447-
})
3448-
} else {
3449-
// TODO: With prefetch sentinels this should yield a build error.
3450-
it('should not fail the build and show no runtime error (caught in userland)', async () => {
3451-
await prerender('/use-cache-private-headers')
3452-
await next.start({ skipBuild: true })
3453-
cliOutputLength = next.cliOutput.length
3454-
3455-
const browser = await next.browser('/use-cache-private-headers', {
3456-
pushErrorAsConsoleLog: true,
3457-
})
3458-
3459-
expect(await browser.elementById('private').text()).toBe('Private')
3460-
3461-
expect(await browser.log()).not.toContainEqual(
3462-
expect.objectContaining({ source: 'error' })
3463-
)
3464-
3465-
expect(next.cliOutput.slice(cliOutputLength)).not.toInclude('Error')
3466-
})
3467-
}
3468-
})
34693412
})
34703413

34713414
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)