diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index ec58249fcd9e2..7ebad8e808308 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -676,6 +676,7 @@ async function generateRuntimePrefetchResult(
prerenderResumeDataCache,
renderResumeDataCache,
rootParams,
+ requestStore.headers,
requestStore.cookies,
requestStore.draftMode
)
@@ -686,6 +687,7 @@ async function generateRuntimePrefetchResult(
prerenderResumeDataCache,
renderResumeDataCache,
rootParams,
+ requestStore.headers,
requestStore.cookies,
requestStore.draftMode,
onError
@@ -707,6 +709,7 @@ async function prospectiveRuntimeServerPrerender(
prerenderResumeDataCache: PrerenderResumeDataCache | null,
renderResumeDataCache: RenderResumeDataCache | null,
rootParams: Params,
+ headers: PrerenderStoreModernRuntime['headers'],
cookies: PrerenderStoreModernRuntime['cookies'],
draftMode: PrerenderStoreModernRuntime['draftMode']
) {
@@ -757,6 +760,7 @@ async function prospectiveRuntimeServerPrerender(
// We only need task sequencing in the final prerender.
runtimeStagePromise: null,
// These are not present in regular prerenders, but allowed in a runtime prerender.
+ headers,
cookies,
draftMode,
}
@@ -842,6 +846,7 @@ async function finalRuntimeServerPrerender(
prerenderResumeDataCache: PrerenderResumeDataCache | null,
renderResumeDataCache: RenderResumeDataCache | null,
rootParams: Params,
+ headers: PrerenderStoreModernRuntime['headers'],
cookies: PrerenderStoreModernRuntime['cookies'],
draftMode: PrerenderStoreModernRuntime['draftMode'],
onError: (err: unknown) => string | undefined
@@ -892,6 +897,7 @@ async function finalRuntimeServerPrerender(
// Used to separate the "Static" stage from the "Runtime" stage.
runtimeStagePromise,
// These are not present in regular prerenders, but allowed in a runtime prerender.
+ headers,
cookies,
draftMode,
}
diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts
index a963d0d8255fc..50870fa103c64 100644
--- a/packages/next/src/server/app-render/work-unit-async-storage.external.ts
+++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts
@@ -120,6 +120,7 @@ export interface PrerenderStoreModernRuntime
*/
readonly runtimeStagePromise: Promise | null
+ readonly headers: RequestStore['headers']
readonly cookies: RequestStore['cookies']
readonly draftMode: RequestStore['draftMode']
}
@@ -294,10 +295,7 @@ export interface PrivateUseCacheStore extends CommonUseCacheStore {
*/
readonly runtimeStagePromise: Promise | null
- /**
- * As opposed to the public cache store, the private cache store is allowed to
- * access the request cookies.
- */
+ readonly headers: ReadonlyHeaders
readonly cookies: ReadonlyRequestCookies
/**
diff --git a/packages/next/src/server/request/cookies.ts b/packages/next/src/server/request/cookies.ts
index 191b7735fe4a6..269484074262d 100644
--- a/packages/next/src/server/request/cookies.ts
+++ b/packages/next/src/server/request/cookies.ts
@@ -126,10 +126,11 @@ export function cookies(): Promise {
makeUntrackedCookies(workUnitStore.cookies)
)
case 'private-cache':
+ // Private caches are delayed until the runtime stage in use-cache-wrapper,
+ // so we don't need an additional delay here.
if (process.env.__NEXT_CACHE_COMPONENTS) {
return makeUntrackedCookies(workUnitStore.cookies)
}
-
return makeUntrackedExoticCookies(workUnitStore.cookies)
case 'request':
trackDynamicDataInDynamicRender(workUnitStore)
diff --git a/packages/next/src/server/request/headers.ts b/packages/next/src/server/request/headers.ts
index bb5867073209e..aad290f479068 100644
--- a/packages/next/src/server/request/headers.ts
+++ b/packages/next/src/server/request/headers.ts
@@ -12,6 +12,7 @@ import {
type PrerenderStoreModern,
} from '../app-render/work-unit-async-storage.external'
import {
+ delayUntilRuntimeStage,
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
@@ -92,20 +93,13 @@ export function headers(): Promise {
workStore.invalidDynamicUsageError ??= error
throw error
}
- case 'private-cache': {
- const error = new Error(
- `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`
- )
- Error.captureStackTrace(error, headers)
- workStore.invalidDynamicUsageError ??= error
- throw error
- }
case 'unstable-cache':
throw new Error(
`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`
)
case 'prerender':
case 'prerender-client':
+ case 'private-cache':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
@@ -125,7 +119,6 @@ export function headers(): Promise {
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender':
- case 'prerender-runtime':
return makeHangingHeaders(workStore, workUnitStore)
case 'prerender-client':
const exportName = '`headers`'
@@ -152,6 +145,18 @@ export function headers(): Promise {
workStore,
workUnitStore
)
+ case 'prerender-runtime':
+ return delayUntilRuntimeStage(
+ workUnitStore,
+ makeUntrackedHeaders(workUnitStore.headers)
+ )
+ case 'private-cache':
+ // Private caches are delayed until the runtime stage in use-cache-wrapper,
+ // so we don't need an additional delay here.
+ if (process.env.__NEXT_CACHE_COMPONENTS) {
+ return makeUntrackedHeaders(workUnitStore.headers)
+ }
+ return makeUntrackedExoticHeaders(workUnitStore.headers)
case 'request':
trackDynamicDataInDynamicRender(workUnitStore)
diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts
index b911020caa21c..85a505d0e50e6 100644
--- a/packages/next/src/server/use-cache/use-cache-wrapper.ts
+++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts
@@ -211,6 +211,7 @@ function createUseCacheStore(
outerWorkUnitStore
),
rootParams: outerWorkUnitStore.rootParams,
+ headers: outerWorkUnitStore.headers,
cookies: outerWorkUnitStore.cookies,
}
} else {
diff --git a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
index bcf2709469e9d..4875e6c6616c1 100644
--- a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
+++ b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
@@ -3406,63 +3406,6 @@ describe('Cache Components Errors', () => {
})
}
})
-
- describe('with `headers()`', () => {
- if (isNextDev) {
- it('should show a redbox error', async () => {
- const browser = await next.browser('/use-cache-private-headers')
-
- if (isTurbopack) {
- await expect(browser).toDisplayRedbox(`
- {
- "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",
- "environmentLabel": null,
- "label": "Runtime Error",
- "source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
- > 25 | await headers()
- | ^",
- "stack": [
- "Private app/use-cache-private-headers/page.tsx (25:18)",
- ],
- }
- `)
- } else {
- await expect(browser).toDisplayRedbox(`
- {
- "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",
- "environmentLabel": null,
- "label": "Runtime Error",
- "source": "app/use-cache-private-headers/page.tsx (25:18) @ Private
- > 25 | await headers()
- | ^",
- "stack": [
- "Private app/use-cache-private-headers/page.tsx (25:18)",
- ],
- }
- `)
- }
- })
- } else {
- // TODO: With prefetch sentinels this should yield a build error.
- it('should not fail the build and show no runtime error (caught in userland)', async () => {
- await prerender('/use-cache-private-headers')
- await next.start({ skipBuild: true })
- cliOutputLength = next.cliOutput.length
-
- const browser = await next.browser('/use-cache-private-headers', {
- pushErrorAsConsoleLog: true,
- })
-
- expect(await browser.elementById('private').text()).toBe('Private')
-
- expect(await browser.log()).not.toContainEqual(
- expect.objectContaining({ source: 'error' })
- )
-
- expect(next.cliOutput.slice(cliOutputLength)).not.toInclude('Error')
- })
- }
- })
})
describe('Sync IO - Current Time - Date()', () => {
diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/layout.tsx b/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/layout.tsx
deleted file mode 100644
index 745e32b8a8d23..0000000000000
--- a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/layout.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function Root({ children }: { children: React.ReactNode }) {
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/page.tsx b/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/page.tsx
deleted file mode 100644
index ea3db2a9b0f2b..0000000000000
--- a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/use-cache-private-headers/page.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { headers } from 'next/headers'
-import { Suspense } from 'react'
-
-export default function Page() {
- return (
- <>
-
- This page uses `headers()` inside `'use cache: private'`, which triggers
- an error at runtime.
-
- Loading...
}>
-
-
- >
- )
-}
-
-async function Private() {
- 'use cache: private'
-
- // Reading headers in a cache context is not allowed. We're try/catching here
- // to ensure that, in dev mode, this error is shown even when it's caught in
- // userland.
- try {
- await headers()
- } catch {}
-
- return Private
-}
diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/page.tsx
index 54b66be9a1a57..e28378a63b623 100644
--- a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/page.tsx
+++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/page.tsx
@@ -23,6 +23,12 @@ export default async function Page() {
prefetch={true}
/>
+
+
+
+
+
+ This page performs sync IO after a headers() call, so we should only see
+ the error in a runtime prefetch
+
+ Loading 1...}>
+
+
+
+ )
+}
+
+async function RuntimePrefetchable() {
+ await headers()
+ return Timestamp: {Date.now()}
+}
diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/headers/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/headers/page.tsx
new file mode 100644
index 0000000000000..22b40e7af4bfc
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/headers/page.tsx
@@ -0,0 +1,45 @@
+import { headers } from 'next/headers'
+import { Suspense } from 'react'
+import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared'
+import { connection } from 'next/server'
+
+export const unstable_prefetch = 'unstable_runtime'
+
+export default async function Page() {
+ return (
+
+
+
+ This page uses headers and some uncached IO, so parts of it should be
+ runtime-prefetchable.
+
+ Loading 1...}>
+
+
+
+ )
+}
+
+async function RuntimePrefetchable() {
+ const headersStore = await headers()
+ const headerValue = headersStore.get('host') === null ? 'missing' : 'present'
+ await cachedDelay([__filename, headerValue])
+ return (
+
+
+ Loading 2...
}>
+
+
+
+ )
+}
+
+async function Dynamic() {
+ await uncachedIO()
+ await connection()
+ return (
+
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/headers/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/headers/page.tsx
new file mode 100644
index 0000000000000..947bfe37f57e7
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/headers/page.tsx
@@ -0,0 +1,51 @@
+import { headers } from 'next/headers'
+import { Suspense } from 'react'
+import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared'
+import { connection } from 'next/server'
+
+export const unstable_prefetch = 'unstable_runtime'
+
+export default async function Page() {
+ return (
+
+
+
+ This page uses headers (from inside a private cache) and some uncached
+ IO, so parts of it should be runtime-prefetchable.
+
+ Loading 1...}>
+
+
+
+ )
+}
+
+async function RuntimePrefetchable() {
+ const headerValue = await privateCache()
+ return (
+
+
+ Loading 2...
}>
+
+
+
+ )
+}
+
+async function privateCache() {
+ 'use cache: private'
+ const headersStore = await headers()
+ const headerValue = headersStore.get('host') === null ? 'missing' : 'present'
+ await cachedDelay([__filename, headerValue])
+ return headerValue
+}
+
+async function Dynamic() {
+ await uncachedIO()
+ await connection()
+ return (
+
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx
index 42e0687fddcd5..c8042f4d3c088 100644
--- a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx
+++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx
@@ -16,6 +16,15 @@ export default async function Page() {
+
+ headers + dynamic content
+
+
+
search params + dynamic content
@@ -60,6 +69,14 @@ export default async function Page() {
+
+ headers in private cache + dynamic content
+
+
dynamic params in private cache + dynamic content
@@ -112,6 +129,14 @@ export default async function Page() {
+
+ headers() promise passed to public cache + dynamic content
+
+
dynamic params promise passed to public cache + dynamic content