Skip to content

Commit 4d14e83

Browse files
authored
[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.
1 parent 6c1c004 commit 4d14e83

10 files changed

Lines changed: 168 additions & 59 deletions

File tree

packages/next/src/build/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,21 @@ export async function buildAppStaticPaths({
14381438
const newParams: Params[] = []
14391439

14401440
if (curGenerate.generateStaticParams) {
1441+
const curStore =
1442+
ComponentMod.staticGenerationAsyncStorage.getStore()
1443+
1444+
if (curStore) {
1445+
if (typeof curGenerate?.config?.fetchCache !== 'undefined') {
1446+
curStore.fetchCache = curGenerate.config.fetchCache
1447+
}
1448+
if (typeof curGenerate?.config?.revalidate !== 'undefined') {
1449+
curStore.revalidate = curGenerate.config.revalidate
1450+
}
1451+
if (curGenerate?.config?.dynamic === 'force-dynamic') {
1452+
curStore.forceDynamic = true
1453+
}
1454+
}
1455+
14411456
for (const params of paramsItems) {
14421457
const result = await curGenerate.generateStaticParams({
14431458
params,

packages/next/src/server/lib/patch-fetch.ts

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ function createPatchedFetcher(
289289
return value || (isRequestInput ? (input as any)[field] : null)
290290
}
291291

292-
let revalidate: number | undefined | false = undefined
292+
let finalRevalidate: number | undefined | false = undefined
293293
const getNextField = (field: 'revalidate' | 'tags') => {
294294
return typeof init?.next?.[field] !== 'undefined'
295295
? init?.next?.[field]
@@ -299,7 +299,7 @@ function createPatchedFetcher(
299299
}
300300
// RequestInit doesn't keep extra fields e.g. next so it's
301301
// only available if init is used separate
302-
let curRevalidate = getNextField('revalidate')
302+
let currentFetchRevalidate = getNextField('revalidate')
303303
const tags: string[] = validateTags(
304304
getNextField('tags') || [],
305305
`fetch ${input.toString()}`
@@ -317,49 +317,52 @@ function createPatchedFetcher(
317317
}
318318
const implicitTags = addImplicitTags(staticGenerationStore)
319319

320-
const fetchCacheMode = staticGenerationStore.fetchCache
320+
const pageFetchCacheMode = staticGenerationStore.fetchCache
321321
const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore
322322

323-
let _cache = getRequestMeta('cache')
323+
let currentFetchCacheConfig = getRequestMeta('cache')
324324
let cacheReason = ''
325325

326326
if (
327-
typeof _cache === 'string' &&
328-
typeof curRevalidate !== 'undefined'
327+
typeof currentFetchCacheConfig === 'string' &&
328+
typeof currentFetchRevalidate !== 'undefined'
329329
) {
330330
// when providing fetch with a Request input, it'll automatically set a cache value of 'default'
331331
// we only want to warn if the user is explicitly setting a cache value
332-
if (!(isRequestInput && _cache === 'default')) {
332+
if (!(isRequestInput && currentFetchCacheConfig === 'default')) {
333333
Log.warn(
334-
`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.`
334+
`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.`
335335
)
336336
}
337-
_cache = undefined
337+
currentFetchCacheConfig = undefined
338338
}
339339

340-
if (_cache === 'force-cache') {
341-
curRevalidate = false
340+
if (currentFetchCacheConfig === 'force-cache') {
341+
currentFetchRevalidate = false
342342
} else if (
343-
_cache === 'no-cache' ||
344-
_cache === 'no-store' ||
345-
fetchCacheMode === 'force-no-store' ||
346-
fetchCacheMode === 'only-no-store' ||
343+
currentFetchCacheConfig === 'no-cache' ||
344+
currentFetchCacheConfig === 'no-store' ||
345+
pageFetchCacheMode === 'force-no-store' ||
346+
pageFetchCacheMode === 'only-no-store' ||
347347
// If no explicit fetch cache mode is set, but dynamic = `force-dynamic` is set,
348348
// we shouldn't consider caching the fetch. This is because the `dynamic` cache
349349
// is considered a "top-level" cache mode, whereas something like `fetchCache` is more
350350
// fine-grained. Top-level modes are responsible for setting reasonable defaults for the
351351
// other configurations.
352-
(!fetchCacheMode && staticGenerationStore.forceDynamic)
352+
(!pageFetchCacheMode && staticGenerationStore.forceDynamic)
353353
) {
354-
curRevalidate = 0
354+
currentFetchRevalidate = 0
355355
}
356356

357-
if (_cache === 'no-cache' || _cache === 'no-store') {
358-
cacheReason = `cache: ${_cache}`
357+
if (
358+
currentFetchCacheConfig === 'no-cache' ||
359+
currentFetchCacheConfig === 'no-store'
360+
) {
361+
cacheReason = `cache: ${currentFetchCacheConfig}`
359362
}
360363

361-
revalidate = validateRevalidate(
362-
curRevalidate,
364+
finalRevalidate = validateRevalidate(
365+
currentFetchRevalidate,
363366
staticGenerationStore.urlPathname
364367
)
365368

@@ -379,20 +382,29 @@ function createPatchedFetcher(
379382
// if there are authorized headers or a POST method and
380383
// dynamic data usage was present above the tree we bail
381384
// e.g. if cookies() is used before an authed/POST fetch
385+
// or no user provided fetch cache config or revalidate
386+
// is provided we don't cache
382387
const autoNoCache =
383-
(hasUnCacheableHeader || isUnCacheableMethod) &&
384-
staticGenerationStore.revalidate === 0
385-
386-
switch (fetchCacheMode) {
388+
// this condition is hit for null/undefined
389+
// eslint-disable-next-line eqeqeq
390+
(pageFetchCacheMode == undefined &&
391+
// eslint-disable-next-line eqeqeq
392+
currentFetchCacheConfig == undefined &&
393+
// eslint-disable-next-line eqeqeq
394+
currentFetchRevalidate == undefined) ||
395+
((hasUnCacheableHeader || isUnCacheableMethod) &&
396+
staticGenerationStore.revalidate === 0)
397+
398+
switch (pageFetchCacheMode) {
387399
case 'force-no-store': {
388400
cacheReason = 'fetchCache = force-no-store'
389401
break
390402
}
391403
case 'only-no-store': {
392404
if (
393-
_cache === 'force-cache' ||
394-
(typeof revalidate !== 'undefined' &&
395-
(revalidate === false || revalidate > 0))
405+
currentFetchCacheConfig === 'force-cache' ||
406+
(typeof finalRevalidate !== 'undefined' &&
407+
(finalRevalidate === false || finalRevalidate > 0))
396408
) {
397409
throw new Error(
398410
`cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'`
@@ -402,17 +414,20 @@ function createPatchedFetcher(
402414
break
403415
}
404416
case 'only-cache': {
405-
if (_cache === 'no-store') {
417+
if (currentFetchCacheConfig === 'no-store') {
406418
throw new Error(
407419
`cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'`
408420
)
409421
}
410422
break
411423
}
412424
case 'force-cache': {
413-
if (typeof curRevalidate === 'undefined' || curRevalidate === 0) {
425+
if (
426+
typeof currentFetchRevalidate === 'undefined' ||
427+
currentFetchRevalidate === 0
428+
) {
414429
cacheReason = 'fetchCache = force-cache'
415-
revalidate = false
430+
finalRevalidate = false
416431
}
417432
break
418433
}
@@ -423,59 +438,59 @@ function createPatchedFetcher(
423438
// simplify the switch case and ensure we have an exhaustive switch handling all modes
424439
}
425440

426-
if (typeof revalidate === 'undefined') {
427-
if (fetchCacheMode === 'default-cache') {
428-
revalidate = false
441+
if (typeof finalRevalidate === 'undefined') {
442+
if (pageFetchCacheMode === 'default-cache' && !isUsingNoStore) {
443+
finalRevalidate = false
429444
cacheReason = 'fetchCache = default-cache'
430-
} else if (autoNoCache) {
431-
revalidate = 0
432-
cacheReason = 'auto no cache'
433-
} else if (fetchCacheMode === 'default-no-store') {
434-
revalidate = 0
445+
} else if (pageFetchCacheMode === 'default-no-store') {
446+
finalRevalidate = 0
435447
cacheReason = 'fetchCache = default-no-store'
436448
} else if (isUsingNoStore) {
437-
revalidate = 0
449+
finalRevalidate = 0
438450
cacheReason = 'noStore call'
451+
} else if (autoNoCache) {
452+
finalRevalidate = 0
453+
cacheReason = 'auto no cache'
439454
} else {
455+
// TODO: should we consider this case an invariant?
440456
cacheReason = 'auto cache'
441-
revalidate =
457+
finalRevalidate =
442458
typeof staticGenerationStore.revalidate === 'boolean' ||
443459
typeof staticGenerationStore.revalidate === 'undefined'
444460
? false
445461
: staticGenerationStore.revalidate
446462
}
447463
} else if (!cacheReason) {
448-
cacheReason = `revalidate: ${revalidate}`
464+
cacheReason = `revalidate: ${finalRevalidate}`
449465
}
450466

451467
if (
452468
// when force static is configured we don't bail from
453469
// `revalidate: 0` values
454-
!(staticGenerationStore.forceStatic && revalidate === 0) &&
455-
// we don't consider autoNoCache to switch to dynamic during
456-
// revalidate although if it occurs during build we do
470+
!(staticGenerationStore.forceStatic && finalRevalidate === 0) &&
471+
// we don't consider autoNoCache to switch to dynamic for ISR
457472
!autoNoCache &&
458473
// If the revalidate value isn't currently set or the value is less
459474
// than the current revalidate value, we should update the revalidate
460475
// value.
461476
(typeof staticGenerationStore.revalidate === 'undefined' ||
462-
(typeof revalidate === 'number' &&
477+
(typeof finalRevalidate === 'number' &&
463478
(staticGenerationStore.revalidate === false ||
464479
(typeof staticGenerationStore.revalidate === 'number' &&
465-
revalidate < staticGenerationStore.revalidate))))
480+
finalRevalidate < staticGenerationStore.revalidate))))
466481
) {
467482
// If we were setting the revalidate value to 0, we should try to
468483
// postpone instead first.
469-
if (revalidate === 0) {
484+
if (finalRevalidate === 0) {
470485
trackDynamicFetch(staticGenerationStore, 'revalidate: 0')
471486
}
472487

473-
staticGenerationStore.revalidate = revalidate
488+
staticGenerationStore.revalidate = finalRevalidate
474489
}
475490

476491
const isCacheableRevalidate =
477-
(typeof revalidate === 'number' && revalidate > 0) ||
478-
revalidate === false
492+
(typeof finalRevalidate === 'number' && finalRevalidate > 0) ||
493+
finalRevalidate === false
479494

480495
let cacheKey: string | undefined
481496
if (staticGenerationStore.incrementalCache && isCacheableRevalidate) {
@@ -494,7 +509,7 @@ function createPatchedFetcher(
494509
staticGenerationStore.nextFetchId = fetchIdx + 1
495510

496511
const normalizedRevalidate =
497-
typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate
512+
typeof finalRevalidate !== 'number' ? CACHE_ONE_YEAR : finalRevalidate
498513

499514
const doOriginalFetch = async (
500515
isStale?: boolean,
@@ -552,7 +567,9 @@ function createPatchedFetcher(
552567
url: fetchUrl,
553568
cacheReason: cacheReasonOverride || cacheReason,
554569
cacheStatus:
555-
revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss',
570+
finalRevalidate === 0 || cacheReasonOverride
571+
? 'skip'
572+
: 'miss',
556573
status: res.status,
557574
method: clonedInit.method || 'GET',
558575
})
@@ -580,7 +597,7 @@ function createPatchedFetcher(
580597
},
581598
{
582599
fetchCache: true,
583-
revalidate,
600+
revalidate: finalRevalidate,
584601
fetchUrl,
585602
fetchIdx,
586603
tags,
@@ -613,7 +630,7 @@ function createPatchedFetcher(
613630
? null
614631
: await staticGenerationStore.incrementalCache.get(cacheKey, {
615632
kindHint: 'fetch',
616-
revalidate,
633+
revalidate: finalRevalidate,
617634
fetchUrl,
618635
fetchIdx,
619636
tags,

test/e2e/app-dir/app-custom-cache-handler/app/layout.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export const fetchCache = 'default-cache'
2+
13
export const metadata = {
24
title: 'Next.js',
35
description: 'Generated by Next.js',

test/e2e/app-dir/app-fetch-deduping/app/layout.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export const fetchCache = 'default-cache'
2+
13
export default function Layout({ children }) {
24
return (
35
<html lang="en">

0 commit comments

Comments
 (0)