Fix fetchCache and no-store handling (#49638)
Follow-up to https://github.com/vercel/next.js/pull/49628 this updates the `export const fetchCache` handling and `cache: 'no-store'` handling as discussed which also aligns with our docs here https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#fetchcache - `export const fetchCache = 'force-cache'`: forces all fetches to be cached regardless of `cache: 'no-store'` but cacheable `revalidate` values still take priority - `export const fetchCache = 'default-cache'`: ensures fetches are cached even if they come after a `cache: 'no-store'` fetch but don't override `cache: 'no-store'` itself. - without `export const fetchCache`, we still disable fetch cache for successive fetches after a fetch that does `cache: 'no-store'` x-ref: [slack thread](https://vercel.slack.com/archives/C03KAR5DCKC/p1683732826894469)
This commit is contained in:
parent
f3222471c6
commit
2eeb1c1bd1
6 changed files with 327 additions and 11 deletions
|
@ -15,6 +15,7 @@ export interface StaticGenerationStore {
|
|||
fetchCache?:
|
||||
| 'only-cache'
|
||||
| 'force-cache'
|
||||
| 'default-cache'
|
||||
| 'force-no-store'
|
||||
| 'default-no-store'
|
||||
| 'only-no-store'
|
||||
|
|
|
@ -32,6 +32,7 @@ function trackFetchMetric(
|
|||
url: string
|
||||
status: number
|
||||
method: string
|
||||
cacheReason: string
|
||||
cacheStatus: 'hit' | 'miss'
|
||||
start: number
|
||||
}
|
||||
|
@ -158,6 +159,8 @@ export function patchFetch({
|
|||
}
|
||||
const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache'
|
||||
const isForceCache = staticGenerationStore.fetchCache === 'force-cache'
|
||||
const isDefaultCache =
|
||||
staticGenerationStore.fetchCache === 'default-cache'
|
||||
const isDefaultNoStore =
|
||||
staticGenerationStore.fetchCache === 'default-no-store'
|
||||
const isOnlyNoStore =
|
||||
|
@ -165,9 +168,19 @@ export function patchFetch({
|
|||
const isForceNoStore =
|
||||
staticGenerationStore.fetchCache === 'force-no-store'
|
||||
|
||||
const _cache = getRequestMeta('cache')
|
||||
let _cache = getRequestMeta('cache')
|
||||
|
||||
if (_cache === 'force-cache' || isForceCache) {
|
||||
if (
|
||||
typeof _cache === 'string' &&
|
||||
typeof curRevalidate !== 'undefined'
|
||||
) {
|
||||
console.warn(
|
||||
`Warning: fetch for ${input.toString()} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.`
|
||||
)
|
||||
_cache = undefined
|
||||
}
|
||||
|
||||
if (_cache === 'force-cache') {
|
||||
curRevalidate = false
|
||||
}
|
||||
if (['no-cache', 'no-store'].includes(_cache || '')) {
|
||||
|
@ -177,6 +190,7 @@ export function patchFetch({
|
|||
revalidate = curRevalidate
|
||||
}
|
||||
|
||||
let cacheReason = ''
|
||||
const _headers = getRequestMeta('headers')
|
||||
const initHeaders: Headers =
|
||||
typeof _headers?.get === 'function'
|
||||
|
@ -199,6 +213,7 @@ export function patchFetch({
|
|||
|
||||
if (isForceNoStore) {
|
||||
revalidate = 0
|
||||
cacheReason = 'fetchCache = force-no-store'
|
||||
}
|
||||
|
||||
if (isOnlyNoStore) {
|
||||
|
@ -208,26 +223,43 @@ export function patchFetch({
|
|||
)
|
||||
}
|
||||
revalidate = 0
|
||||
cacheReason = 'fetchCache = only-no-store'
|
||||
}
|
||||
|
||||
if (isOnlyCache && _cache === 'no-store') {
|
||||
throw new Error(
|
||||
`cache: 'no-store' used on fetch for ${input.toString()} with 'export const fetchCache = 'only-cache'`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
isForceCache &&
|
||||
(typeof curRevalidate === 'undefined' || curRevalidate === 0)
|
||||
) {
|
||||
cacheReason = 'fetchCache = force-cache'
|
||||
revalidate = false
|
||||
}
|
||||
|
||||
if (typeof revalidate === 'undefined') {
|
||||
if (isOnlyCache && _cache === 'no-store') {
|
||||
throw new Error(
|
||||
`cache: 'no-store' used on fetch for ${input.toString()} with 'export const fetchCache = 'only-cache'`
|
||||
)
|
||||
}
|
||||
|
||||
if (autoNoCache) {
|
||||
if (isDefaultCache) {
|
||||
revalidate = false
|
||||
cacheReason = 'fetchCache = default-cache'
|
||||
} else if (autoNoCache) {
|
||||
revalidate = 0
|
||||
cacheReason = 'auto no cache'
|
||||
} else if (isDefaultNoStore) {
|
||||
revalidate = 0
|
||||
cacheReason = 'fetchCache = default-no-store'
|
||||
} else {
|
||||
cacheReason = 'auto cache'
|
||||
revalidate =
|
||||
typeof staticGenerationStore.revalidate === 'boolean' ||
|
||||
typeof staticGenerationStore.revalidate === 'undefined'
|
||||
? false
|
||||
: staticGenerationStore.revalidate
|
||||
}
|
||||
} else if (!cacheReason) {
|
||||
cacheReason = `revalidate: ${revalidate}`
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -316,6 +348,7 @@ export function patchFetch({
|
|||
trackFetchMetric(staticGenerationStore, {
|
||||
start: fetchStart,
|
||||
url: fetchUrl,
|
||||
cacheReason,
|
||||
cacheStatus: 'miss',
|
||||
status: res.status,
|
||||
method: clonedInit.method || 'GET',
|
||||
|
@ -422,6 +455,7 @@ export function patchFetch({
|
|||
trackFetchMetric(staticGenerationStore, {
|
||||
start: fetchStart,
|
||||
url: fetchUrl,
|
||||
cacheReason,
|
||||
cacheStatus: 'hit',
|
||||
status: resData.status || 200,
|
||||
method: init?.method || 'GET',
|
||||
|
|
|
@ -69,8 +69,12 @@ createNextDescribe(
|
|||
curData = JSON.parse(cur$('#props').text())
|
||||
}
|
||||
|
||||
expect(curData.data.random).toBeTruthy()
|
||||
expect(curData.data.random).toBe(prevData.data.random)
|
||||
try {
|
||||
expect(curData.data.random).toBeTruthy()
|
||||
expect(curData.data.random).toBe(prevData.data.random)
|
||||
} finally {
|
||||
prevData = curData
|
||||
}
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
@ -437,10 +441,15 @@ createNextDescribe(
|
|||
'blog/tim.rsc',
|
||||
'blog/tim/first-post.html',
|
||||
'blog/tim/first-post.rsc',
|
||||
'default-cache/page.js',
|
||||
'dynamic-error/[id]/page.js',
|
||||
'dynamic-no-gen-params-ssr/[slug]/page.js',
|
||||
'dynamic-no-gen-params/[slug]/page.js',
|
||||
'fetch-no-cache/page.js',
|
||||
'flight/[slug]/[slug2]/page.js',
|
||||
'force-cache.html',
|
||||
'force-cache.rsc',
|
||||
'force-cache/page.js',
|
||||
'force-dynamic-catch-all/[slug]/[[...id]]/page.js',
|
||||
'force-dynamic-no-prerender/[id]/page.js',
|
||||
'force-dynamic-prerender/[slug]/page.js',
|
||||
|
@ -618,6 +627,11 @@ createNextDescribe(
|
|||
srcRoute: '/blog/[author]/[slug]',
|
||||
dataRoute: '/blog/styfle/second-post.rsc',
|
||||
},
|
||||
'/force-cache': {
|
||||
dataRoute: '/force-cache.rsc',
|
||||
initialRevalidateSeconds: 3,
|
||||
srcRoute: '/force-cache',
|
||||
},
|
||||
'/hooks/use-pathname/slug': {
|
||||
dataRoute: '/hooks/use-pathname/slug.rsc',
|
||||
initialRevalidateSeconds: false,
|
||||
|
@ -880,6 +894,116 @@ createNextDescribe(
|
|||
})
|
||||
}
|
||||
|
||||
it('should cache correctly for fetchCache = default-cache', async () => {
|
||||
const res = await next.fetch('/default-cache')
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
let prevHtml = await res.text()
|
||||
let prev$ = cheerio.load(prevHtml)
|
||||
|
||||
await check(async () => {
|
||||
const curRes = await next.fetch('/default-cache')
|
||||
expect(curRes.status).toBe(200)
|
||||
|
||||
const curHtml = await curRes.text()
|
||||
const cur$ = cheerio.load(curHtml)
|
||||
|
||||
try {
|
||||
expect(cur$('#data-no-cache').text()).not.toBe(
|
||||
prev$('#data-no-cache').text()
|
||||
)
|
||||
expect(cur$('#data-force-cache').text()).toBe(
|
||||
prev$('#data-force-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||||
prev$('#data-revalidate-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||||
prev$('#data-revalidate-and-fetch-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||||
prev$('#data-revalidate-and-fetch-cache').text()
|
||||
)
|
||||
} finally {
|
||||
prevHtml = curHtml
|
||||
prev$ = cur$
|
||||
}
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should cache correctly for fetchCache = force-cache', async () => {
|
||||
const res = await next.fetch('/force-cache')
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
let prevHtml = await res.text()
|
||||
let prev$ = cheerio.load(prevHtml)
|
||||
|
||||
await check(async () => {
|
||||
const curRes = await next.fetch('/force-cache')
|
||||
expect(curRes.status).toBe(200)
|
||||
|
||||
const curHtml = await curRes.text()
|
||||
const cur$ = cheerio.load(curHtml)
|
||||
|
||||
expect(cur$('#data-no-cache').text()).toBe(
|
||||
prev$('#data-no-cache').text()
|
||||
)
|
||||
expect(cur$('#data-force-cache').text()).toBe(
|
||||
prev$('#data-force-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||||
prev$('#data-revalidate-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||||
prev$('#data-revalidate-and-fetch-cache').text()
|
||||
)
|
||||
expect(cur$('#data-auto-cache').text()).toBe(
|
||||
prev$('#data-auto-cache').text()
|
||||
)
|
||||
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should cache correctly for cache: no-store', async () => {
|
||||
const res = await next.fetch('/fetch-no-cache')
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
let prevHtml = await res.text()
|
||||
let prev$ = cheerio.load(prevHtml)
|
||||
|
||||
await check(async () => {
|
||||
const curRes = await next.fetch('/fetch-no-cache')
|
||||
expect(curRes.status).toBe(200)
|
||||
|
||||
const curHtml = await curRes.text()
|
||||
const cur$ = cheerio.load(curHtml)
|
||||
|
||||
try {
|
||||
expect(cur$('#data-no-cache').text()).not.toBe(
|
||||
prev$('#data-no-cache').text()
|
||||
)
|
||||
expect(cur$('#data-force-cache').text()).toBe(
|
||||
prev$('#data-force-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||||
prev$('#data-revalidate-cache').text()
|
||||
)
|
||||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||||
prev$('#data-revalidate-and-fetch-cache').text()
|
||||
)
|
||||
expect(cur$('#data-auto-cache').text()).not.toBe(
|
||||
prev$('#data-auto-cache').text()
|
||||
)
|
||||
} finally {
|
||||
prevHtml = curHtml
|
||||
prev$ = cur$
|
||||
}
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
if (isDev) {
|
||||
it('should bypass fetch cache with cache-control: no-cache', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
|
|
53
test/e2e/app-dir/app-static/app/default-cache/page.js
Normal file
53
test/e2e/app-dir/app-static/app/default-cache/page.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
export const fetchCache = 'default-cache'
|
||||
|
||||
export default async function Page() {
|
||||
const dataNoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?a1',
|
||||
{
|
||||
cache: 'no-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataForceCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?b2',
|
||||
{
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?c3',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateAndFetchCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d4',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataAutoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d4'
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>/force-cache</p>
|
||||
<p id="data-no-cache">"cache: no-cache" {dataNoCache}</p>
|
||||
<p id="data-force-cache">"cache: force-cache" {dataForceCache}</p>
|
||||
<p id="data-revalidate-cache">"revalidate: 3" {dataRevalidateCache}</p>
|
||||
<p id="data-revalidate-and-fetch-cache">
|
||||
"revalidate: 3 and cache: force-cache" {dataRevalidateAndFetchCache}
|
||||
</p>
|
||||
<p id="data-auto-cache">"auto cache" {dataAutoCache}</p>
|
||||
</>
|
||||
)
|
||||
}
|
51
test/e2e/app-dir/app-static/app/fetch-no-cache/page.js
Normal file
51
test/e2e/app-dir/app-static/app/fetch-no-cache/page.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
export default async function Page() {
|
||||
const dataNoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?a2',
|
||||
{
|
||||
cache: 'no-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataForceCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?b3',
|
||||
{
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?c4',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateAndFetchCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d5',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataAutoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d6'
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>/fetch-no-cache</p>
|
||||
<p id="data-no-cache">"cache: no-cache" {dataNoCache}</p>
|
||||
<p id="data-force-cache">"cache: force-cache" {dataForceCache}</p>
|
||||
<p id="data-revalidate-cache">"revalidate: 3" {dataRevalidateCache}</p>
|
||||
<p id="data-revalidate-and-fetch-cache">
|
||||
"revalidate: 3 and cache: force-cache" {dataRevalidateAndFetchCache}
|
||||
</p>
|
||||
<p id="data-auto-cache">"auto cache" {dataAutoCache}</p>
|
||||
</>
|
||||
)
|
||||
}
|
53
test/e2e/app-dir/app-static/app/force-cache/page.js
Normal file
53
test/e2e/app-dir/app-static/app/force-cache/page.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
export const fetchCache = 'force-cache'
|
||||
|
||||
export default async function Page() {
|
||||
const dataNoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?a1',
|
||||
{
|
||||
cache: 'no-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataForceCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?b2',
|
||||
{
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?c3',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataRevalidateAndFetchCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d4',
|
||||
{
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
cache: 'force-cache',
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const dataAutoCache = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?d4'
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>/force-cache</p>
|
||||
<p id="data-no-cache">"cache: no-cache" {dataNoCache}</p>
|
||||
<p id="data-force-cache">"cache: force-cache" {dataForceCache}</p>
|
||||
<p id="data-revalidate-cache">"revalidate: 3" {dataRevalidateCache}</p>
|
||||
<p id="data-revalidate-and-fetch-cache">
|
||||
"revalidate: 3 and cache: force-cache" {dataRevalidateAndFetchCache}
|
||||
</p>
|
||||
<p id="data-auto-cache">"auto cache" {dataAutoCache}</p>
|
||||
</>
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue