@@ -24,7 +24,7 @@ import { Buffer } from "node:buffer";
2424import { getDataCacheHandler , type CachedFetchValue , type CacheHandler } from "./cache-handler.js" ;
2525import { encodeCacheTags } from "../utils/encode-cache-tag.js" ;
2626import { getOrCreateAls } from "./internal/als-registry.js" ;
27- import { markDynamicUsage } from "./headers.js" ;
27+ import { getHeadersContext , markDynamicUsage } from "./headers.js" ;
2828import { _hasPendingRevalidatedTag , _setRequestScopedCacheLife } from "./cache-request-state.js" ;
2929import { getRequestExecutionContext } from "./request-context.js" ;
3030import {
@@ -555,6 +555,7 @@ export type FetchCacheState = {
555555 currentRequestTags : string [ ] ;
556556 currentFetchSoftTags : string [ ] ;
557557 currentFetchCacheMode : FetchCacheMode | null ;
558+ currentFetchRevalidate : number | null ;
558559 currentForceDynamicFetchDefault : boolean ;
559560 dynamicFetchUrls : Set < string > ;
560561 refreshStaleFetchesInForeground : boolean ;
@@ -592,6 +593,7 @@ const _fallbackState = (_g[_FALLBACK_KEY] ??= {
592593 currentRequestTags : [ ] ,
593594 currentFetchSoftTags : [ ] ,
594595 currentFetchCacheMode : null ,
596+ currentFetchRevalidate : null ,
595597 currentForceDynamicFetchDefault : false ,
596598 dynamicFetchUrls : new Set < string > ( ) ,
597599 refreshStaleFetchesInForeground : false ,
@@ -615,6 +617,7 @@ function _resetFallbackState(isFetchDedupeActive: boolean): void {
615617 _fallbackState . currentRequestTags = [ ] ;
616618 _fallbackState . currentFetchSoftTags = [ ] ;
617619 _fallbackState . currentFetchCacheMode = null ;
620+ _fallbackState . currentFetchRevalidate = null ;
618621 _fallbackState . currentForceDynamicFetchDefault = false ;
619622 _fallbackState . dynamicFetchUrls = new Set < string > ( ) ;
620623 _fallbackState . refreshStaleFetchesInForeground = false ;
@@ -632,6 +635,13 @@ function recordDynamicFetchObservation(input: string | URL | Request): void {
632635
633636function markUncachedFetchForPageOutput ( input : string | URL | Request ) : void {
634637 recordDynamicFetchObservation ( input ) ;
638+ // Next.js lowers the active prerender store to zero when an uncached fetch
639+ // makes the render dynamic. `force-static` is the exception: dynamic usage
640+ // is suppressed there, so later metadata-only fetches keep inheriting the
641+ // configured route interval.
642+ if ( getHeadersContext ( ) ?. forceStatic !== true ) {
643+ _getState ( ) . currentFetchRevalidate = 0 ;
644+ }
635645 markDynamicUsage ( ) ;
636646}
637647
@@ -645,6 +655,13 @@ function recordFiniteFetchRevalidate(revalidateSeconds: number): void {
645655 }
646656}
647657
658+ function lowerCurrentFetchRevalidate ( revalidateSeconds : number ) : void {
659+ const state = _getState ( ) ;
660+ if ( state . currentFetchRevalidate === null || revalidateSeconds < state . currentFetchRevalidate ) {
661+ state . currentFetchRevalidate = revalidateSeconds ;
662+ }
663+ }
664+
648665function shouldRefreshStaleFetchInForeground ( ) : boolean {
649666 return _getState ( ) . refreshStaleFetchesInForeground ;
650667}
@@ -797,6 +814,10 @@ export function setCurrentFetchCacheMode(mode: FetchCacheMode | null): void {
797814 _getState ( ) . currentFetchCacheMode = mode ;
798815}
799816
817+ export function setCurrentFetchRevalidate ( revalidate : number | null ) : void {
818+ _getState ( ) . currentFetchRevalidate = revalidate ;
819+ }
820+
800821export function setCurrentForceDynamicFetchDefault ( enabled : boolean ) : void {
801822 _getState ( ) . currentForceDynamicFetchDefault = enabled ;
802823}
@@ -1178,11 +1199,20 @@ function createPatchedFetch(): typeof globalThis.fetch {
11781199 } else if ( typeof nextOpts ?. revalidate === "number" && nextOpts . revalidate > 0 ) {
11791200 revalidateSeconds = nextOpts . revalidate ;
11801201 } else {
1181- // Has `next` options but no explicit revalidate — Next.js defaults to
1182- // caching when `next` is present (force-cache behavior).
1183- // If only tags are specified, cache indefinitely.
1202+ // During prerender, a fetch without an explicit cache lifetime inherits
1203+ // the active route's revalidate value in Next.js. Tags make this fetch
1204+ // cacheable, but do not independently make it cache indefinitely.
11841205 if ( nextOpts ?. tags && nextOpts . tags . length > 0 ) {
1185- revalidateSeconds = ONE_YEAR_SECONDS ;
1206+ const routeRevalidate = _getState ( ) . currentFetchRevalidate ;
1207+ if ( routeRevalidate === 0 ) {
1208+ const cleanInit = stripNextFromInit ( init , cacheDirective ) ;
1209+ markUncachedFetchForPageOutput ( input ) ;
1210+ return dedupeFetch ( input , cleanInit ) ;
1211+ }
1212+ revalidateSeconds =
1213+ routeRevalidate === null || routeRevalidate === Infinity
1214+ ? ONE_YEAR_SECONDS
1215+ : routeRevalidate ;
11861216 } else {
11871217 // next: {} with no revalidate or tags — pass through
11881218 const cleanInit = stripNextFromInit ( init , cacheDirective ) ;
@@ -1199,6 +1229,12 @@ function createPatchedFetch(): typeof globalThis.fetch {
11991229 // recording both cacheable and dynamic is conservative — a false "unsafe"
12001230 // result costs performance, not correctness.
12011231 recordCacheableFetchObservation ( input ) ;
1232+ if ( typeof nextOpts ?. revalidate === "number" && nextOpts . revalidate > 0 ) {
1233+ // Upstream mutates the active prerender store when an explicit fetch
1234+ // lifetime is shorter. Later metadata-only fetches inherit that live
1235+ // minimum rather than the route's original segment-config seed.
1236+ lowerCurrentFetchRevalidate ( nextOpts . revalidate ) ;
1237+ }
12021238 recordFiniteFetchRevalidate ( revalidateSeconds ) ;
12031239 const reqTags = _getState ( ) . currentRequestTags ;
12041240 const tags = encodeCacheTags ( nextOpts ?. tags ?? [ ] ) ;
@@ -1440,6 +1476,7 @@ export async function runWithFetchCache<T>(fn: () => Promise<T>): Promise<T> {
14401476 currentRequestTags : [ ] ,
14411477 currentFetchSoftTags : [ ] ,
14421478 currentFetchCacheMode : null ,
1479+ currentFetchRevalidate : null ,
14431480 currentForceDynamicFetchDefault : false ,
14441481 dynamicFetchUrls : new Set < string > ( ) ,
14451482 refreshStaleFetchesInForeground : false ,
0 commit comments