Skip to content

Cache: honour Cache-Control on cache entries - #47

Open
conico974 wants to merge 1 commit into
conico/cache-4-swr-portfrom
conico/cache-5-cache-control
Open

Cache: honour Cache-Control on cache entries#47
conico974 wants to merge 1 commit into
conico/cache-4-swr-portfrom
conico/cache-5-cache-control

Conversation

@conico974

@conico974 conico974 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Part 5 of 6 of the cache stack, split out of #31. Review only this PR's diff — it is based on the previous branch.

PR Base
#43 — core: dedicated cache handler function conico/share-build (#35)
#44 — cloudflare: OpenNextCache entrypoint #43
#45 — core: route all caching through the cache override ⚠️ breaking #44
#46 — port SWR tag revalidation from AWS #45
👉 #47 — core: honour Cache-Control on cache entries #46
#48 — cloudflare: per-entrypoint Workers caching #47

The cache handler 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; 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.

@pkg-pr-new

pkg-pr-new Bot commented Aug 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/aws@3182170
npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/cloudflare@3182170
npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/core@3182170

commit: 3182170

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +79 to +86
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@conico974
conico974 force-pushed the conico/cache-5-cache-control branch from 26ebe6c to d118f38 Compare August 16, 2026 13:47
@conico974
conico974 force-pushed the conico/cache-4-swr-port branch from 18f4542 to 8672cf3 Compare August 16, 2026 13:47
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.
@conico974
conico974 force-pushed the conico/cache-5-cache-control branch from d118f38 to 3182170 Compare August 16, 2026 13:55
@conico974
conico974 force-pushed the conico/cache-4-swr-port branch from 8672cf3 to 0799572 Compare August 16, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant