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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant