Skip to content

feat(cache): classify and warm static Route Handlers - #3113

Open
james-elicx wants to merge 9 commits into
codex/cacheability-pages-routerfrom
codex/cacheability-route-handlers
Open

feat(cache): classify and warm static Route Handlers#3113
james-elicx wants to merge 9 commits into
codex/cacheability-pages-routerfrom
codex/cacheability-route-handlers

Conversation

@james-elicx

@james-elicx james-elicx commented Aug 27, 2026

Copy link
Copy Markdown
Member

Capability stack 9/9. Exact head: 85ab9571f5ac14604bb5d4d4f67b14983a1fba8f. Base: #3098.

Full chain: #3108#3090#3091#3092#3103#3093#3094#3098#3113.

This is the cumulative capability head. It completes two-stage cacheability probing, embedded-manifest admission, and final CDN warming for statically eligible App Router Route Handlers.

What this adds

  • match Next.js Route Handler static eligibility for dynamic = "force-static", dynamic = "error", positive or false revalidate, and dynamic routes with generateStaticParams
  • invoke Route Handler generateStaticParams during staged Worker discovery and produce concrete request identities
  • probe eligible handlers only after the response completes, so late dynamic API reads and stream failures veto caching
  • represent Route Handlers explicitly in the embedded manifest and require an exact pathname + query match for admission
  • use canonical fetch semantics (Accept: */*) for probing, readiness, and final warming
  • carry Route Handler identities through discovery, probing, artifact generation, readiness, warming, and targeted retries
  • preserve explicit public application cache policy only after completed-response admission proves the request safe
  • match Next.js when a dynamic request read and a handler-owned public policy coexist: the deliberate public policy survives only after bounded clean completion, while the same dynamic read without an explicit policy remains private
  • allow manifest-absent mixed-method handlers to opt into bounded runtime admission only through a handler-owned or unconditional-config public policy; framework-generated revalidate policy alone cannot bypass manifest absence
  • keep mixed-method handlers out of the ordinary ISR/KV read, write, and framework response-policy path, matching Next.js while preserving their handler-owned policies
  • cache direct Route Handler module classification during ownership discovery, avoiding repeated per-path parsing
  • recognize aliased GET and generateStaticParams exports, using exported names rather than local bindings
  • reject GET handlers that also export POST, PUT, DELETE, PATCH, or OPTIONS, matching Next.js static-prerender eligibility
  • fail closed for value-bearing export * declarations without adding a module-graph walk; type-only export stars remain harmless
  • record trusted inner body completion and outer-capture only final public responses that still lack completion proof
  • prevent exact manifest status mismatches from falling through to runtime opt-in and cache route-pattern membership once while parsing the manifest, rather than scanning it on requests
  • fail closed on late stream errors before public policy survives, with built-workerd coverage
  • classify Route Handler probe responses with Set-Cookie or unsupported Vary fields as dynamic, aligning probe output with final admission vetoes
  • treat handler-owned Cache-Control, CDN-Cache-Control, and Cloudflare-CDN-Cache-Control consistently, so framework revalidation cannot overwrite an explicit provider-private policy or write it to ISR

Warming behavior

The default flow sends exactly one final cache-fill request for each admitted Route Handler identity. It does not perform a second fill or certification request.

Only --warm-cdn-certify enables the optional header-only re-request introduced by #3094. This keeps the stricter deployment check available without doubling normal warm traffic.

Safety and Next.js parity

Static source configuration makes a Route Handler eligible for probing; it does not certify the request by itself. Runtime dynamic API usage, middleware or conditional routing participation, incomplete bodies, late errors, and private response policy remain fail-closed. A Route Handler omitted because Next.js would not statically generate it can enter bounded runtime admission only when the handler itself or unconditional next.config headers explicitly supply a public policy. Framework-generated revalidate policy does not qualify. For a route pattern represented in the manifest, unlisted pathname/query identities remain private even when the handler owns a public policy.

For parameterized handlers, only concrete paths returned by generateStaticParams are admitted and warmed. Other paths remain private rather than relying on a route-pattern-wide assertion.

Cloudflare Cache Rules must preserve the full query string for these exact identities. Query-insensitive or query-normalizing cache-key rules are unsupported because they can make distinct manifest identities share one edge object; this requirement is documented in the root, adapter, package, and workers-cache example documentation.

Boundaries

  • the manifest is an ESM module asset embedded in the second Worker upload, not a binding and never a response-body store
  • this stack does not add Vary: Cookie, change Worker entrypoints, or implement the separate uncached preview/draft gateway
  • upgrading from a vulnerable release requires purging/invalidation of objects admitted by the old Worker before relying on the new admission rules

Review guide

  1. app-route-handler-dispatch.ts derives and memoizes Next.js-compatible static eligibility and wires generateStaticParams.
  2. prerender-paths.ts discovers concrete Route Handler identities from the staged Worker.
  3. app-route-handler-execution.ts enforces completed-response handling before public policy can survive.
  4. cacheability-manifest.ts and cacheability-request.ts enforce exact Route Handler identity admission.
  5. cacheability-probe.ts, cdn-warm.ts, and deploy.ts carry Route Handler identities through both deployment stages.
  6. The PPR and Cloudflare Workers Playwright suites exercise built-workerd probing, exact admission, explicit mixed-method opt-in, framework-policy rejection, middleware vetoes, draft behavior, and Pages-only fallback behavior.

Review size

Layer-only diff against #3098: 38 files, +1,396/-83.

Validation

Current exact head:

  • cumulative changed-file suites — 2,864/2,864
  • focused Route Handler admission suite — 39/39
  • PPR probe/admission/Pages built-workerd E2E — 8/8
  • vp check
  • git diff --check
  • exact-head CI and deploy previews are green
@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@3113
npm i https://pkg.pr.new/create-vinext-app@3113
npm i https://pkg.pr.new/@vinext/types@3113
npm i https://pkg.pr.new/vinext@3113

commit: 85ab957

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 85ab957 against base b40e1a9 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 142.3 KB 142.3 KB ⚫ +0.0%
Client entry size (gzip) vinext 129.6 KB 129.6 KB ⚫ +0.0%
Dev server cold start vinext 3.19 s 3.17 s ⚫ -0.5%
Production build time vinext 3.60 s 3.61 s ⚫ +0.2%
RSC entry closure size (gzip) vinext 119.0 KB 119.1 KB ⚫ +0.1%
Server bundle size (gzip) vinext 202.3 KB 202.6 KB ⚫ +0.1%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx
james-elicx force-pushed the codex/cacheability-route-handlers branch from 4d106e8 to 8bd2cbe Compare August 27, 2026 00:50
@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
static-export preview production
web preview production
@james-elicx
james-elicx force-pushed the codex/cacheability-route-handlers branch 2 times, most recently from c5c2079 to e77095a Compare August 27, 2026 00:58
@james-elicx
james-elicx force-pushed the codex/cacheability-route-handlers branch 2 times, most recently from c8d2632 to 52db9f5 Compare August 27, 2026 01:24
@james-elicx
james-elicx force-pushed the codex/cacheability-route-handlers branch from 52db9f5 to 796082a Compare August 27, 2026 01:41
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/cacheability-route-handlers branch from 796082a to 1246114 Compare August 27, 2026 01:50
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant