| | import type { |
| | WorkAsyncStorage, |
| | WorkStore, |
| | } from '../app-render/work-async-storage.external' |
| |
|
| | import { AppRenderSpan, NextNodeServerSpan } from './trace/constants' |
| | import { getTracer, SpanKind } from './trace/tracer' |
| | import { |
| | CACHE_ONE_YEAR, |
| | INFINITE_CACHE, |
| | NEXT_CACHE_TAG_MAX_ITEMS, |
| | NEXT_CACHE_TAG_MAX_LENGTH, |
| | } from '../../lib/constants' |
| | import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering' |
| | import { makeHangingPromise } from '../dynamic-rendering-utils' |
| | import type { FetchMetric } from '../base-http' |
| | import { createDedupeFetch } from './dedupe-fetch' |
| | import { |
| | getCacheSignal, |
| | type RevalidateStore, |
| | type WorkUnitAsyncStorage, |
| | } from '../app-render/work-unit-async-storage.external' |
| | import { |
| | CachedRouteKind, |
| | IncrementalCacheKind, |
| | type CachedFetchData, |
| | type ServerComponentsHmrCache, |
| | type SetIncrementalFetchCacheContext, |
| | } from '../response-cache' |
| | import { cloneResponse } from './clone-response' |
| | import type { IncrementalCache } from './incremental-cache' |
| | import { RenderStage } from '../app-render/staged-rendering' |
| |
|
| | const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' |
| |
|
| | type Fetcher = typeof fetch |
| |
|
| | type PatchedFetcher = Fetcher & { |
| | readonly __nextPatched: true |
| | readonly __nextGetStaticStore: () => WorkAsyncStorage |
| | readonly _nextOriginalFetch: Fetcher |
| | } |
| |
|
| | export const NEXT_PATCH_SYMBOL = Symbol.for('next-patch') |
| |
|
| | function isFetchPatched() { |
| | return (globalThis as Record<symbol, unknown>)[NEXT_PATCH_SYMBOL] === true |
| | } |
| |
|
| | export function validateRevalidate( |
| | revalidateVal: unknown, |
| | route: string |
| | ): undefined | number { |
| | try { |
| | let normalizedRevalidate: number | undefined = undefined |
| |
|
| | if (revalidateVal === false) { |
| | normalizedRevalidate = INFINITE_CACHE |
| | } else if ( |
| | typeof revalidateVal === 'number' && |
| | !isNaN(revalidateVal) && |
| | revalidateVal > -1 |
| | ) { |
| | normalizedRevalidate = revalidateVal |
| | } else if (typeof revalidateVal !== 'undefined') { |
| | throw new Error( |
| | `Invalid revalidate value "${revalidateVal}" on "${route}", must be a non-negative number or false` |
| | ) |
| | } |
| | return normalizedRevalidate |
| | } catch (err: any) { |
| | |
| | if (err instanceof Error && err.message.includes('Invalid revalidate')) { |
| | throw err |
| | } |
| | return undefined |
| | } |
| | } |
| |
|
| | export function validateTags(tags: any[], description: string) { |
| | const validTags: string[] = [] |
| | const invalidTags: Array<{ |
| | tag: any |
| | reason: string |
| | }> = [] |
| |
|
| | for (let i = 0; i < tags.length; i++) { |
| | const tag = tags[i] |
| |
|
| | if (typeof tag !== 'string') { |
| | invalidTags.push({ tag, reason: 'invalid type, must be a string' }) |
| | } else if (tag.length > NEXT_CACHE_TAG_MAX_LENGTH) { |
| | invalidTags.push({ |
| | tag, |
| | reason: `exceeded max length of ${NEXT_CACHE_TAG_MAX_LENGTH}`, |
| | }) |
| | } else { |
| | validTags.push(tag) |
| | } |
| |
|
| | if (validTags.length > NEXT_CACHE_TAG_MAX_ITEMS) { |
| | console.warn( |
| | `Warning: exceeded max tag count for ${description}, dropped tags:`, |
| | tags.slice(i).join(', ') |
| | ) |
| | break |
| | } |
| | } |
| |
|
| | if (invalidTags.length > 0) { |
| | console.warn(`Warning: invalid tags passed to ${description}: `) |
| |
|
| | for (const { tag, reason } of invalidTags) { |
| | console.log(`tag: "${tag}" ${reason}`) |
| | } |
| | } |
| | return validTags |
| | } |
| |
|
| | function trackFetchMetric( |
| | workStore: WorkStore, |
| | ctx: Omit<FetchMetric, 'end' | 'idx'> |
| | ) { |
| | if (!workStore.shouldTrackFetchMetrics) { |
| | return |
| | } |
| |
|
| | workStore.fetchMetrics ??= [] |
| |
|
| | workStore.fetchMetrics.push({ |
| | ...ctx, |
| | end: performance.timeOrigin + performance.now(), |
| | idx: workStore.nextFetchId || 0, |
| | }) |
| | } |
| |
|
| | async function createCachedPrerenderResponse( |
| | res: Response, |
| | cacheKey: string, |
| | incrementalCacheContext: SetIncrementalFetchCacheContext | undefined, |
| | incrementalCache: IncrementalCache, |
| | revalidate: number, |
| | handleUnlock: () => Promise<void> | void |
| | ): Promise<Response> { |
| | |
| | |
| | |
| | const bodyBuffer = await res.arrayBuffer() |
| |
|
| | const fetchedData = { |
| | headers: Object.fromEntries(res.headers.entries()), |
| | body: Buffer.from(bodyBuffer).toString('base64'), |
| | status: res.status, |
| | url: res.url, |
| | } |
| |
|
| | |
| | |
| |
|
| | if (incrementalCacheContext) { |
| | await incrementalCache.set( |
| | cacheKey, |
| | { kind: CachedRouteKind.FETCH, data: fetchedData, revalidate }, |
| | incrementalCacheContext |
| | ) |
| | } |
| |
|
| | await handleUnlock() |
| |
|
| | |
| | return new Response(bodyBuffer, { |
| | headers: res.headers, |
| | status: res.status, |
| | statusText: res.statusText, |
| | }) |
| | } |
| |
|
| | async function createCachedDynamicResponse( |
| | workStore: WorkStore, |
| | res: Response, |
| | cacheKey: string, |
| | incrementalCacheContext: SetIncrementalFetchCacheContext | undefined, |
| | incrementalCache: IncrementalCache, |
| | serverComponentsHmrCache: ServerComponentsHmrCache | undefined, |
| | revalidate: number, |
| | input: RequestInfo | URL, |
| | handleUnlock: () => Promise<void> | void |
| | ): Promise<Response> { |
| | |
| | |
| | |
| | const [cloned1, cloned2] = cloneResponse(res) |
| |
|
| | |
| | |
| | |
| | const cacheSetPromise = cloned1 |
| | .arrayBuffer() |
| | .then(async (arrayBuffer) => { |
| | const bodyBuffer = Buffer.from(arrayBuffer) |
| |
|
| | const fetchedData = { |
| | headers: Object.fromEntries(cloned1.headers.entries()), |
| | body: bodyBuffer.toString('base64'), |
| | status: cloned1.status, |
| | url: cloned1.url, |
| | } |
| |
|
| | serverComponentsHmrCache?.set(cacheKey, fetchedData) |
| |
|
| | if (incrementalCacheContext) { |
| | await incrementalCache.set( |
| | cacheKey, |
| | { kind: CachedRouteKind.FETCH, data: fetchedData, revalidate }, |
| | incrementalCacheContext |
| | ) |
| | } |
| | }) |
| | .catch((error) => console.warn(`Failed to set fetch cache`, input, error)) |
| | .finally(handleUnlock) |
| |
|
| | const pendingRevalidateKey = `cache-set-${cacheKey}` |
| | const pendingRevalidates = (workStore.pendingRevalidates ??= {}) |
| |
|
| | let pendingRevalidatePromise = Promise.resolve() |
| | if (pendingRevalidateKey in pendingRevalidates) { |
| | |
| | |
| | pendingRevalidatePromise = pendingRevalidates[pendingRevalidateKey] |
| | } |
| |
|
| | pendingRevalidates[pendingRevalidateKey] = pendingRevalidatePromise |
| | .then(() => cacheSetPromise) |
| | .finally(() => { |
| | |
| | |
| | if (!pendingRevalidates?.[pendingRevalidateKey]) { |
| | return |
| | } |
| |
|
| | delete pendingRevalidates[pendingRevalidateKey] |
| | }) |
| |
|
| | return cloned2 |
| | } |
| |
|
| | interface PatchableModule { |
| | workAsyncStorage: WorkAsyncStorage |
| | workUnitAsyncStorage: WorkUnitAsyncStorage |
| | } |
| |
|
| | export function createPatchedFetcher( |
| | originFetch: Fetcher, |
| | { workAsyncStorage, workUnitAsyncStorage }: PatchableModule |
| | ): PatchedFetcher { |
| | |
| | const patched = async function fetch( |
| | input: RequestInfo | URL, |
| | init: RequestInit | undefined |
| | ): Promise<Response> { |
| | let url: URL | undefined |
| | try { |
| | url = new URL(input instanceof Request ? input.url : input) |
| | url.username = '' |
| | url.password = '' |
| | } catch { |
| | |
| | url = undefined |
| | } |
| | const fetchUrl = url?.href ?? '' |
| | const method = init?.method?.toUpperCase() || 'GET' |
| |
|
| | |
| | |
| | const isInternal = (init?.next as any)?.internal === true |
| | const hideSpan = process.env.NEXT_OTEL_FETCH_DISABLED === '1' |
| | |
| | |
| | |
| | |
| | const fetchStart: number | undefined = isInternal |
| | ? undefined |
| | : performance.timeOrigin + performance.now() |
| |
|
| | const workStore = workAsyncStorage.getStore() |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| |
|
| | let cacheSignal = workUnitStore ? getCacheSignal(workUnitStore) : null |
| | if (cacheSignal) { |
| | cacheSignal.beginRead() |
| | } |
| |
|
| | const result = getTracer().trace( |
| | isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch, |
| | { |
| | hideSpan, |
| | kind: SpanKind.CLIENT, |
| | spanName: ['fetch', method, fetchUrl].filter(Boolean).join(' '), |
| | attributes: { |
| | 'http.url': fetchUrl, |
| | 'http.method': method, |
| | 'net.peer.name': url?.hostname, |
| | 'net.peer.port': url?.port || undefined, |
| | }, |
| | }, |
| | async () => { |
| | |
| | if (isInternal) { |
| | return originFetch(input, init) |
| | } |
| |
|
| | |
| | |
| | |
| | if (!workStore) { |
| | return originFetch(input, init) |
| | } |
| |
|
| | |
| | |
| | if (workStore.isDraftMode) { |
| | return originFetch(input, init) |
| | } |
| |
|
| | const isRequestInput = |
| | input && |
| | typeof input === 'object' && |
| | typeof (input as Request).method === 'string' |
| |
|
| | const getRequestMeta = (field: string) => { |
| | |
| | const value = (init as any)?.[field] |
| | return value || (isRequestInput ? (input as any)[field] : null) |
| | } |
| |
|
| | let finalRevalidate: number | undefined = undefined |
| | const getNextField = (field: 'revalidate' | 'tags') => { |
| | return typeof init?.next?.[field] !== 'undefined' |
| | ? init?.next?.[field] |
| | : isRequestInput |
| | ? (input as any).next?.[field] |
| | : undefined |
| | } |
| | |
| | |
| | const originalFetchRevalidate = getNextField('revalidate') |
| | let currentFetchRevalidate = originalFetchRevalidate |
| | const tags: string[] = validateTags( |
| | getNextField('tags') || [], |
| | `fetch ${input.toString()}` |
| | ) |
| |
|
| | let revalidateStore: RevalidateStore | undefined |
| |
|
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-runtime': |
| | |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | revalidateStore = workUnitStore |
| | break |
| | case 'request': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | if (revalidateStore) { |
| | if (Array.isArray(tags)) { |
| | |
| | const collectedTags = |
| | revalidateStore.tags ?? (revalidateStore.tags = []) |
| | for (const tag of tags) { |
| | if (!collectedTags.includes(tag)) { |
| | collectedTags.push(tag) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | const implicitTags = workUnitStore?.implicitTags |
| |
|
| | let pageFetchCacheMode = workStore.fetchCache |
| |
|
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'unstable-cache': |
| | |
| | |
| | pageFetchCacheMode = 'force-no-store' |
| | break |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | const isUsingNoStore = !!workStore.isUnstableNoStore |
| |
|
| | let currentFetchCacheConfig = getRequestMeta('cache') |
| | let cacheReason = '' |
| | let cacheWarning: string | undefined |
| |
|
| | if ( |
| | typeof currentFetchCacheConfig === 'string' && |
| | typeof currentFetchRevalidate !== 'undefined' |
| | ) { |
| | |
| | const isConflictingRevalidate = |
| | |
| | (currentFetchCacheConfig === 'force-cache' && |
| | currentFetchRevalidate === 0) || |
| | |
| | (currentFetchCacheConfig === 'no-store' && |
| | (currentFetchRevalidate > 0 || currentFetchRevalidate === false)) |
| |
|
| | if (isConflictingRevalidate) { |
| | cacheWarning = `Specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.` |
| | currentFetchCacheConfig = undefined |
| | currentFetchRevalidate = undefined |
| | } |
| | } |
| |
|
| | const hasExplicitFetchCacheOptOut = |
| | |
| | currentFetchCacheConfig === 'no-cache' || |
| | currentFetchCacheConfig === 'no-store' || |
| | |
| | |
| | pageFetchCacheMode === 'force-no-store' || |
| | pageFetchCacheMode === 'only-no-store' |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const noFetchConfigAndForceDynamic = |
| | !pageFetchCacheMode && |
| | !currentFetchCacheConfig && |
| | !currentFetchRevalidate && |
| | workStore.forceDynamic |
| |
|
| | if ( |
| | |
| | |
| | currentFetchCacheConfig === 'force-cache' && |
| | typeof currentFetchRevalidate === 'undefined' |
| | ) { |
| | currentFetchRevalidate = false |
| | } else if ( |
| | hasExplicitFetchCacheOptOut || |
| | noFetchConfigAndForceDynamic |
| | ) { |
| | currentFetchRevalidate = 0 |
| | } |
| |
|
| | if ( |
| | currentFetchCacheConfig === 'no-cache' || |
| | currentFetchCacheConfig === 'no-store' |
| | ) { |
| | cacheReason = `cache: ${currentFetchCacheConfig}` |
| | } |
| |
|
| | finalRevalidate = validateRevalidate( |
| | currentFetchRevalidate, |
| | workStore.route |
| | ) |
| |
|
| | const _headers = getRequestMeta('headers') |
| | const initHeaders: Headers = |
| | typeof _headers?.get === 'function' |
| | ? _headers |
| | : new Headers(_headers || {}) |
| |
|
| | const hasUnCacheableHeader = |
| | initHeaders.get('authorization') || initHeaders.get('cookie') |
| |
|
| | const isUnCacheableMethod = !['get', 'head'].includes( |
| | getRequestMeta('method')?.toLowerCase() || 'get' |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const hasNoExplicitCacheConfig = |
| | |
| | pageFetchCacheMode == undefined && |
| | |
| | (currentFetchCacheConfig == undefined || |
| | |
| | |
| | currentFetchCacheConfig === 'default') && |
| | |
| | currentFetchRevalidate == undefined |
| |
|
| | let autoNoCache = Boolean( |
| | (hasUnCacheableHeader || isUnCacheableMethod) && |
| | revalidateStore?.revalidate === 0 |
| | ) |
| |
|
| | let isImplicitBuildTimeCache = false |
| |
|
| | if (!autoNoCache && hasNoExplicitCacheConfig) { |
| | |
| | |
| | |
| | if (workStore.isBuildTimePrerendering) { |
| | isImplicitBuildTimeCache = true |
| | } else { |
| | autoNoCache = true |
| | } |
| | } |
| |
|
| | |
| | |
| | if (hasNoExplicitCacheConfig && workUnitStore !== undefined) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-runtime': |
| | |
| | |
| | |
| | case 'prerender-client': |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| |
|
| | return makeHangingPromise<Response>( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'fetch()' |
| | ) |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering |
| | ) { |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| | await workUnitStore.stagedRendering.waitForStage( |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | switch (pageFetchCacheMode) { |
| | case 'force-no-store': { |
| | cacheReason = 'fetchCache = force-no-store' |
| | break |
| | } |
| | case 'only-no-store': { |
| | if ( |
| | currentFetchCacheConfig === 'force-cache' || |
| | (typeof finalRevalidate !== 'undefined' && finalRevalidate > 0) |
| | ) { |
| | throw new Error( |
| | `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` |
| | ) |
| | } |
| | cacheReason = 'fetchCache = only-no-store' |
| | break |
| | } |
| | case 'only-cache': { |
| | if (currentFetchCacheConfig === 'no-store') { |
| | throw new Error( |
| | `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` |
| | ) |
| | } |
| | break |
| | } |
| | case 'force-cache': { |
| | if ( |
| | typeof currentFetchRevalidate === 'undefined' || |
| | currentFetchRevalidate === 0 |
| | ) { |
| | cacheReason = 'fetchCache = force-cache' |
| | finalRevalidate = INFINITE_CACHE |
| | } |
| | break |
| | } |
| | case 'default-cache': |
| | case 'default-no-store': |
| | case 'auto': |
| | case undefined: |
| | |
| | |
| | |
| | |
| | break |
| | default: |
| | pageFetchCacheMode satisfies never |
| | } |
| |
|
| | if (typeof finalRevalidate === 'undefined') { |
| | if (pageFetchCacheMode === 'default-cache' && !isUsingNoStore) { |
| | finalRevalidate = INFINITE_CACHE |
| | cacheReason = 'fetchCache = default-cache' |
| | } else if (pageFetchCacheMode === 'default-no-store') { |
| | finalRevalidate = 0 |
| | cacheReason = 'fetchCache = default-no-store' |
| | } else if (isUsingNoStore) { |
| | finalRevalidate = 0 |
| | cacheReason = 'noStore call' |
| | } else if (autoNoCache) { |
| | finalRevalidate = 0 |
| | cacheReason = 'auto no cache' |
| | } else { |
| | |
| | cacheReason = 'auto cache' |
| | finalRevalidate = revalidateStore |
| | ? revalidateStore.revalidate |
| | : INFINITE_CACHE |
| | } |
| | } else if (!cacheReason) { |
| | cacheReason = `revalidate: ${finalRevalidate}` |
| | } |
| |
|
| | if ( |
| | |
| | |
| | !(workStore.forceStatic && finalRevalidate === 0) && |
| | |
| | !autoNoCache && |
| | |
| | |
| | |
| | revalidateStore && |
| | finalRevalidate < revalidateStore.revalidate |
| | ) { |
| | |
| | |
| | if (finalRevalidate === 0) { |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| | return makeHangingPromise<Response>( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'fetch()' |
| | ) |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering |
| | ) { |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| | await workUnitStore.stagedRendering.waitForStage( |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | markCurrentScopeAsDynamic( |
| | workStore, |
| | workUnitStore, |
| | `revalidate: 0 fetch ${input} ${workStore.route}` |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | if (revalidateStore && originalFetchRevalidate === finalRevalidate) { |
| | revalidateStore.revalidate = finalRevalidate |
| | } |
| | } |
| |
|
| | const isCacheableRevalidate = |
| | typeof finalRevalidate === 'number' && finalRevalidate > 0 |
| |
|
| | let cacheKey: string | undefined |
| | const { incrementalCache } = workStore |
| | let isHmrRefresh = false |
| | let serverComponentsHmrCache: ServerComponentsHmrCache | undefined |
| |
|
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | isHmrRefresh = workUnitStore.isHmrRefresh ?? false |
| | serverComponentsHmrCache = workUnitStore.serverComponentsHmrCache |
| | break |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | if ( |
| | incrementalCache && |
| | (isCacheableRevalidate || serverComponentsHmrCache) |
| | ) { |
| | try { |
| | cacheKey = await incrementalCache.generateCacheKey( |
| | fetchUrl, |
| | isRequestInput ? (input as RequestInit) : init |
| | ) |
| | } catch (err) { |
| | console.error(`Failed to generate cache key for`, input) |
| | } |
| | } |
| |
|
| | const fetchIdx = workStore.nextFetchId ?? 1 |
| | workStore.nextFetchId = fetchIdx + 1 |
| |
|
| | let handleUnlock: () => Promise<void> | void = () => {} |
| |
|
| | const doOriginalFetch = async ( |
| | isStale?: boolean, |
| | cacheReasonOverride?: string |
| | ) => { |
| | const requestInputFields = [ |
| | 'cache', |
| | 'credentials', |
| | 'headers', |
| | 'integrity', |
| | 'keepalive', |
| | 'method', |
| | 'mode', |
| | 'redirect', |
| | 'referrer', |
| | 'referrerPolicy', |
| | 'window', |
| | 'duplex', |
| |
|
| | |
| | ...(isStale ? [] : ['signal']), |
| | ] |
| |
|
| | if (isRequestInput) { |
| | const reqInput: Request = input as any |
| | const reqOptions: RequestInit = { |
| | body: (reqInput as any)._ogBody || reqInput.body, |
| | } |
| |
|
| | for (const field of requestInputFields) { |
| | |
| | reqOptions[field] = reqInput[field] |
| | } |
| | input = new Request(reqInput.url, reqOptions) |
| | } else if (init) { |
| | const { _ogBody, body, signal, ...otherInput } = |
| | init as RequestInit & { _ogBody?: any } |
| | init = { |
| | ...otherInput, |
| | body: _ogBody || body, |
| | signal: isStale ? undefined : signal, |
| | } |
| | } |
| |
|
| | |
| | const clonedInit = { |
| | ...init, |
| | next: { ...init?.next, fetchType: 'origin', fetchIdx }, |
| | } |
| |
|
| | return originFetch(input, clonedInit) |
| | .then(async (res) => { |
| | if (!isStale && fetchStart) { |
| | trackFetchMetric(workStore, { |
| | start: fetchStart, |
| | url: fetchUrl, |
| | cacheReason: cacheReasonOverride || cacheReason, |
| | cacheStatus: |
| | finalRevalidate === 0 || cacheReasonOverride |
| | ? 'skip' |
| | : 'miss', |
| | cacheWarning, |
| | status: res.status, |
| | method: clonedInit.method || 'GET', |
| | }) |
| | } |
| | if ( |
| | res.status === 200 && |
| | incrementalCache && |
| | cacheKey && |
| | (isCacheableRevalidate || serverComponentsHmrCache) |
| | ) { |
| | const normalizedRevalidate = |
| | finalRevalidate >= INFINITE_CACHE |
| | ? CACHE_ONE_YEAR |
| | : finalRevalidate |
| |
|
| | const incrementalCacheConfig: |
| | | SetIncrementalFetchCacheContext |
| | | undefined = isCacheableRevalidate |
| | ? { |
| | fetchCache: true, |
| | fetchUrl, |
| | fetchIdx, |
| | tags, |
| | isImplicitBuildTimeCache, |
| | } |
| | : undefined |
| |
|
| | switch (workUnitStore?.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | return createCachedPrerenderResponse( |
| | res, |
| | cacheKey, |
| | incrementalCacheConfig, |
| | incrementalCache, |
| | normalizedRevalidate, |
| | handleUnlock |
| | ) |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering && |
| | workUnitStore.cacheSignal |
| | ) { |
| | |
| | |
| | return createCachedPrerenderResponse( |
| | res, |
| | cacheKey, |
| | incrementalCacheConfig, |
| | incrementalCache, |
| | normalizedRevalidate, |
| | handleUnlock |
| | ) |
| | } |
| | |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | case undefined: |
| | return createCachedDynamicResponse( |
| | workStore, |
| | res, |
| | cacheKey, |
| | incrementalCacheConfig, |
| | incrementalCache, |
| | serverComponentsHmrCache, |
| | normalizedRevalidate, |
| | input, |
| | handleUnlock |
| | ) |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | |
| | |
| | await handleUnlock() |
| |
|
| | return res |
| | }) |
| | .catch((error) => { |
| | handleUnlock() |
| | throw error |
| | }) |
| | } |
| |
|
| | let cacheReasonOverride |
| | let isForegroundRevalidate = false |
| | let isHmrRefreshCache = false |
| |
|
| | if (cacheKey && incrementalCache) { |
| | let cachedFetchData: CachedFetchData | undefined |
| |
|
| | if (isHmrRefresh && serverComponentsHmrCache) { |
| | cachedFetchData = serverComponentsHmrCache.get(cacheKey) |
| | isHmrRefreshCache = true |
| | } |
| |
|
| | if (isCacheableRevalidate && !cachedFetchData) { |
| | handleUnlock = await incrementalCache.lock(cacheKey) |
| | const entry = workStore.isOnDemandRevalidate |
| | ? null |
| | : await incrementalCache.get(cacheKey, { |
| | kind: IncrementalCacheKind.FETCH, |
| | revalidate: finalRevalidate, |
| | fetchUrl, |
| | fetchIdx, |
| | tags, |
| | softTags: implicitTags?.tags, |
| | }) |
| |
|
| | if (hasNoExplicitCacheConfig && workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | |
| | |
| | |
| | |
| | |
| | await getTimeoutBoundary() |
| | break |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering |
| | ) { |
| | await workUnitStore.stagedRendering.waitForStage( |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | if (entry) { |
| | await handleUnlock() |
| | } else { |
| | |
| | |
| | cacheReasonOverride = 'cache-control: no-cache (hard refresh)' |
| | } |
| |
|
| | if (entry?.value && entry.value.kind === CachedRouteKind.FETCH) { |
| | |
| | |
| | if (workStore.isStaticGeneration && entry.isStale) { |
| | isForegroundRevalidate = true |
| | } else { |
| | if (entry.isStale) { |
| | workStore.pendingRevalidates ??= {} |
| | if (!workStore.pendingRevalidates[cacheKey]) { |
| | const pendingRevalidate = doOriginalFetch(true) |
| | .then(async (response) => ({ |
| | body: await response.arrayBuffer(), |
| | headers: response.headers, |
| | status: response.status, |
| | statusText: response.statusText, |
| | })) |
| | .finally(() => { |
| | workStore.pendingRevalidates ??= {} |
| | delete workStore.pendingRevalidates[cacheKey || ''] |
| | }) |
| |
|
| | |
| | |
| | pendingRevalidate.catch(console.error) |
| |
|
| | workStore.pendingRevalidates[cacheKey] = pendingRevalidate |
| | } |
| | } |
| |
|
| | cachedFetchData = entry.value.data |
| | } |
| | } |
| | } |
| |
|
| | if (cachedFetchData) { |
| | if (fetchStart) { |
| | trackFetchMetric(workStore, { |
| | start: fetchStart, |
| | url: fetchUrl, |
| | cacheReason, |
| | cacheStatus: isHmrRefreshCache ? 'hmr' : 'hit', |
| | cacheWarning, |
| | status: cachedFetchData.status || 200, |
| | method: init?.method || 'GET', |
| | }) |
| | } |
| |
|
| | const response = new Response( |
| | Buffer.from(cachedFetchData.body, 'base64'), |
| | { |
| | headers: cachedFetchData.headers, |
| | status: cachedFetchData.status, |
| | } |
| | ) |
| |
|
| | Object.defineProperty(response, 'url', { |
| | value: cachedFetchData.url, |
| | }) |
| |
|
| | return response |
| | } |
| | } |
| |
|
| | if ( |
| | (workStore.isStaticGeneration || |
| | (process.env.NODE_ENV === 'development' && |
| | process.env.__NEXT_CACHE_COMPONENTS && |
| | workUnitStore && |
| | |
| | workUnitStore.type === 'request' && |
| | workUnitStore.stagedRendering)) && |
| | init && |
| | typeof init === 'object' |
| | ) { |
| | const { cache } = init |
| |
|
| | |
| | if (isEdgeRuntime) delete init.cache |
| |
|
| | if (cache === 'no-store') { |
| | |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| | return makeHangingPromise<Response>( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'fetch()' |
| | ) |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering |
| | ) { |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | cacheSignal = null |
| | } |
| | await workUnitStore.stagedRendering.waitForStage( |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | markCurrentScopeAsDynamic( |
| | workStore, |
| | workUnitStore, |
| | `no-store fetch ${input} ${workStore.route}` |
| | ) |
| | } |
| |
|
| | const hasNextConfig = 'next' in init |
| | const { next = {} } = init |
| | if ( |
| | typeof next.revalidate === 'number' && |
| | revalidateStore && |
| | next.revalidate < revalidateStore.revalidate |
| | ) { |
| | if (next.revalidate === 0) { |
| | |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-runtime': |
| | return makeHangingPromise<Response>( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'fetch()' |
| | ) |
| | case 'request': |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | workUnitStore.stagedRendering |
| | ) { |
| | await workUnitStore.stagedRendering.waitForStage( |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | case 'prerender-legacy': |
| | case 'prerender-ppr': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | markCurrentScopeAsDynamic( |
| | workStore, |
| | workUnitStore, |
| | `revalidate: 0 fetch ${input} ${workStore.route}` |
| | ) |
| | } |
| |
|
| | if (!workStore.forceStatic || next.revalidate !== 0) { |
| | revalidateStore.revalidate = next.revalidate |
| | } |
| | } |
| | if (hasNextConfig) delete init.next |
| | } |
| |
|
| | |
| | |
| | |
| | if (cacheKey && isForegroundRevalidate) { |
| | const pendingRevalidateKey = cacheKey |
| | workStore.pendingRevalidates ??= {} |
| | let pendingRevalidate = |
| | workStore.pendingRevalidates[pendingRevalidateKey] |
| |
|
| | if (pendingRevalidate) { |
| | const revalidatedResult: { |
| | body: ArrayBuffer |
| | headers: Headers |
| | status: number |
| | statusText: string |
| | } = await pendingRevalidate |
| | return new Response(revalidatedResult.body, { |
| | headers: revalidatedResult.headers, |
| | status: revalidatedResult.status, |
| | statusText: revalidatedResult.statusText, |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const pendingResponse = doOriginalFetch(true, cacheReasonOverride) |
| | |
| | |
| | |
| | |
| | .then(cloneResponse) |
| |
|
| | pendingRevalidate = pendingResponse |
| | .then(async (responses) => { |
| | const response = responses[0] |
| | return { |
| | body: await response.arrayBuffer(), |
| | headers: response.headers, |
| | status: response.status, |
| | statusText: response.statusText, |
| | } |
| | }) |
| | .finally(() => { |
| | |
| | |
| | if (!workStore.pendingRevalidates?.[pendingRevalidateKey]) { |
| | return |
| | } |
| |
|
| | delete workStore.pendingRevalidates[pendingRevalidateKey] |
| | }) |
| |
|
| | |
| | |
| | pendingRevalidate.catch(() => {}) |
| |
|
| | workStore.pendingRevalidates[pendingRevalidateKey] = pendingRevalidate |
| |
|
| | return pendingResponse.then((responses) => responses[1]) |
| | } else { |
| | return doOriginalFetch(false, cacheReasonOverride) |
| | } |
| | } |
| | ) |
| |
|
| | if (cacheSignal) { |
| | try { |
| | return await result |
| | } finally { |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| | } |
| | } |
| | return result |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | patched.__nextPatched = true as const |
| | patched.__nextGetStaticStore = () => workAsyncStorage |
| | patched._nextOriginalFetch = originFetch |
| | ;(globalThis as Record<symbol, unknown>)[NEXT_PATCH_SYMBOL] = true |
| |
|
| | |
| | |
| | Object.defineProperty(patched, 'name', { value: 'fetch', writable: false }) |
| |
|
| | return patched |
| | } |
| |
|
| | |
| | |
| | export function patchFetch(options: PatchableModule) { |
| | |
| | if (isFetchPatched()) return |
| |
|
| | |
| | |
| | const original = createDedupeFetch(globalThis.fetch) |
| |
|
| | |
| | globalThis.fetch = createPatchedFetcher(original, options) |
| | } |
| |
|
| | let currentTimeoutBoundary: null | Promise<void> = null |
| | function getTimeoutBoundary() { |
| | if (!currentTimeoutBoundary) { |
| | currentTimeoutBoundary = new Promise((r) => { |
| | setTimeout(() => { |
| | currentTimeoutBoundary = null |
| | r() |
| | }, 0) |
| | }) |
| | } |
| | return currentTimeoutBoundary |
| | } |
| |
|