Cache: honour Cache-Control on cache entries - #47
Conversation
commit: |
| if (revalidate === undefined) { | ||
| // `revalidate` is written by the cache handler for every entry, we should always have one here. | ||
| error("Missing `revalidate` on a cache entry, assuming it is a static (SSG) entry"); | ||
| } | ||
|
|
||
| if (revalidate === undefined || revalidate === false) { | ||
| return buildCacheControl(CACHE_ONE_YEAR, 0); | ||
| } |
There was a problem hiding this comment.
🔴 Pages built ahead of time are told to stay cached for a year instead of their own refresh interval
Pre-built page entries, which carry no refresh interval, are treated as never-changing and given a one-year lifetime (buildCacheControl(CACHE_ONE_YEAR, 0) at packages/core/src/utils/cache-control.ts:85) instead of the page's configured refresh interval, so incrementally-regenerated pages can stay frozen for a year in any cache sitting in front of the cache handler.
Impact: Pages that are supposed to refresh every few seconds/minutes may keep serving build-time content until manually purged, and an error is logged on every read of such a page.
Why build-time entries reach this branch without a revalidate value
The cache assets generated at build time (packages/core/src/build/createAssets.ts:184-197) write only { type, meta, html, json, rsc, body, segmentData } — there is no revalidate field. Those files are what globalThis.incrementalCache.get(key, "cache") returns until the page is regenerated at runtime (runtime writes do include revalidate, see packages/core/src/adapters/cache.ts:145-233).
So for every prerendered ISR page, computeEntryCacheControl hits the revalidate === undefined path: it logs error("Missing \revalidate` on a cache entry, ...") (packages/core/src/utils/cache-control.ts:81) and returns s-maxage=31536000`.
The pre-existing computeCacheControl in packages/core/src/core/routing/cacheInterceptor.ts:42-50 handles exactly this case by falling back to PrerenderManifest.routes[path].initialRevalidateSeconds when revalidate is undefined; the new util has no equivalent fallback.
Prompt for agents
computeEntryCacheControl in packages/core/src/utils/cache-control.ts assumes every cached-file entry carries a `revalidate` value and treats a missing one as a static (SSG) entry cached for a year, while also logging an error. That assumption does not hold for entries produced at build time: createCacheAssets (packages/core/src/build/createAssets.ts) writes cache files containing only type/meta/html/json/rsc/body/segmentData, with no `revalidate`. Those entries are served for every prerendered route until the first runtime regeneration, so ISR pages with e.g. `revalidate: 60` would be advertised as cacheable for a year (and would spam the error log on every read).
Possible approaches: derive the fallback the same way cacheInterceptor's computeCacheControl already does, by looking up PrerenderManifest.routes[path].initialRevalidateSeconds when `revalidate` is undefined (this requires the entry key/path to be threaded into the helper), or make the undefined case conservative (no-store / short s-maxage) instead of a one-year lifetime, and drop or downgrade the error log since the situation is expected for build-time entries.
Was this helpful? React with 👍 or 👎 to provide feedback.
26ebe6c to
d118f38
Compare
18f4542 to
8672cf3
Compare
The cache handler function now derives freshness from the entry's `Cache-Control` rather than from the revalidation timestamp alone. - New `utils/cache-control.ts` parses `s-maxage`, `stale-while-revalidate` and `must-revalidate`, and the resulting fresh / stale / expired state travels back to the caller through the response headers that `cache-get.ts` reads. - `cacheInterceptor` follows the same state. - The `fetch` and `local` cache overrides forward the cache type in `set`: incremental caches that key entries on the type were writing them where `get` does not look.
d118f38 to
3182170
Compare
8672cf3 to
0799572
Compare
The cache handler now derives freshness from the entry's
Cache-Controlrather than from the revalidation timestamp alone.utils/cache-control.tsparsess-maxage,stale-while-revalidateandmust-revalidate; the resulting fresh / stale / expired state travels back to the caller through the response headers thatcache-get.tsreads.cacheInterceptorfollows the same state.fetchandlocalcache overrides forward the cache type inset— incremental caches that key entries on the type were writing them wheregetdoes not look.