fix(edge): keep what a middleware let on — optimized images in the cache, hashed files in the browser
SO
19h ago+645+645+012345678901234567890123456789-18-18-01234567890123456789
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/imageunder 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/imageand_next/staticunless 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-Controlwent outprivate, 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, theacceptthe 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 noCache-Controlof 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 keepprivate, max-age=0, must-revalidate; a rule'sno-storestands (private, no-store); a middleware's ownCache-Controlstill stands; validation and private delivery stayno-store. (Next.js gives_next/staticits year only where nothing set aCache-Controlalready, 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 ownCache-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'sno-storewritten 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'sno-storegoes out asprivate, no-store, and any other lifetime a rule gave is revalidated. Covered by new cases. - cubic (
static-file.ts, a middleware'sno-storelost beside a cookie): a middleware'sno-storenow stands as a rule's does (private, no-store); such an answer was revalidated before, never held for the year, as it is nevershareable.
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, thenrewrittenRequest) — 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, whosehrefis 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.