diff --git a/packages/next/src/client/components/error-boundary.tsx b/packages/next/src/client/components/error-boundary.tsx index ad661efaac03c..60384a880afc5 100644 --- a/packages/next/src/client/components/error-boundary.tsx +++ b/packages/next/src/client/components/error-boundary.tsx @@ -3,6 +3,7 @@ import React from 'react' import { usePathname } from './navigation' import { isNextRouterError } from './is-next-router-error' +import { staticGenerationAsyncStorage } from './static-generation-async-storage.external' const styles = { error: { @@ -50,17 +51,12 @@ interface ErrorBoundaryHandlerState { // function crashes so we can maintain our previous cache // instead of caching the error page function HandleISRError({ error }: { error: any }) { - if (typeof (fetch as any).__nextGetStaticStore === 'function') { - const store: - | undefined - | import('./static-generation-async-storage.external').StaticGenerationStore = - (fetch as any).__nextGetStaticStore()?.getStore() - - if (store?.isRevalidate || store?.isStaticGeneration) { - console.error(error) - throw error - } + const store = staticGenerationAsyncStorage.getStore() + if (store?.isRevalidate || store?.isStaticGeneration) { + console.error(error) + throw error } + return null } diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index bba75df1ca594..9cbc4e45d2e5c 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -18,6 +18,20 @@ import type { FetchMetric } from '../base-http' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' +type Fetcher = typeof fetch + +type PatchedFetcher = Fetcher & { + readonly __nextPatched: true + readonly __nextGetStaticStore: () => StaticGenerationAsyncStorage + readonly _nextOriginalFetch: Fetcher +} + +function isPatchedFetch( + fetch: Fetcher | PatchedFetcher +): fetch is PatchedFetcher { + return '__nextPatched' in fetch && fetch.__nextPatched === true +} + export function validateRevalidate( revalidateVal: unknown, pathname: string @@ -174,22 +188,16 @@ interface PatchableModule { staticGenerationAsyncStorage: StaticGenerationAsyncStorage } -// we patch fetch to collect cache information used for -// determining if a page is static or not -export function patchFetch({ - serverHooks, - staticGenerationAsyncStorage, -}: PatchableModule) { - if (!(globalThis as any)._nextOriginalFetch) { - ;(globalThis as any)._nextOriginalFetch = globalThis.fetch - } - - if ((globalThis.fetch as any).__nextPatched) return - - const { DynamicServerError } = serverHooks - const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch - - globalThis.fetch = async ( +function createPatchedFetcher( + originFetch: Fetcher, + { + serverHooks: { DynamicServerError }, + staticGenerationAsyncStorage, + }: PatchableModule +): PatchedFetcher { + // Create the patched fetch function. We don't set the type here, as it's + // verified as the return value of this function. + const patched = async ( input: RequestInfo | URL, init: RequestInit | undefined ) => { @@ -211,7 +219,7 @@ export function patchFetch({ const isInternal = (init?.next as any)?.internal === true const hideSpan = process.env.NEXT_OTEL_FETCH_DISABLED === '1' - return await getTracer().trace( + return getTracer().trace( isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch, { hideSpan, @@ -225,9 +233,18 @@ export function patchFetch({ }, }, async () => { - const staticGenerationStore: StaticGenerationStore = - staticGenerationAsyncStorage.getStore() || - (fetch as any).__nextGetStaticStore?.() + // If this is an internal fetch, we should not do any special treatment. + if (isInternal) return originFetch(input, init) + + const staticGenerationStore = staticGenerationAsyncStorage.getStore() + + // If the staticGenerationStore is not available, we can't do any + // special treatment of fetch, therefore fallback to the original + // fetch implementation. + if (!staticGenerationStore || staticGenerationStore.isDraftMode) { + return originFetch(input, init) + } + const isRequestInput = input && typeof input === 'object' && @@ -239,17 +256,6 @@ export function patchFetch({ return value || (isRequestInput ? (input as any)[field] : null) } - // If the staticGenerationStore is not available, we can't do any - // special treatment of fetch, therefore fallback to the original - // fetch implementation. - if ( - !staticGenerationStore || - isInternal || - staticGenerationStore.isDraftMode - ) { - return originFetch(input, init) - } - let revalidate: number | undefined | false = undefined const getNextField = (field: 'revalidate' | 'tags') => { return typeof init?.next?.[field] !== 'undefined' @@ -718,8 +724,25 @@ export function patchFetch({ } ) } - ;(globalThis.fetch as any).__nextGetStaticStore = () => { - return staticGenerationAsyncStorage - } - ;(globalThis.fetch as any).__nextPatched = true + + // Attach the necessary properties to the patched fetch function. + patched.__nextPatched = true as const + patched.__nextGetStaticStore = () => staticGenerationAsyncStorage + patched._nextOriginalFetch = originFetch + + return patched +} + +// we patch fetch to collect cache information used for +// determining if a page is static or not +export function patchFetch(options: PatchableModule) { + // If we've already patched fetch, we should not patch it again. + if (isPatchedFetch(globalThis.fetch)) return + + // Grab the original fetch function. We'll attach this so we can use it in + // the patched fetch function. + const original = globalThis.fetch + + // Set the global fetch to the patched fetch. + globalThis.fetch = createPatchedFetcher(original, options) } diff --git a/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts b/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts index c606c3ae7dd4b..659b7073d9622 100644 --- a/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts +++ b/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts @@ -1,8 +1,8 @@ import type { RequestCookies } from '../cookies' -import type { StaticGenerationStore } from '../../../../client/components/static-generation-async-storage.external' import { ResponseCookies } from '../cookies' import { ReflectAdapter } from './reflect' +import { staticGenerationAsyncStorage } from '../../../../client/components/static-generation-async-storage.external' /** * @internal @@ -106,9 +106,7 @@ export class MutableRequestCookiesAdapter { const modifiedCookies = new Set() const updateResponseCookies = () => { // TODO-APP: change method of getting staticGenerationAsyncStore - const staticGenerationAsyncStore = (fetch as any) - .__nextGetStaticStore?.() - ?.getStore() as undefined | StaticGenerationStore + const staticGenerationAsyncStore = staticGenerationAsyncStorage.getStore() if (staticGenerationAsyncStore) { staticGenerationAsyncStore.pathWasRevalidated = true } diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index 600c3fd495b7b..ef10e466fe07f 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -1,7 +1,3 @@ -import type { - StaticGenerationAsyncStorage, - StaticGenerationStore, -} from '../../../client/components/static-generation-async-storage.external' import { trackDynamicDataAccessed } from '../../app-render/dynamic-rendering' import { isDynamicRoute } from '../../../shared/lib/router/utils' import { @@ -9,6 +5,7 @@ import { NEXT_CACHE_SOFT_TAG_MAX_LENGTH, } from '../../../lib/constants' import { getPathname } from '../../../lib/url' +import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external' /** * 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') { } function revalidate(tag: string, expression: string) { - const staticGenerationAsyncStorage = ( - fetch as any - ).__nextGetStaticStore?.() as undefined | StaticGenerationAsyncStorage - - const store: undefined | StaticGenerationStore = - staticGenerationAsyncStorage?.getStore() - + const store = staticGenerationAsyncStorage.getStore() if (!store || !store.incrementalCache) { throw new Error( `Invariant: static generation store missing in ${expression}` diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index 66c887b23a64f..ca98abeb11964 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -1,13 +1,12 @@ -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' import { CACHE_ONE_YEAR } from '../../../lib/constants' import { addImplicitTags, validateRevalidate, validateTags, } from '../../lib/patch-fetch' +import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external' type Callback = (...args: any[]) => Promise @@ -59,11 +58,6 @@ export function unstable_cache( tags?: string[] } = {} ): T { - const staticGenerationAsyncStorage = - ((fetch as any).__nextGetStaticStore?.() as - | StaticGenerationAsyncStorage - | undefined) ?? _staticGenerationAsyncStorage - if (options.revalidate === 0) { throw new Error( `Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${cb.toString()}`