Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: clarify Dynamic API calls in wrong context #62143

Merged
merged 17 commits into from
Feb 19, 2024
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
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