mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[GH-ISSUE #957] Honor route-level expire value with blocking revalidation in ISR #202
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#202
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @github-actions[bot] on GitHub (Apr 29, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/957
Next.js Change
A prerendered route's
expirevalue — set viacacheLife({ expire })inside'use cache'or via theexpireTimeconfig fallback — was previously not honored at runtime. Past expire, Next.js was serving stale with a background refresh instead of the blocking regeneration that thecacheLifeexpiredocs describe. This commit fixes it so that pastexpire, the runtime performs blocking revalidation and returns the freshly regenerated output to the user.Commit:
8e4cfc5PR: #93211
Fixes: #78269
What Changed
Three coordinated changes:
responseGeneratorinapp-page.ts,app-route.ts,pages-handler.ts: Applies theexpireTimefallback as soon as it has the render'scacheControl, so every downstream consumer (the cache stored viaIncrementalCache.set, the responseCache-Controlheader, the entry returned tohandleResponse) sees a finalizedcacheControlwith a populatedexpire— mirroring the build-time fallback.IncrementalCache.get: ReturnsisStale = -1whenlastModified + expire * 1000 < now.response-cache.handleGet: Skips its earlyresolve(previousEntry)whenisStale === -1, so the blocking revalidation insideresponseGenerator(which already picksBLOCKING_STATIC_RENDERon that signal) can return its fresh output to the user.Previously the early resolve committed the stale value to the response first, so even though
responseGeneratorstill ran a fresh render its output only warmed the cache for the next request. As a side effect this also closes the same early-resolve hole on the existing tag-expiredisStale = -1path.Impact on vinext
vinext has its own ISR implementation (
isr-cache.ts,server/modules) that distinguishes time-expired stale entries (returned with stale-while-revalidate semantics) from tag-invalidated entries (returned as null/MISS for blocking regen). The current architecture, perAGENTS.md, treats every time-based expiration as SWR.vinext should match Next.js behavior:
revalidatewindow but withinexpire: serve stale, regenerate in background (current behavior, OK).expirewindow: block the response on a fresh render, just like a tag-invalidated entry.This requires:
revalidateandexpiredurations per cache key (vinext currently tracks onlyrevalidate).expireTimeconfig fallback so routes without an explicitcacheLife({ expire })still get a finite expire window.Suggested Action
isr-cache.tsand the cache write path to confirm whetherexpireis currently captured; if not, plumb it through alongsiderevalidate.expireTimeconfig fallback so routes withoutcacheLifestill pick up the configured ceiling.test/production/app-dir/use-cache-expire(custom cache handler +x-test-cache-age-offset-msheader) andtest/e2e/app-dir/expire-time(classicrevalidate+expireTimepair).