fix(edge): keep what a middleware let on — optimized images in the cache, hashed files in the browser

SO
19h ago
+645-18

Summary

Two caching fixes for what an application's middleware covered and let through: optimized images are now kept in the edge's image cache, and build-hashed files go out as browser-cacheable for their year (still private).

Why

  • /_next/image under a continuing middleware read the image cache but never wrote it. Every such request — every image, for a middleware with no matcher (Next.js runs one on _next/image and _next/static unless a matcher excludes them) — and every browser revalidation (answered 304 only after a full answer) re-read the source and re-ran the transformation; a source behind a middleware of its own ran it again. Next.js's own optimizer keys its cache on href, width, quality and type only, and fetches internal sources with no request headers.
  • A build-hashed file under a continuing middleware that set no Cache-Control went out private, max-age=0, must-revalidate, so each repeat view revalidated every render-blocking script and style sheet (a round trip plus a middleware run per file), where Next.js lets every browser keep such a file for a year.

Changes

  • edge/handlers/image.ts: the store condition no longer excludes middleware-covered requests. The bytes are everyone's: the source is fetched anonymously (fetchImageSource), and what the middleware can change of the result (rewritten parameters, the accept the format is chosen by) is in the key. The middleware's headers, rewrite status and private caching are laid on each answer, hit or miss, never on the entry; validation still neither reads nor writes.
  • core/request/constants.ts: PRIVATE_IMMUTABLE_CACHE_CONTROL (private, max-age=31536000, immutable).
  • edge/handlers/static-file.ts, hosted-static-file.ts: a file the middleware let on, with no Cache-Control of its own, no cookie and no conditioned rule beside it, goes out with the new value when it is the file as itself — immutable, at the name the request asked for (or kept for it by a rewrite that adds a query or a locale), under its own status. A file a rewrite landed another path on, a rewrite status, a non-immutable file, a cookie, a conditioned rule, or a rule that gave the file a lifetime of its own keep private, max-age=0, must-revalidate; a rule's no-store stands (private, no-store); a middleware's own Cache-Control still stands; validation and private delivery stay no-store. (Next.js gives _next/static its year only where nothing set a Cache-Control already, so a rule's stands there too.)
  • runtime/native/image-cache.ts: comment only — it no longer claims the edge keeps nothing a middleware had a say on.

Tests

  • edge/test/image-middleware-cache.test.ts: a miss is kept and the next request is a HIT with no source read and no transformation, carrying that request's own middleware headers, rewrite status and private caching; a browser revalidation is a 304 from the entry; a rewritten request is kept under the rewritten parameters; validation neither reads nor writes.
  • edge/test/static-file-middleware-holding.test.ts: the year for the file, its 304, a rewrite that keeps the file; revalidated for a non-immutable file, a cookie, a conditioned rule, a rewrite from another path, a rewrite status; the middleware's own Cache-Control; private delivery.
  • Locally: the touched and neighbouring edge suites (705 tests), eslint, oxfmt, tsc (edge, core), check:vocabulary.

Notes

  • The native target's file and image caching keep their previous, stricter rules; aligning them is a possible follow-up.

Review fixes

  • CodeRabbit (static-file.ts, a rule's no-store written over with the year): the year now goes only where the answer still carries one — the file's own, or the year-long rule a build gives its assets; a rule's no-store goes out as private, no-store, and any other lifetime a rule gave is revalidated. Covered by new cases.
  • cubic (static-file.ts, a middleware's no-store lost beside a cookie): a middleware's no-store now stands as a rule's does (private, no-store); such an answer was revalidated before, never held for the year, as it is never shareable.

Not changed

  • cubic (image.ts, caching results whose gated source was fetched with middleware-provided request headers): the headers a gated source is fetched with come from the source's own middleware, run on an anonymous request the edge builds from the source's path alone (localRequest, then rewrittenRequest) — never from the visitor's request or the middleware the visitor's image request ran. They are the same for every request for that source, whose href is in the key, as Next.js's own optimizer fetches an internal source with no request headers and keys its cache on href, width, quality and type.