Skip to content

Commit 4e0b112

Browse files
authored
Improve error handling for headers/cookies/draftMode in 'use cache' (#81716)
This ensures that we show a proper error with an error stack (potentially source-mapped) when accessing `headers`/`cookies`/`draftMode` in `'use cache'`, even when caught in user-land code. For `searchParams` (currently triggering a timeout error) we'll need a slightly different solution, which will be handled in a future PR. The approach chosen here is somewhat temporary, as we'd like to implement compile-time errors instead for accessing any kind of request data in `'use cache'` functions. However, this would require a larger change to our bundlers. closes NAR-201
1 parent 63090e2 commit 4e0b112

File tree

19 files changed

+601
-193
lines changed

19 files changed

+601
-193
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ import {
135135
PreludeState,
136136
consumeDynamicAccess,
137137
type DynamicAccess,
138+
logDisallowedDynamicError,
138139
} from './dynamic-rendering'
139140
import {
140141
getClientComponentLoaderMetrics,
@@ -1456,7 +1457,8 @@ async function renderToHTMLOrFlightImpl(
14561457
// If we encountered any unexpected errors during build we fail the
14571458
// prerendering phase and the build.
14581459
if (workStore.invalidDynamicUsageError) {
1459-
throw workStore.invalidDynamicUsageError
1460+
logDisallowedDynamicError(workStore, workStore.invalidDynamicUsageError)
1461+
throw new StaticGenBailoutError()
14601462
}
14611463
if (response.digestErrorsMap.size) {
14621464
const buildFailingError = response.digestErrorsMap.values().next().value
@@ -3047,7 +3049,8 @@ async function prerenderToStream(
30473049
// We don't need to continue the prerender process if we already
30483050
// detected invalid dynamic usage in the initial prerender phase.
30493051
if (workStore.invalidDynamicUsageError) {
3050-
throw workStore.invalidDynamicUsageError
3052+
logDisallowedDynamicError(workStore, workStore.invalidDynamicUsageError)
3053+
throw new StaticGenBailoutError()
30513054
}
30523055

30533056
let initialServerResult

packages/next/src/server/app-render/dynamic-rendering.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,10 @@ export enum PreludeState {
678678
Errored = 2,
679679
}
680680

681-
function logDisallowedDynamicError(workStore: WorkStore, error: Error): void {
681+
export function logDisallowedDynamicError(
682+
workStore: WorkStore,
683+
error: Error
684+
): void {
682685
console.error(error)
683686

684687
if (!workStore.dev) {
@@ -700,11 +703,6 @@ export function throwIfDisallowedDynamic(
700703
dynamicValidation: DynamicValidationState,
701704
serverDynamic: DynamicTrackingState
702705
): void {
703-
if (workStore.invalidDynamicUsageError) {
704-
logDisallowedDynamicError(workStore, workStore.invalidDynamicUsageError)
705-
throw new StaticGenBailoutError()
706-
}
707-
708706
if (prelude !== PreludeState.Full) {
709707
if (dynamicValidation.hasSuspenseAboveBody) {
710708
// This route has opted into allowing fully dynamic rendering

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ export function cookies(): Promise<ReadonlyRequestCookies> {
8181
if (workUnitStore) {
8282
switch (workUnitStore.type) {
8383
case 'cache':
84-
throw new Error(
84+
const error = new Error(
8585
`Route ${workStore.route} used "cookies" inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "cookies" 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`
8686
)
87+
Error.captureStackTrace(error, cookies)
88+
workStore.invalidDynamicUsageError ??= error
89+
throw error
8790
case 'unstable-cache':
8891
throw new Error(
8992
`Route ${workStore.route} used "cookies" 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 "cookies" 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`

packages/next/src/server/request/draft-mode.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,13 @@ class DraftMode {
229229
public enable() {
230230
// We have a store we want to track dynamic data access to ensure we
231231
// don't statically generate routes that manipulate draft mode.
232-
trackDynamicDraftMode('draftMode().enable()')
232+
trackDynamicDraftMode('draftMode().enable()', this.enable)
233233
if (this._provider !== null) {
234234
this._provider.enable()
235235
}
236236
}
237237
public disable() {
238-
trackDynamicDraftMode('draftMode().disable()')
238+
trackDynamicDraftMode('draftMode().disable()', this.disable)
239239
if (this._provider !== null) {
240240
this._provider.disable()
241241
}
@@ -286,63 +286,69 @@ function createDraftModeAccessError(
286286
)
287287
}
288288

289-
function trackDynamicDraftMode(expression: string) {
290-
const store = workAsyncStorage.getStore()
289+
function trackDynamicDraftMode(expression: string, constructorOpt: Function) {
290+
const workStore = workAsyncStorage.getStore()
291291
const workUnitStore = workUnitAsyncStorage.getStore()
292-
if (store) {
292+
293+
if (workStore) {
293294
// We have a store we want to track dynamic data access to ensure we
294295
// don't statically generate routes that manipulate draft mode.
295296
if (workUnitStore?.phase === 'after') {
296297
throw new Error(
297-
`Route ${store.route} used "${expression}" inside \`after\`. The enabled status of draftMode can be read inside \`after\` but you cannot enable or disable draftMode. See more info here: https://nextjs.org/docs/app/api-reference/functions/after`
298+
`Route ${workStore.route} used "${expression}" inside \`after\`. The enabled status of draftMode can be read inside \`after\` but you cannot enable or disable draftMode. See more info here: https://nextjs.org/docs/app/api-reference/functions/after`
298299
)
299300
}
300301

301-
if (store.dynamicShouldError) {
302+
if (workStore.dynamicShouldError) {
302303
throw new StaticGenBailoutError(
303-
`Route ${store.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
304+
`Route ${workStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
304305
)
305306
}
306307

307308
if (workUnitStore) {
308309
switch (workUnitStore.type) {
309-
case 'cache':
310-
throw new Error(
311-
`Route ${store.route} used "${expression}" inside "use cache". The enabled status of draftMode can be read in caches but you must not enable or disable draftMode inside a cache. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`
310+
case 'cache': {
311+
const error = new Error(
312+
`Route ${workStore.route} used "${expression}" inside "use cache". The enabled status of draftMode can be read in caches but you must not enable or disable draftMode inside a cache. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`
312313
)
314+
Error.captureStackTrace(error, constructorOpt)
315+
workStore.invalidDynamicUsageError ??= error
316+
throw error
317+
}
313318
case 'unstable-cache':
314319
throw new Error(
315-
`Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)". The enabled status of draftMode can be read in caches but you must not enable or disable draftMode inside a cache. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
320+
`Route ${workStore.route} used "${expression}" inside a function cached with "unstable_cache(...)". The enabled status of draftMode can be read in caches but you must not enable or disable draftMode inside a cache. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
316321
)
317-
case 'prerender':
322+
case 'prerender': {
318323
const error = new Error(
319-
`Route ${store.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers`
324+
`Route ${workStore.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers`
320325
)
321326
return abortAndThrowOnSynchronousRequestDataAccess(
322-
store.route,
327+
workStore.route,
323328
expression,
324329
error,
325330
workUnitStore
326331
)
332+
}
327333
case 'prerender-client':
328334
const exportName = '`draftMode`'
329335
throw new InvariantError(
330336
`${exportName} must not be used within a client component. Next.js should be preventing ${exportName} from being included in client components statically, but did not in this case.`
331337
)
332338
case 'prerender-ppr':
333339
return postponeWithTracking(
334-
store.route,
340+
workStore.route,
335341
expression,
336342
workUnitStore.dynamicTracking
337343
)
338344
case 'prerender-legacy':
339345
workUnitStore.revalidate = 0
340346

341347
const err = new DynamicServerError(
342-
`Route ${store.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
348+
`Route ${workStore.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
343349
)
344-
store.dynamicUsageDescription = expression
345-
store.dynamicUsageStack = err.stack
350+
workStore.dynamicUsageDescription = expression
351+
workStore.dynamicUsageStack = err.stack
346352

347353
throw err
348354
case 'request':

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ export function headers(): Promise<ReadonlyHeaders> {
7979
if (workUnitStore) {
8080
switch (workUnitStore.type) {
8181
case 'cache':
82-
throw new Error(
82+
const error = new Error(
8383
`Route ${workStore.route} used "headers" inside "use 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/messages/next-request-in-use-cache`
8484
)
85+
Error.captureStackTrace(error, headers)
86+
workStore.invalidDynamicUsageError ??= error
87+
throw error
8588
case 'unstable-cache':
8689
throw new Error(
8790
`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`

0 commit comments

Comments
 (0)