[Breaking] Disable automatic fetch caching (#66004)
## Background Previously we introduced automatic caching for `fetch` based on certain heuristics that were a bit tricky to grasp all scenarios. The scenarios we would automatically cache were no dynamic data access before the fetch call e.g. `headers()` or `cookies()`, the fetch call is inside of a dynamic page e.g. `POST` method or `export const revalidate = 0` page and the fetch is a non-`GET` request or has `Authorization` or `Cookie` headers, or the fetch had `cache: 'no-store' | 'no-cache'` or `revalidate: 0`. ## New Behavior By default fetches will no longer automatically be cached. Instead they need to be opted-in to caching via `export const fetchCache = 'default-cache' | 'force-cache',` `next: { revalidate: false or value > 0 }` or `cache: 'force-cache' | 'default-cache'`. When the fetch call is automatically skipping the cache it won't impact the page level ISR cacheability although if a fetch call manually specifies `cache: 'no-store'` or `revalidate: 0` it will still bail from the page being statically generated as it was before. To achieve the previous behavior of automatic fetch caching all that needs to be added is `export const fetchCache = 'default-cache'` in the root layout(s) of your project.
This commit is contained in:
parent
6c1c004953
commit
4d14e83173
10 changed files with 167 additions and 58 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
// 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 (fetchCacheMode) {
|
||||
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,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const fetchCache = 'default-cache'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next.js',
|
||||
description: 'Generated by Next.js',
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const fetchCache = 'default-cache'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<p>/no-config-fetch</p>
|
||||
<p id="data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export const dynamicParams = true
|
||||
export const fetchCache = 'default-cache'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const res = await fetch(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const dynamicParams = false
|
||||
export const fetchCache = 'default-cache'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const res = await fetch(
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const fetchCache = 'default-cache'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const fetchCache = 'default-cache'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
|
|
Loading…
Reference in a new issue