Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ async function generateRuntimePrefetchResult(
prerenderResumeDataCache,
renderResumeDataCache,
rootParams,
requestStore.headers,
requestStore.cookies,
requestStore.draftMode
)
Expand All @@ -686,6 +687,7 @@ async function generateRuntimePrefetchResult(
prerenderResumeDataCache,
renderResumeDataCache,
rootParams,
requestStore.headers,
requestStore.cookies,
requestStore.draftMode,
onError
Expand All @@ -707,6 +709,7 @@ async function prospectiveRuntimeServerPrerender(
prerenderResumeDataCache: PrerenderResumeDataCache | null,
renderResumeDataCache: RenderResumeDataCache | null,
rootParams: Params,
headers: PrerenderStoreModernRuntime['headers'],
cookies: PrerenderStoreModernRuntime['cookies'],
draftMode: PrerenderStoreModernRuntime['draftMode']
) {
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export interface PrerenderStoreModernRuntime
*/
readonly runtimeStagePromise: Promise<void> | null

readonly headers: RequestStore['headers']
readonly cookies: RequestStore['cookies']
readonly draftMode: RequestStore['draftMode']
}
Expand Down Expand Up @@ -294,10 +295,7 @@ export interface PrivateUseCacheStore extends CommonUseCacheStore {
*/
readonly runtimeStagePromise: Promise<void> | 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

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/request/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ export function cookies(): Promise<ReadonlyRequestCookies> {
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)
Expand Down
23 changes: 14 additions & 9 deletions packages/next/src/server/request/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type PrerenderStoreModern,
} from '../app-render/work-unit-async-storage.external'
import {
delayUntilRuntimeStage,
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
Expand Down Expand Up @@ -92,20 +93,13 @@ export function headers(): Promise<ReadonlyHeaders> {
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':
Expand All @@ -125,7 +119,6 @@ export function headers(): Promise<ReadonlyHeaders> {
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-runtime':
return makeHangingHeaders(workStore, workUnitStore)
case 'prerender-client':
const exportName = '`headers`'
Expand All @@ -152,6 +145,18 @@ export function headers(): Promise<ReadonlyHeaders> {
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)

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ function createUseCacheStore(
outerWorkUnitStore
),
rootParams: outerWorkUnitStore.rootParams,
headers: outerWorkUnitStore.headers,
cookies: outerWorkUnitStore.cookies,
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export default async function Page() {
prefetch={true}
/>
</li>
<li>
<DebugLinkAccordion
href="/errors/sync-io-after-runtime-api/headers"
prefetch={true}
/>
</li>
<li>
<DebugLinkAccordion
href="/errors/sync-io-after-runtime-api/dynamic-params/123"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { headers } from 'next/headers'
import { Suspense } from 'react'
import { DebugRenderKind } from '../../../../shared'

export default async function Page() {
return (
<main>
<DebugRenderKind />
<p id="intro">
This page performs sync IO after a headers() call, so we should only see
the error in a runtime prefetch
</p>
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 1...</div>}>
<RuntimePrefetchable />
</Suspense>
</main>
)
}

async function RuntimePrefetchable() {
await headers()
return <div id="timestamp">Timestamp: {Date.now()}</div>
}
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<DebugRenderKind />
<p>
This page uses headers and some uncached IO, so parts of it should be
runtime-prefetchable.
</p>
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 1...</div>}>
<RuntimePrefetchable />
</Suspense>
</main>
)
}

async function RuntimePrefetchable() {
const headersStore = await headers()
const headerValue = headersStore.get('host') === null ? 'missing' : 'present'
await cachedDelay([__filename, headerValue])
return (
<div style={{ border: '1px solid blue', padding: '1em' }}>
<div id="header-value">{`Header: ${headerValue}`}</div>
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 2...</div>}>
<Dynamic />
</Suspense>
</div>
)
}

async function Dynamic() {
await uncachedIO()
await connection()
return (
<div style={{ border: '1px solid tomato', padding: '1em' }}>
<div id="dynamic-content">Dynamic content</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<DebugRenderKind />
<p>
This page uses headers (from inside a private cache) and some uncached
IO, so parts of it should be runtime-prefetchable.
</p>
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 1...</div>}>
<RuntimePrefetchable />
</Suspense>
</main>
)
}

async function RuntimePrefetchable() {
const headerValue = await privateCache()
return (
<div style={{ border: '1px solid blue', padding: '1em' }}>
<div id="header-value">{`Header: ${headerValue}`}</div>
<Suspense fallback={<div style={{ color: 'grey' }}>Loading 2...</div>}>
<Dynamic />
</Suspense>
</div>
)
}

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 (
<div style={{ border: '1px solid tomato', padding: '1em' }}>
<div id="dynamic-content">Dynamic content</div>
</div>
)
}
Loading
Loading