Repository navigation
fix(edge): keep what a middleware let on — optimized images in the cache, hashed files in the browser - #441
soleil-colza wants to merge 4 commits into
Conversation
`/_next/image` under a middleware that let the request on read the image cache but never wrote it (`input.middleware === undefined` in the store condition). Every such request — every image at all, for a middleware with no matcher — and every browser revalidation, which is answered 304 only once a full answer has been made, read the source again and paid for another transformation, and a source behind a middleware of its own ran that middleware again. The bytes are everyone's. The source is asked for as an anonymous visitor asks for it: none of the visitor's headers or cookies, nor what the middleware set on the request, travel with it (`fetchImageSource`), and whatever the middleware can change of the result — the parameters it rewrote the request to, the `accept` the format is chosen by — is in the key. What the middleware said to one visitor (its headers, the status it gave a rewrite, the private caching its say calls for) is laid on each answer, a hit as much as a miss, and never on the entry; and it runs ahead of every lookup, so an entry only answers a request it let on to those parameters. The store condition now drops the middleware test, and keeps validation and the entry size limit. A comment left from when the gated `Cache-Control` was a constant of this module is removed: it said a shared cache must not hand on what a middleware had a say on, which describes the client headers, not the edge's own cache. The native target's image cache keeps its own, stricter rule, and its comment no longer says the edge does the same. `test/image-middleware-cache.test.ts` covers a miss kept and the next request answered from it with that request's own middleware headers, status and private caching, a rewritten request kept under the parameters it was rewritten to, and validation neither reading nor writing the cache.
A build-hashed file under a middleware that let the request on, set no `Cache-Control` of its own, and left nothing that made the answer one visitor's (no cookie on it, no rule the request selected by a condition) went out `private, max-age=0, must-revalidate`. Every later view of a page then revalidated each script and style sheet it cannot render without: a round trip, the edge, and a run of the middleware per file, where Next.js has every browser keep such a file for a year. Such a file now goes out `private, max-age=31536000, immutable` (`PRIVATE_IMMUTABLE_CACHE_CONTROL`, beside `GATED_CACHE_CONTROL` in core): still kept by no shared cache, with the shared-cache overrides taken off as before, but kept by the visitor's own browser. The bytes under a hashed name never change and the visitor already holds them; all a revalidation could still do is have the middleware refuse that visitor what it has. Only for the file as itself (`heldAsTheFile`): marked immutable, at the name the request asked for — or kept for it by a rewrite that adds a query or a locale (`ownName`, set where the classified file is served) — and under its own status. A file a rewrite landed another path on is that path's answer, chosen per request, and a status the middleware gave its rewrite is its say on one request; both stay revalidated, as does a file not marked immutable, one with a cookie or a conditioned rule beside it. A middleware's own `Cache-Control` still stands, and a validation or private delivery is still kept by nobody. `test/static-file-middleware-holding.test.ts` covers the year for the file, its 304 and a rewrite that keeps it, and the revalidated, the middleware's own, and the kept-by-nobody cases.
|
Arkor — Preview deployments for arkorlab-upwind Use the checkbox below to choose whether this pull request gets preview deployments.
Checking this builds the latest commit and future pushes. Unchecking stops queued and future builds; a running build can finish and an existing preview stays available. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 35 minutes. View limit details
Walkthrough
Merge Risk:
|
[Medium impact] The changes appear safe to merge, with no new blocking issues found. SummaryThis PR keeps optimized images in the edge cache after middleware lets them through. It also lets browsers keep eligible build-hashed files for a year, while keeping those responses private.
Reviews (3) · Last reviewed commit: "fix(edge): keep a middleware's no-store ..." · Reviewed by Greptile |
There was a problem hiding this comment.
1 issue found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/edge/src/handlers/image.ts">
<violation number="1" location="packages/edge/src/handlers/image.ts:632">
P2: This now caches results whose gated source was fetched with middleware-provided request headers, but those headers are absent from `imageSourceId`/`cacheKey`; a later variant can receive the first request's transformed bytes. Skip caching when the source fetch depends on middleware request headers, or include the effective source identity in the key.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| // Unkept, every request it covers — every image at all, under a middleware with no matcher — and | ||
| // every revalidation of one a browser holds read the source and transformed it again, and ran a | ||
| // gated source's own middleware again. | ||
| if (!edge.validated && bytes.byteLength <= CACHEABLE_RESULT_BYTES) { |
There was a problem hiding this comment.
P2: This now caches results whose gated source was fetched with middleware-provided request headers, but those headers are absent from imageSourceId/cacheKey; a later variant can receive the first request's transformed bytes. Skip caching when the source fetch depends on middleware request headers, or include the effective source identity in the key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/edge/src/handlers/image.ts, line 632:
<comment>This now caches results whose gated source was fetched with middleware-provided request headers, but those headers are absent from `imageSourceId`/`cacheKey`; a later variant can receive the first request's transformed bytes. Skip caching when the source fetch depends on middleware request headers, or include the effective source identity in the key.</comment>
<file context>
@@ -621,13 +617,19 @@ export async function handleImage(input: ImageInput): Promise<Response> {
+ // Unkept, every request it covers — every image at all, under a middleware with no matcher — and
+ // every revalidation of one a browser holds read the source and transformed it again, and ran a
+ // gated source's own middleware again.
+ if (!edge.validated && bytes.byteLength <= CACHEABLE_RESULT_BYTES) {
const entry = new Headers({
'content-type': image.contentType,
</file context>
Under a middleware that let the request on and set no Cache-Control, a build-hashed file went out held for the visitor's year whatever a rule of `next.config` had set on its path: a rule's `no-store`, or a lifetime of its own, was written over with a year-long one (CodeRabbit). Next.js gives a `_next/static` file its year only where nothing set a Cache-Control already (`router-server`), so a rule's stands there. The year now goes only to a file whose answer still carries one — its own, or the year-long rule a build gives its assets. A rule's `no-store` stands, kept from every cache (`private, no-store`, where it used to be revalidated); any other lifetime a rule gave is revalidated, as before the year was given.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Where a cookie or a conditioned rule kept a middleware's own Cache-Control from standing, a file under it went out revalidated (`private, max-age=0, must-revalidate`) even where the middleware had said `no-store`, which the visitor's browser could then keep (cubic). A middleware's `no-store` now stands as a rule's does: `private, no-store`. Not changed: cubic's reading that such a file was held for the year — an answer with a cookie or a conditioned rule is never `shareable`, so it was revalidated, not kept; the fix is for the `no-store` alone.
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.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.tsc(edge, core),check:vocabulary.Notes
Review fixes
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.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
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.