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:
JJ Kasper 2023-05-10 20:42:25 -07:00 committed by GitHub
parent f3222471c6
commit 2eeb1c1bd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 327 additions and 11 deletions

View file

@ -15,6 +15,7 @@ export interface StaticGenerationStore {
fetchCache?:
| 'only-cache'
| 'force-cache'
| 'default-cache'
| 'force-no-store'
| 'default-no-store'
| 'only-no-store'

View file

@ -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',

View file

@ -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(

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}