Skip to content

Commit

Permalink
fix: clarify Dynamic API calls in wrong context (#62143)
Browse files Browse the repository at this point in the history
### What?

An unactionable error is thrown when `headers()`, `cookies()` or other
Dynamic API functions are called outside the render/request context.
This PR clarifies what the user can do to fix the problem.

### Why?

The current error is  hard to understand

> Error: Invariant: `cookies` expects to have requestAsyncStorage, none
available.

### How?

I am adding a dedicated error page and rephrasing the error message.

Closes NEXT-2509
  • Loading branch information
balazsorban44 authored Feb 19, 2024
1 parent 031cf70 commit 971843d
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 31 deletions.
45 changes: 45 additions & 0 deletions errors/next-dynamic-api-wrong-context.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Dynamic API was called outside request
---

#### Why This Error Occurred

A Dynamic API was called outside a request scope. (Eg.: Global scope).

Note that Dynamic APIs could have been called deep inside other modules/functions (eg.: third-party libraries) that are not immediately visible.

#### Possible Ways to Fix It

Make sure that all Dynamic API calls happen in a request scope.

Example:

```diff
// app/page.ts
import { cookies } from 'next/headers'

- const cookieStore = cookies()
export default function Page() {
+ const cookieStore = cookies()
return ...
}
```

```diff
// app/foo/route.ts
import { headers } from 'next/headers'

- const headersList = headers()
export async function GET() {
+ const headersList = headers()
return ...
}
```

### Useful Links

- [`headers()` function](https://nextjs.org/docs/app/api-reference/functions/headers)
- [`cookies()` function](https://nextjs.org/docs/app/api-reference/functions/cookies)
- [`draftMode()` function](https://nextjs.org/docs/app/api-reference/functions/draft-mode)
- [`unstable_noStore()` function](https://nextjs.org/docs/app/api-reference/functions/unstable_noStore)
- [`unstable_cache()` function](https://nextjs.org/docs/app/api-reference/functions/unstable_cache)
8 changes: 2 additions & 6 deletions packages/next/src/client/components/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export function headers() {
}
}

const requestStore = getExpectedRequestStore(callingExpression)
return requestStore.headers
return getExpectedRequestStore(callingExpression).headers
}

export function cookies() {
Expand All @@ -54,10 +53,7 @@ export function cookies() {
const requestStore = getExpectedRequestStore(callingExpression)

const asyncActionStore = actionAsyncStorage.getStore()
if (
asyncActionStore &&
(asyncActionStore.isAction || asyncActionStore.isAppRoute)
) {
if (asyncActionStore?.isAction || asyncActionStore?.isAppRoute) {
// We can't conditionally return different types here based on the context.
// To avoid confusion, we always return the readonly type here.
return requestStore.mutableCookies as unknown as ReadonlyRequestCookies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ export const requestAsyncStorage: RequestAsyncStorage =

export function getExpectedRequestStore(callingExpression: string) {
const store = requestAsyncStorage.getStore()
if (!store) {
throw new Error(
`Invariant: \`${callingExpression}\` expects to have requestAsyncStorage, none available.`
)
}
return store
if (store) return store
throw new Error(
`\`${callingExpression}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,3 @@ export type StaticGenerationAsyncStorage =

export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage =
createAsyncLocalStorage()

export function getExpectedStaticGenerationStore(callingExpression: string) {
const store = staticGenerationAsyncStorage.getStore()
if (!store) {
throw new Error(
`Invariant: \`${callingExpression}\` expects to have staticGenerationAsyncStorage, none available.`
)
}
return store
}
14 changes: 6 additions & 8 deletions packages/next/src/server/web/spec-extension/unstable-cache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type {
StaticGenerationStore,
StaticGenerationAsyncStorage,
} from '../../../client/components/static-generation-async-storage.external'
import type { StaticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
import type { IncrementalCache } from '../../lib/incremental-cache'

import { staticGenerationAsyncStorage as _staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
Expand Down Expand Up @@ -62,8 +59,10 @@ export function unstable_cache<T extends Callback>(
tags?: string[]
} = {}
): T {
const staticGenerationAsyncStorage: StaticGenerationAsyncStorage =
(fetch as any).__nextGetStaticStore?.() || _staticGenerationAsyncStorage
const staticGenerationAsyncStorage =
((fetch as any).__nextGetStaticStore?.() as
| StaticGenerationAsyncStorage
| undefined) ?? _staticGenerationAsyncStorage

if (options.revalidate === 0) {
throw new Error(
Expand Down Expand Up @@ -94,8 +93,7 @@ export function unstable_cache<T extends Callback>(
}`

const cachedCb = async (...args: any[]) => {
const store: undefined | StaticGenerationStore =
staticGenerationAsyncStorage?.getStore()
const store = staticGenerationAsyncStorage.getStore()

// We must be able to find the incremental cache otherwise we throw
const maybeIncrementalCache:
Expand Down
2 changes: 1 addition & 1 deletion test/development/acceptance-app/rsc-runtime-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ createNextDescribe(
const errorDescription = await getRedboxDescription(browser)

expect(errorDescription).toContain(
`Error: Invariant: \`cookies\` expects to have requestAsyncStorage, none available.`
'Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context'
)
})

Expand Down

0 comments on commit 971843d

Please sign in to comment.