fix(cache): preserve ISR with request-time middleware - #2803
Conversation
commit: |
|
Performance benchmarksCompared 0 improved · 1 regressed · 5 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
22e8ad2 to
f3fdbed
Compare
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(cache): preserve ISR with request-time middleware
I reviewed all 59 files against the four goals in the PR body (request-scope middleware/config effects, origin-managed ISR for affected paths, hardened adapter registration, broad test coverage). The design is coherent and the test coverage is genuinely strong — I ran the affected suites locally and all pass (cache-adapters-config, pages-request-pipeline, middleware-runtime, app-rsc-*, pages-router, pages-revalidate, cloudflare-cdn-cache, shims, deploy — 2,400+ tests green), and vp check is clean on the core changed source files.
Overall this is a well-executed change. A few observations, none blocking.
Correctness — verified good
finalizeMiddlewareSafePageCacheHeaderRecordSet-Cookie preservation (cache-control.ts:93) is careful and correct. BecauseapplyCdnResponseHeadersonly mutates Cache-Control/CDN headers,Set-Cookiecompares equal (afterValue === before.get(name)) and is retained as its original array from the{ ...headers }spread. The copy-back only overwrites names the CDN policy actually changed. Good.app-rsc-response-finalizer.tsrefactor correctly converts the old early-returnonconfigHeadersAlreadyAppliedinto a skip of config-header application while still reaching the finalapplyOriginManagedPageCacheResponseHeadersreassertion. Previously an already-applied response returned early and never got the safe policy — this is a real fix, not just a refactor.linkHeaderthreading (app-page-cache-finalizer.ts/app-page-render.ts:1266) fixes a latent bug: the ISR write previously readresponse.headers.get("link")after middleware headers merged, so a middleware-modified link header could be persisted. It now uses the page-ownedlinkHeadercomputed pre-merge. Consistent with the PR's request-scoping goal.- On-demand revalidate tag purge (
pages-revalidate.ts:121) fallback tagencodeCacheTag(\N_T${stem || "/"}`)matches the tagging used inpages-page-handler.ts:164,pages-page-data.ts:494,pages-page-response.ts:696`. Consistent. middlewarePathMatchedseeding inrunPagesRequest— seeding from the pathname matcher for auth'd on-demand revalidation (middleware is skipped there), and updating from config header/redirect/rewrite matches before step-4 config redirects are finalized — is correctly ordered. The closure reads theletat call time, so pre-middleware responses see updated state.- Generated
virtual:vinext-cache-adaptersoutput is syntactically valid for the data-only, cdn-only, and both-configured branches (verified by generating it). Indentation is cosmetically off but braces balance.
Non-blocking notes
-
wrapMiddlewareWithBasePathbehavioral change (pages-request-pipeline.ts:230). The old code usedaddBasePathToPathname, a no-op when the pathname already started with basePath (viahasBasePath). The new code unconditionally prepends basePath. This changes the middleware URL for apps whose application pathname legitimately begins with the basePath segment (/docs/docs/...). The comment justifies it (adapter strips exactly one segment) and it's covered bypages-request-pipeline.test.ts:2549and the/docs/docs/*cases inpages-router.test.ts. Flagging only because it's a subtle semantics change buried in a large diff — worth a callout in the changelog/commit body if not already there. -
Per-request cost of registration (
cache-adapters-virtual.ts). Removing the__vinextCacheAdaptersRegisteredidempotency guard meansregisterConfiguredCacheAdaptersnow runs its body on every request instead of returning early after the first. The happy path is cheap (symbol lookup +isConfigured*Activecomparison, no factory re-invocation), so this is fine, but it does add a small constant to the common request path — mild tension with the "keep the common request path lightweight" guidance in AGENTS.md. Acceptable given it's needed for cross-module-graph correctness; just noting the tradeoff was made deliberately. -
CodeQL findings are test-only (
tests/e2e/cloudflare-workers/route-handler-draft-cache.spec.ts). Both flagged lines live in the in-test "edge cache stand-in" HTTP server:- SSRF (L41):
fetch(\${BASE_URL}${cacheKey}`)` — host is a fixed test worker; only the path derives from the request, which is the point of simulating a CDN pass-through. Not a production surface. - Stack-trace exposure (L70):
outgoing.end(error.stack)on a 502 — test harness diagnostics.
Neither is a production concern. If you want to keep CodeQL quiet you could return a generic 502 body and/or assert the host, but I would not block on it.
- SSRF (L41):
-
app-pages-bridge.tsAPI-route ctx change (now passingexecutionContext ?? undefinedinstead ofundefined) is a correctness improvement — it wirescacheAdapterEnvinto the pages API path under the App Router bridge, which previously got no binding env. Just confirm the pages API handler tolerates a non-undefined ctx here (tests pass, so it appears to).
Nice work — the request-scoping model, the "bypassesOriginOnCacheHit" adapter contract, and the fail-safe defaults (assume in-scope on malformed URL / custom adapters default to true) are all the conservative-by-default choices you want for a cache-correctness change.
|
Review posted to PR #2803. SummaryI reviewed all 59 files in PR #2803 ( Verdict: well-executed, no blocking issues. The design (request-scoped middleware/config effects + origin-managed ISR fallback when a CDN adapter would bypass the origin on HITs) is coherent with conservative fail-safe defaults. Key findings: Verified correct
Non-blocking notes I flagged
|
f3fdbed to
a491d32
Compare
a491d32 to
c1e5e15
Compare
|
/bigbonk review for issues |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(cache): preserve ISR with request-time middleware
I re-reviewed the full diff (59 files, 2758/-186), traced the request-scoping model end to end, generated the virtual:vinext-cache-adapters output to check it, and ran the affected suites locally — all green:
cache-adapters-config+cloudflare-cdn-cache(50)pages-request-pipeline+middleware-runtime+app-rsc-handler+app-rsc-response-finalizer(330)pages-router+pages-revalidate+app-page-cache+app-pages-bridge+deploy(771)
vp check is clean on the core changed source (cache-control, pages-request-pipeline, app-rsc-handler, app-rsc-response-finalizer, cdn-cache, cache-adapters-virtual).
Verdict: no blocking issues. The core idea — decide cache-safety at the pathname level (middleware matcher scope + header/cookie-dependent config rules), keep it stable per URL, and fall back to origin-managed ISR whenever a HIT-above-origin adapter would skip request-time composition — is the right model and is applied consistently across App and Pages, dev and prod.
Verified correct
- Read/write symmetry holds.
originManagedPageCacheis derived from pathname scope, not from per-requesthas/missinginputs (seematchesMiddlewarePathname,matchesRequestDependentHeaderSource/Redirect/Rewrite). SoisrGet/isrSet/triggerBackgroundRegenerationall resolvegetCdnCacheStorageAdapter()to the same store for every request to a given URL — no split-brain between edge and origin stores. - Generated adapter module is syntactically valid for the data-only, cdn-only, both, and none branches (generated and eyeballed — braces balance despite the cosmetic indentation drift inside
registerConfiguredCacheAdapters). finalizeMiddlewareSafePageCacheResponsecatch path correctly rebuilds only when headers are immutable (redirects), and bails on status-0 responses that can't be reconstructed.finalizeMiddlewareSafePageCacheHeaderRecordSet-Cookie preservation is correct: the CDN policy only touches Cache-Control/CDN headers, so Set-Cookie compares equal and is retained from the{ ...headers }spread as its original array.app-rsc-response-finalizer.tsnow reachesapplyOriginManagedPageCacheResponseHeaderseven for already-applied config headers (the old earlyreturnskipped the safe policy) — a real fix, not just a refactor.app-pages-bridgectx wiring (executionContext ?? undefinedinto the 4th positional) lands inrenderPage'sctxslot (pages-server-entry.ts:501), which is exactly whatregisterConfiguredCacheAdapters(ctx?.cacheAdapterEnv)+_runWithExecutionContext(ctx, …)consume. Previouslyundefinedstarved the Pages API/render path of binding env. Correct improvement.linkHeaderthreading persists the page-owned pre-merge Link value into ISR instead of the post-middleware-merged one.- On-demand revalidate tag purge falls back to
_N_T_${stem}which matches the existing tagging convention, and only fires forbypassesOriginOnCacheHitadapters. - Failed-registration tracking uses a WeakMap for object envs (GC-safe) and a Map only for rare primitive envs, bounded by the number of configured adapters — no practical memory growth.
Non-blocking notes
wrapMiddlewareWithBasePathsemantics change (pages-request-pipeline.ts): now unconditionally re-prepends basePath instead of the oldaddBasePathToPathnameno-op-when-present. Intentional (the adapter strips exactly one segment) and covered by the/docs/docs/*cases, but it's a subtle behavior change worth a commit-body/changelog callout.- Per-request registration cost (
cache-adapters-virtual.ts): dropping__vinextCacheAdaptersRegisteredmeans the body runs every request. The happy path is cheap (isConfigured*Activesymbol compare, no factory re-invocation), but it's a small constant added to the common path — mild tension with the "keep the hot path lightweight" guidance, accepted deliberately for cross-module-graph correctness. pages-page-handler.ts:615defaultoptions?.originManagedPageCache ?? hasMiddleware: fail-safe (conservative) for directrenderPagecallers that bypass the pipeline, but it means a direct call for an out-of-scope path in a middleware app would still be treated as origin-managed. The pipeline always passes an explicit boolean, so this only affects non-pipeline callers — acceptable, just noting it.- CodeQL findings are test-only. Both are in the in-test edge-cache stand-in (
route-handler-draft-cache.spec.ts). SSRF:upstreamUrlis only ever${BASE_URL}${pathname}via a pre-registered map — host is fixed. Stack-trace: the current code returns a generic 502 body and onlyconsole.errors the error, so that finding looks already addressed in this commit. Not production surfaces.
Nice work — the bypassesOriginOnCacheHit contract with custom adapters defaulting to true, the malformed-URL fail-safe (normalizedPathname instanceof Response → true), and pathname-level scoping are the conservative-by-default choices you want for a cache-correctness change.
|
Review posted to PR #2803. SummaryI reviewed the full diff (59 files, +2758/-186), traced the request-scoping model end to end, verified the generated cache-adapters virtual module is syntactically valid, ran the affected test suites locally (1,151+ tests, all green), and confirmed Verdict: no blocking issues. The design is sound and consistently applied. Key finding that makes the design correctThe whole approach hinges on cache-safety being decided at the pathname level (middleware matcher scope + header/cookie-dependent config rules), not per-request Verified correct
Non-blocking notes flagged
|
Summary
Validation