Skip to content

Commit bb74ece

Browse files
authored
Ensure static generation storage is accessed correctly (vercel#64088)
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> ### What? Primarily, this is fixing the code highlighted here: https://github.com/vercel/next.js/blob/073cd74433146d6d3849110e7dada92351ebb7d4/packages/next/src/server/lib/patch-fetch.ts#L228-L230 As `(fetch as any).__nextGetStaticStore?.()` returns `StaticGenerationAsyncStorage | undefined`, and not `StaticGenerationStore`. A call to `.getStore()` as the previous line does corrects this issue. Secondarily, this improves the `as any` type access being done on the patched fetch object to make it more type safe and easier to work with. Since this was added, some features like the `.external` files were added that allowed files to import the correct async local storage object in client and server environments correctly to allow for direct access. Code across Next.js no-longer uses this mechanism to access the storage, and instead relies on this special treated import. Types were improved within the `patch-fetch.ts` file to allow for safer property access by adding consistent types and guards. ### Why? Without this change, checks like: https://github.com/vercel/next.js/blob/073cd74433146d6d3849110e7dada92351ebb7d4/packages/next/src/server/lib/patch-fetch.ts#L246 Always fail, because when `(fetch as any).__nextGetStaticStore?.()` returns `StaticGenerationAsyncStorage`, it isn't the actual store, so the type is wrong. Closes NEXT-3008
1 parent d9b7244 commit bb74ece

File tree

5 files changed

+69
-67
lines changed

5 files changed

+69
-67
lines changed

packages/next/src/client/components/error-boundary.tsx

+6-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React from 'react'
44
import { usePathname } from './navigation'
55
import { isNextRouterError } from './is-next-router-error'
6+
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external'
67

78
const styles = {
89
error: {
@@ -50,17 +51,12 @@ interface ErrorBoundaryHandlerState {
5051
// function crashes so we can maintain our previous cache
5152
// instead of caching the error page
5253
function HandleISRError({ error }: { error: any }) {
53-
if (typeof (fetch as any).__nextGetStaticStore === 'function') {
54-
const store:
55-
| undefined
56-
| import('./static-generation-async-storage.external').StaticGenerationStore =
57-
(fetch as any).__nextGetStaticStore()?.getStore()
58-
59-
if (store?.isRevalidate || store?.isStaticGeneration) {
60-
console.error(error)
61-
throw error
62-
}
54+
const store = staticGenerationAsyncStorage.getStore()
55+
if (store?.isRevalidate || store?.isStaticGeneration) {
56+
console.error(error)
57+
throw error
6358
}
59+
6460
return null
6561
}
6662

packages/next/src/server/lib/patch-fetch.ts

+58-35
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ import type { FetchMetric } from '../base-http'
1818

1919
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
2020

21+
type Fetcher = typeof fetch
22+
23+
type PatchedFetcher = Fetcher & {
24+
readonly __nextPatched: true
25+
readonly __nextGetStaticStore: () => StaticGenerationAsyncStorage
26+
readonly _nextOriginalFetch: Fetcher
27+
}
28+
29+
function isPatchedFetch(
30+
fetch: Fetcher | PatchedFetcher
31+
): fetch is PatchedFetcher {
32+
return '__nextPatched' in fetch && fetch.__nextPatched === true
33+
}
34+
2135
export function validateRevalidate(
2236
revalidateVal: unknown,
2337
pathname: string
@@ -174,22 +188,16 @@ interface PatchableModule {
174188
staticGenerationAsyncStorage: StaticGenerationAsyncStorage
175189
}
176190

177-
// we patch fetch to collect cache information used for
178-
// determining if a page is static or not
179-
export function patchFetch({
180-
serverHooks,
181-
staticGenerationAsyncStorage,
182-
}: PatchableModule) {
183-
if (!(globalThis as any)._nextOriginalFetch) {
184-
;(globalThis as any)._nextOriginalFetch = globalThis.fetch
185-
}
186-
187-
if ((globalThis.fetch as any).__nextPatched) return
188-
189-
const { DynamicServerError } = serverHooks
190-
const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch
191-
192-
globalThis.fetch = async (
191+
function createPatchedFetcher(
192+
originFetch: Fetcher,
193+
{
194+
serverHooks: { DynamicServerError },
195+
staticGenerationAsyncStorage,
196+
}: PatchableModule
197+
): PatchedFetcher {
198+
// Create the patched fetch function. We don't set the type here, as it's
199+
// verified as the return value of this function.
200+
const patched = async (
193201
input: RequestInfo | URL,
194202
init: RequestInit | undefined
195203
) => {
@@ -211,7 +219,7 @@ export function patchFetch({
211219
const isInternal = (init?.next as any)?.internal === true
212220
const hideSpan = process.env.NEXT_OTEL_FETCH_DISABLED === '1'
213221

214-
return await getTracer().trace(
222+
return getTracer().trace(
215223
isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch,
216224
{
217225
hideSpan,
@@ -225,9 +233,18 @@ export function patchFetch({
225233
},
226234
},
227235
async () => {
228-
const staticGenerationStore: StaticGenerationStore =
229-
staticGenerationAsyncStorage.getStore() ||
230-
(fetch as any).__nextGetStaticStore?.()
236+
// If this is an internal fetch, we should not do any special treatment.
237+
if (isInternal) return originFetch(input, init)
238+
239+
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
240+
241+
// If the staticGenerationStore is not available, we can't do any
242+
// special treatment of fetch, therefore fallback to the original
243+
// fetch implementation.
244+
if (!staticGenerationStore || staticGenerationStore.isDraftMode) {
245+
return originFetch(input, init)
246+
}
247+
231248
const isRequestInput =
232249
input &&
233250
typeof input === 'object' &&
@@ -239,17 +256,6 @@ export function patchFetch({
239256
return value || (isRequestInput ? (input as any)[field] : null)
240257
}
241258

242-
// If the staticGenerationStore is not available, we can't do any
243-
// special treatment of fetch, therefore fallback to the original
244-
// fetch implementation.
245-
if (
246-
!staticGenerationStore ||
247-
isInternal ||
248-
staticGenerationStore.isDraftMode
249-
) {
250-
return originFetch(input, init)
251-
}
252-
253259
let revalidate: number | undefined | false = undefined
254260
const getNextField = (field: 'revalidate' | 'tags') => {
255261
return typeof init?.next?.[field] !== 'undefined'
@@ -718,8 +724,25 @@ export function patchFetch({
718724
}
719725
)
720726
}
721-
;(globalThis.fetch as any).__nextGetStaticStore = () => {
722-
return staticGenerationAsyncStorage
723-
}
724-
;(globalThis.fetch as any).__nextPatched = true
727+
728+
// Attach the necessary properties to the patched fetch function.
729+
patched.__nextPatched = true as const
730+
patched.__nextGetStaticStore = () => staticGenerationAsyncStorage
731+
patched._nextOriginalFetch = originFetch
732+
733+
return patched
734+
}
735+
736+
// we patch fetch to collect cache information used for
737+
// determining if a page is static or not
738+
export function patchFetch(options: PatchableModule) {
739+
// If we've already patched fetch, we should not patch it again.
740+
if (isPatchedFetch(globalThis.fetch)) return
741+
742+
// Grab the original fetch function. We'll attach this so we can use it in
743+
// the patched fetch function.
744+
const original = globalThis.fetch
745+
746+
// Set the global fetch to the patched fetch.
747+
globalThis.fetch = createPatchedFetcher(original, options)
725748
}

packages/next/src/server/web/spec-extension/adapters/request-cookies.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { RequestCookies } from '../cookies'
2-
import type { StaticGenerationStore } from '../../../../client/components/static-generation-async-storage.external'
32

43
import { ResponseCookies } from '../cookies'
54
import { ReflectAdapter } from './reflect'
5+
import { staticGenerationAsyncStorage } from '../../../../client/components/static-generation-async-storage.external'
66

77
/**
88
* @internal
@@ -106,9 +106,7 @@ export class MutableRequestCookiesAdapter {
106106
const modifiedCookies = new Set<string>()
107107
const updateResponseCookies = () => {
108108
// TODO-APP: change method of getting staticGenerationAsyncStore
109-
const staticGenerationAsyncStore = (fetch as any)
110-
.__nextGetStaticStore?.()
111-
?.getStore() as undefined | StaticGenerationStore
109+
const staticGenerationAsyncStore = staticGenerationAsyncStorage.getStore()
112110
if (staticGenerationAsyncStore) {
113111
staticGenerationAsyncStore.pathWasRevalidated = true
114112
}

packages/next/src/server/web/spec-extension/revalidate.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import type {
2-
StaticGenerationAsyncStorage,
3-
StaticGenerationStore,
4-
} from '../../../client/components/static-generation-async-storage.external'
51
import { trackDynamicDataAccessed } from '../../app-render/dynamic-rendering'
62
import { isDynamicRoute } from '../../../shared/lib/router/utils'
73
import {
84
NEXT_CACHE_IMPLICIT_TAG_ID,
95
NEXT_CACHE_SOFT_TAG_MAX_LENGTH,
106
} from '../../../lib/constants'
117
import { getPathname } from '../../../lib/url'
8+
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
129

1310
/**
1411
* This function allows you to purge [cached data](https://nextjs.org/docs/app/building-your-application/caching) on-demand for a specific cache tag.
@@ -45,13 +42,7 @@ export function revalidatePath(originalPath: string, type?: 'layout' | 'page') {
4542
}
4643

4744
function revalidate(tag: string, expression: string) {
48-
const staticGenerationAsyncStorage = (
49-
fetch as any
50-
).__nextGetStaticStore?.() as undefined | StaticGenerationAsyncStorage
51-
52-
const store: undefined | StaticGenerationStore =
53-
staticGenerationAsyncStorage?.getStore()
54-
45+
const store = staticGenerationAsyncStorage.getStore()
5546
if (!store || !store.incrementalCache) {
5647
throw new Error(
5748
`Invariant: static generation store missing in ${expression}`

packages/next/src/server/web/spec-extension/unstable-cache.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import type { StaticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
21
import type { IncrementalCache } from '../../lib/incremental-cache'
32

4-
import { staticGenerationAsyncStorage as _staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
53
import { CACHE_ONE_YEAR } from '../../../lib/constants'
64
import {
75
addImplicitTags,
86
validateRevalidate,
97
validateTags,
108
} from '../../lib/patch-fetch'
9+
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
1110

1211
type Callback = (...args: any[]) => Promise<any>
1312

@@ -59,11 +58,6 @@ export function unstable_cache<T extends Callback>(
5958
tags?: string[]
6059
} = {}
6160
): T {
62-
const staticGenerationAsyncStorage =
63-
((fetch as any).__nextGetStaticStore?.() as
64-
| StaticGenerationAsyncStorage
65-
| undefined) ?? _staticGenerationAsyncStorage
66-
6761
if (options.revalidate === 0) {
6862
throw new Error(
6963
`Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${cb.toString()}`

0 commit comments

Comments
 (0)