Skip to content

Commit

Permalink
[Breaking] Disable automatic fetch caching (#66004)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
ijjk authored May 23, 2024
1 parent 6c1c004 commit 4d14e83
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 59 deletions.
15 changes: 15 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
127 changes: 72 additions & 55 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()}`
Expand All @@ -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
)

Expand All @@ -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'`
Expand All @@ -402,17 +414,20 @@ 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'`
)
}
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
}
Expand All @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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',
})
Expand Down Expand Up @@ -580,7 +597,7 @@ function createPatchedFetcher(
},
{
fetchCache: true,
revalidate,
revalidate: finalRevalidate,
fetchUrl,
fetchIdx,
tags,
Expand Down Expand Up @@ -613,7 +630,7 @@ function createPatchedFetcher(
? null
: await staticGenerationStore.incrementalCache.get(cacheKey, {
kindHint: 'fetch',
revalidate,
revalidate: finalRevalidate,
fetchUrl,
fetchIdx,
tags,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-custom-cache-handler/app/layout.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const fetchCache = 'default-cache'

export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-fetch-deduping/app/layout.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const fetchCache = 'default-cache'

export default function Layout({ children }) {
return (
<html lang="en">
Expand Down
Loading

0 comments on commit 4d14e83

Please sign in to comment.