diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 9a1e1ae14b2d8..bcfd2c5643381 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1438,6 +1438,21 @@ export async function buildAppStaticPaths({ const newParams: Params[] = [] if (curGenerate.generateStaticParams) { + const curStore = + ComponentMod.staticGenerationAsyncStorage.getStore() + + if (curStore) { + if (typeof curGenerate?.config?.fetchCache !== 'undefined') { + curStore.fetchCache = curGenerate.config.fetchCache + } + if (typeof curGenerate?.config?.revalidate !== 'undefined') { + curStore.revalidate = curGenerate.config.revalidate + } + if (curGenerate?.config?.dynamic === 'force-dynamic') { + curStore.forceDynamic = true + } + } + for (const params of paramsItems) { const result = await curGenerate.generateStaticParams({ params, diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index ea9631992eee9..90939a89f0529 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -289,7 +289,7 @@ function createPatchedFetcher( return value || (isRequestInput ? (input as any)[field] : null) } - let revalidate: number | undefined | false = undefined + let finalRevalidate: number | undefined | false = undefined const getNextField = (field: 'revalidate' | 'tags') => { return typeof init?.next?.[field] !== 'undefined' ? init?.next?.[field] @@ -299,7 +299,7 @@ function createPatchedFetcher( } // RequestInit doesn't keep extra fields e.g. next so it's // only available if init is used separate - let curRevalidate = getNextField('revalidate') + let currentFetchRevalidate = getNextField('revalidate') const tags: string[] = validateTags( getNextField('tags') || [], `fetch ${input.toString()}` @@ -317,49 +317,52 @@ function createPatchedFetcher( } const implicitTags = addImplicitTags(staticGenerationStore) - const fetchCacheMode = staticGenerationStore.fetchCache + const pageFetchCacheMode = staticGenerationStore.fetchCache const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore - let _cache = getRequestMeta('cache') + let currentFetchCacheConfig = getRequestMeta('cache') let cacheReason = '' if ( - typeof _cache === 'string' && - typeof curRevalidate !== 'undefined' + typeof currentFetchCacheConfig === 'string' && + typeof currentFetchRevalidate !== 'undefined' ) { // when providing fetch with a Request input, it'll automatically set a cache value of 'default' // we only want to warn if the user is explicitly setting a cache value - if (!(isRequestInput && _cache === 'default')) { + if (!(isRequestInput && currentFetchCacheConfig === 'default')) { Log.warn( - `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.` + `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.` ) } - _cache = undefined + currentFetchCacheConfig = undefined } - if (_cache === 'force-cache') { - curRevalidate = false + if (currentFetchCacheConfig === 'force-cache') { + currentFetchRevalidate = false } else if ( - _cache === 'no-cache' || - _cache === 'no-store' || - fetchCacheMode === 'force-no-store' || - fetchCacheMode === 'only-no-store' || + currentFetchCacheConfig === 'no-cache' || + currentFetchCacheConfig === 'no-store' || + pageFetchCacheMode === 'force-no-store' || + pageFetchCacheMode === 'only-no-store' || // If no explicit fetch cache mode is set, but dynamic = `force-dynamic` is set, // we shouldn't consider caching the fetch. This is because the `dynamic` cache // is considered a "top-level" cache mode, whereas something like `fetchCache` is more // fine-grained. Top-level modes are responsible for setting reasonable defaults for the // other configurations. - (!fetchCacheMode && staticGenerationStore.forceDynamic) + (!pageFetchCacheMode && staticGenerationStore.forceDynamic) ) { - curRevalidate = 0 + currentFetchRevalidate = 0 } - if (_cache === 'no-cache' || _cache === 'no-store') { - cacheReason = `cache: ${_cache}` + if ( + currentFetchCacheConfig === 'no-cache' || + currentFetchCacheConfig === 'no-store' + ) { + cacheReason = `cache: ${currentFetchCacheConfig}` } - revalidate = validateRevalidate( - curRevalidate, + finalRevalidate = validateRevalidate( + currentFetchRevalidate, staticGenerationStore.urlPathname ) @@ -379,20 +382,29 @@ function createPatchedFetcher( // if there are authorized headers or a POST method and // dynamic data usage was present above the tree we bail // e.g. if cookies() is used before an authed/POST fetch + // or no user provided fetch cache config or revalidate + // is provided we don't cache const autoNoCache = - (hasUnCacheableHeader || isUnCacheableMethod) && - staticGenerationStore.revalidate === 0 - - switch (fetchCacheMode) { + // this condition is hit for null/undefined + // eslint-disable-next-line eqeqeq + (pageFetchCacheMode == undefined && + // eslint-disable-next-line eqeqeq + currentFetchCacheConfig == undefined && + // eslint-disable-next-line eqeqeq + currentFetchRevalidate == undefined) || + ((hasUnCacheableHeader || isUnCacheableMethod) && + staticGenerationStore.revalidate === 0) + + switch (pageFetchCacheMode) { case 'force-no-store': { cacheReason = 'fetchCache = force-no-store' break } case 'only-no-store': { if ( - _cache === 'force-cache' || - (typeof revalidate !== 'undefined' && - (revalidate === false || revalidate > 0)) + currentFetchCacheConfig === 'force-cache' || + (typeof finalRevalidate !== 'undefined' && + (finalRevalidate === false || finalRevalidate > 0)) ) { throw new Error( `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` @@ -402,7 +414,7 @@ function createPatchedFetcher( break } case 'only-cache': { - if (_cache === 'no-store') { + if (currentFetchCacheConfig === 'no-store') { throw new Error( `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` ) @@ -410,9 +422,12 @@ function createPatchedFetcher( break } case 'force-cache': { - if (typeof curRevalidate === 'undefined' || curRevalidate === 0) { + if ( + typeof currentFetchRevalidate === 'undefined' || + currentFetchRevalidate === 0 + ) { cacheReason = 'fetchCache = force-cache' - revalidate = false + finalRevalidate = false } break } @@ -423,59 +438,59 @@ function createPatchedFetcher( // simplify the switch case and ensure we have an exhaustive switch handling all modes } - if (typeof revalidate === 'undefined') { - if (fetchCacheMode === 'default-cache') { - revalidate = false + if (typeof finalRevalidate === 'undefined') { + if (pageFetchCacheMode === 'default-cache' && !isUsingNoStore) { + finalRevalidate = false cacheReason = 'fetchCache = default-cache' - } else if (autoNoCache) { - revalidate = 0 - cacheReason = 'auto no cache' - } else if (fetchCacheMode === 'default-no-store') { - revalidate = 0 + } else if (pageFetchCacheMode === 'default-no-store') { + finalRevalidate = 0 cacheReason = 'fetchCache = default-no-store' } else if (isUsingNoStore) { - revalidate = 0 + finalRevalidate = 0 cacheReason = 'noStore call' + } else if (autoNoCache) { + finalRevalidate = 0 + cacheReason = 'auto no cache' } else { + // TODO: should we consider this case an invariant? cacheReason = 'auto cache' - revalidate = + finalRevalidate = typeof staticGenerationStore.revalidate === 'boolean' || typeof staticGenerationStore.revalidate === 'undefined' ? false : staticGenerationStore.revalidate } } else if (!cacheReason) { - cacheReason = `revalidate: ${revalidate}` + cacheReason = `revalidate: ${finalRevalidate}` } if ( // when force static is configured we don't bail from // `revalidate: 0` values - !(staticGenerationStore.forceStatic && revalidate === 0) && - // we don't consider autoNoCache to switch to dynamic during - // revalidate although if it occurs during build we do + !(staticGenerationStore.forceStatic && finalRevalidate === 0) && + // we don't consider autoNoCache to switch to dynamic for ISR !autoNoCache && // If the revalidate value isn't currently set or the value is less // than the current revalidate value, we should update the revalidate // value. (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof revalidate === 'number' && + (typeof finalRevalidate === 'number' && (staticGenerationStore.revalidate === false || (typeof staticGenerationStore.revalidate === 'number' && - revalidate < staticGenerationStore.revalidate)))) + finalRevalidate < staticGenerationStore.revalidate)))) ) { // If we were setting the revalidate value to 0, we should try to // postpone instead first. - if (revalidate === 0) { + if (finalRevalidate === 0) { trackDynamicFetch(staticGenerationStore, 'revalidate: 0') } - staticGenerationStore.revalidate = revalidate + staticGenerationStore.revalidate = finalRevalidate } const isCacheableRevalidate = - (typeof revalidate === 'number' && revalidate > 0) || - revalidate === false + (typeof finalRevalidate === 'number' && finalRevalidate > 0) || + finalRevalidate === false let cacheKey: string | undefined if (staticGenerationStore.incrementalCache && isCacheableRevalidate) { @@ -494,7 +509,7 @@ function createPatchedFetcher( staticGenerationStore.nextFetchId = fetchIdx + 1 const normalizedRevalidate = - typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate + typeof finalRevalidate !== 'number' ? CACHE_ONE_YEAR : finalRevalidate const doOriginalFetch = async ( isStale?: boolean, @@ -552,7 +567,9 @@ function createPatchedFetcher( url: fetchUrl, cacheReason: cacheReasonOverride || cacheReason, cacheStatus: - revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss', + finalRevalidate === 0 || cacheReasonOverride + ? 'skip' + : 'miss', status: res.status, method: clonedInit.method || 'GET', }) @@ -580,7 +597,7 @@ function createPatchedFetcher( }, { fetchCache: true, - revalidate, + revalidate: finalRevalidate, fetchUrl, fetchIdx, tags, @@ -613,7 +630,7 @@ function createPatchedFetcher( ? null : await staticGenerationStore.incrementalCache.get(cacheKey, { kindHint: 'fetch', - revalidate, + revalidate: finalRevalidate, fetchUrl, fetchIdx, tags, diff --git a/test/e2e/app-dir/app-custom-cache-handler/app/layout.js b/test/e2e/app-dir/app-custom-cache-handler/app/layout.js index 8525f5f8c0b2a..49031b1ac08ad 100644 --- a/test/e2e/app-dir/app-custom-cache-handler/app/layout.js +++ b/test/e2e/app-dir/app-custom-cache-handler/app/layout.js @@ -1,3 +1,5 @@ +export const fetchCache = 'default-cache' + export const metadata = { title: 'Next.js', description: 'Generated by Next.js', diff --git a/test/e2e/app-dir/app-fetch-deduping/app/layout.js b/test/e2e/app-dir/app-fetch-deduping/app/layout.js index 69e348c39ef16..eba742a8ebe04 100644 --- a/test/e2e/app-dir/app-fetch-deduping/app/layout.js +++ b/test/e2e/app-dir/app-fetch-deduping/app/layout.js @@ -1,3 +1,5 @@ +export const fetchCache = 'default-cache' + export default function Layout({ children }) { return ( diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 204714fe9270c..ca8e6d170d3d3 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -39,6 +39,33 @@ describe('app-dir static/dynamic handling', () => { } }) + it('should use auto no cache when no fetch config', async () => { + const res = await next.fetch('/no-config-fetch') + expect(res.status).toBe(200) + + const html = await res.text() + const $ = cheerio.load(html) + const data = $('#data').text() + + expect(data).toBeTruthy() + + const res2 = await next.fetch('/no-config-fetch') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#data').text() + + if (isNextDev) { + expect(data).not.toBe(data2) + } else { + const pageCache = ( + res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache') + ).toLowerCase() + + expect(pageCache).toBeTruthy() + expect(pageCache).not.toBe('MISS') + expect(data).toBe(data2) + } + }) + it('should still cache even though the `traceparent` header was different', async () => { const res = await next.fetch('/strip-header-traceparent') expect(res.status).toBe(200) @@ -642,6 +669,8 @@ describe('app-dir static/dynamic handling', () => { [ "(new)/custom/page.js", "(new)/custom/page_client-reference-manifest.js", + "(new)/no-config-fetch/page.js", + "(new)/no-config-fetch/page_client-reference-manifest.js", "_not-found.html", "_not-found.rsc", "_not-found/page.js", @@ -744,6 +773,8 @@ describe('app-dir static/dynamic handling', () => { "isr-error-handling.rsc", "isr-error-handling/page.js", "isr-error-handling/page_client-reference-manifest.js", + "no-config-fetch.html", + "no-config-fetch.rsc", "no-store/dynamic/page.js", "no-store/dynamic/page_client-reference-manifest.js", "no-store/static.html", @@ -1233,6 +1264,22 @@ describe('app-dir static/dynamic handling', () => { "initialRevalidateSeconds": 3, "srcRoute": "/isr-error-handling", }, + "/no-config-fetch": { + "dataRoute": "/no-config-fetch.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/no-config-fetch", + }, "/no-store/static": { "dataRoute": "/no-store/static.rsc", "experimentalBypassFor": [ @@ -2465,8 +2512,12 @@ describe('app-dir static/dynamic handling', () => { const html2 = await res2.text() const $2 = cheerio.load(html2) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) + // this relies on ISR level cache which isn't + // applied in dev + if (!isNextDev) { + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + } return 'success' }, 'success') }) @@ -2610,8 +2661,12 @@ describe('app-dir static/dynamic handling', () => { const html2 = await res2.text() const $2 = cheerio.load(html2) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) + // this relies on ISR level cache which isn't + // applied in dev + if (!isNextDev) { + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + } return 'success' }, 'success') }) diff --git a/test/e2e/app-dir/app-static/app/(new)/no-config-fetch/page.js b/test/e2e/app-dir/app-static/app/(new)/no-config-fetch/page.js new file mode 100644 index 0000000000000..c0c85280ff101 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/(new)/no-config-fetch/page.js @@ -0,0 +1,12 @@ +export default async function Page() { + const data = await fetch( + `https://next-data-api-endpoint.vercel.app/api/random` + ).then((res) => res.text()) + + return ( + <> +

/no-config-fetch

+

{data}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-lang/[lang]/[slug]/page.js b/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-lang/[lang]/[slug]/page.js index 2767974e967ce..45da73dcfed37 100644 --- a/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-lang/[lang]/[slug]/page.js +++ b/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-lang/[lang]/[slug]/page.js @@ -1,4 +1,5 @@ export const dynamicParams = true +export const fetchCache = 'default-cache' export async function generateStaticParams() { const res = await fetch( diff --git a/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-slug/[lang]/[slug]/page.js b/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-slug/[lang]/[slug]/page.js index 5fd4a8c2e7907..7e132527eb4c4 100644 --- a/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-slug/[lang]/[slug]/page.js +++ b/test/e2e/app-dir/app-static/app/partial-gen-params-no-additional-slug/[lang]/[slug]/page.js @@ -1,4 +1,5 @@ export const dynamicParams = false +export const fetchCache = 'default-cache' export async function generateStaticParams() { const res = await fetch( diff --git a/test/e2e/app-dir/logging/app/layout.js b/test/e2e/app-dir/logging/app/layout.js index cec70c5647bac..a2ab1682784bf 100644 --- a/test/e2e/app-dir/logging/app/layout.js +++ b/test/e2e/app-dir/logging/app/layout.js @@ -1,5 +1,7 @@ import Link from 'next/link' +export const fetchCache = 'default-cache' + export default function Layout({ children }) { return ( diff --git a/test/e2e/app-dir/revalidate-dynamic/app/layout.js b/test/e2e/app-dir/revalidate-dynamic/app/layout.js index 0ea378c6fcbbd..d8c9b3718fa4e 100644 --- a/test/e2e/app-dir/revalidate-dynamic/app/layout.js +++ b/test/e2e/app-dir/revalidate-dynamic/app/layout.js @@ -1,3 +1,5 @@ +export const fetchCache = 'default-cache' + export default function Layout({ children }) { return (