Skip to content

fix(router): minimize client rewrite manifests - #2734

Merged
james-elicx merged 4 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/minimize-client-rewrite-data
Jul 31, 2026
Merged

fix(router): minimize client rewrite manifests#2734
james-elicx merged 4 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/minimize-client-rewrite-data

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Overview

Goal Publish only rewrite data the browser can evaluate authoritatively
Core change Project resolved rewrites into a client-specific discriminated manifest
Key boundary Only internal destinations with query or host conditions remain client-resolvable
Expected impact Client-safe rewrites keep SPA navigation; every other rule returns to the server

Why

Client routing is correct only when every input to a rewrite decision is authoritative in the browser. Query and host conditions meet that requirement. Cookie conditions may depend on HttpOnly values, arbitrary headers may be added by the server or an intermediary, and missing conditions require complete request context.

The client projection therefore fails closed. Rules with external destinations, missing conditions, cookie or header has conditions, or unknown future condition types omit both the destination and server-only condition data. The source and any safe query or host preconditions remain available so the router can conservatively hand matching navigations back to the server.

What changed

Scenario Before After
Internal rewrite with query or host conditions Full rule serialized and resolved on the client Minimal rule serialized and resolved on the client
Rewrite with cookie or header conditions Condition and destination serialized; browser matching could false-negative Server-only values omitted; matching source returns to the server
External or missing rewrite Server-only data could be present in client output Destination and server-only conditions omitted
_buildManifest.js Used a separate rewrite normalizer Uses the same projection as App and Pages entries
Unknown future condition type No explicit policy Defaults to server evaluation
Maintainer review path
  1. packages/vinext/src/client/client-rewrites.ts defines the allow-list and discriminated client manifest.
  2. packages/vinext/src/build/next-client-runtime-manifests.ts applies that projection to _buildManifest.js.
  3. packages/vinext/src/client/client-rewrite-matcher.ts performs client resolution or server handoff.
  4. tests/client-build-manifest.test.ts verifies the emitted file and serialized projection.
  5. tests/hybrid-client-route-owner.test.ts and tests/shims.test.ts verify App/Link and Pages Router behavior from raw rewrite config.

Validation

  • App and Pages generated entries omit external, missing, cookie, and header canaries.
  • The emitted _buildManifest.js uses the same projection and omits protocol-relative destinations.
  • HttpOnly-cookie and server-added-header rules perform document navigation through both client consumers.
  • Query and host conditions remain client-resolvable; mixed safe and server-only conditions retain only a safe prefilter.
Commands and results
  • vp check: passed across 1,164 checked files.
  • vp test run tests/client-build-manifest.test.ts tests/entry-templates.test.ts tests/hybrid-client-route-owner.test.ts tests/shims.test.ts: 1,372 tests passed.
  • vp run vinext#build: passed.
  • Pre-commit full check, staged unit and integration tests, and knip: passed.

Risk / compatibility

  • Public API: unchanged.
  • Internal rewrites using only query or host conditions retain soft navigation.
  • Rules requiring server-only inputs may perform a full document navigation even when the hidden condition ultimately does not match. This is deliberate because the server is the first authoritative evaluation boundary.
  • The client manifest gains an internal requiresServerEvaluation discriminator; destination-less entries remain compatible with the existing server-handoff behavior.

Browser entries currently serialize resolved rewrite objects verbatim even when the client cannot execute a rule locally. Project rewrites into a client-specific discriminated shape, retaining internal destinations that support SPA routing while handing external and missing-condition rules to the server.

Keep the full matcher lazy on Pages and cover generated manifests and both client navigation ownership paths.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: e784ecf

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared e784ecf against base 25dc2f3 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 134.3 KB 134.4 KB ⚫ +0.1%
Client entry size (gzip) vinext 121.9 KB 121.9 KB ⚫ +0.0%
Dev server cold start vinext 2.91 s 2.87 s ⚫ -1.4%
Production build time vinext 3.16 s 3.18 s ⚫ +0.7%
RSC entry closure size (gzip) vinext 111.3 KB 111.2 KB ⚫ -0.0%
Server bundle size (gzip) vinext 189.0 KB 188.9 KB ⚫ -0.0%

View detailed results and traces

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50582478a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/client/client-rewrites.ts Outdated
NathanDrake2406 and others added 2 commits July 28, 2026 02:24
Cookie and header rewrite conditions can depend on values that browser JavaScript cannot observe. Evaluating them client-side can both publish those values and skip rewrites that the server would match.

Project only query and host conditions, fail closed for every other condition type, and use the same projection for the public build manifest so all client outputs preserve the same boundary.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 816edae7b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the client rewrite projection change. This is a well-designed, correctly fail-closed change and the tests are thorough (canary strings + full manifest equality + both client consumers). No blocking issues found.

Correctness reasoning I verified:

  • The client has prefilter is provably safe. checkHasConditions uses AND semantics (config-matchers.ts:565 — "every condition must match"), so retaining only the host/query subset of has on a requiresServerEvaluation rule can only ever make the client more likely to hand off to the server, never less. If the client-safe subset fails to match, the full rule cannot match, so skipping is safe.
  • matchClientRewrite still checks isExternalUrl(destination) after substitution, so a config-internal destination that becomes protocol-relative via decoded catch-all params still forces a document navigation. Parity with the old inline isExternalUrl(rewritten) check is preserved.
  • All consumers of __VINEXT_CLIENT_REWRITES__ were updated: the sync path in router.ts:1824 correctly folds the old rewrite.missing check into requiresServerEvaluation, and the length-only consumers (router.ts:1611, router.ts:3231) need no field changes.
  • _buildManifest.js now shares the same toClientRewrites projection, resolving the earlier Codex P2 finding about protocol-relative destinations leaking into the publicly served manifest.

A couple of minor, non-blocking observations below.

function toClientRewrite(rewrite: NextRewrite): ClientRewrite {
const clientHas = rewrite.has?.filter(isClientHasCondition);
const hasServerOnlyCondition =
rewrite.has?.some((condition) => !isClientHasCondition(condition)) ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: isClientHasCondition is called twice per condition here — once in the clientHas filter (line 54) and again in this some(...) predicate. Functionally fine, but you could compute both in a single pass to avoid the double classification, e.g. partition into clientHas / server-only in one reduce. Not worth blocking on given rewrite counts are tiny.

};
}

return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the client-resolvable branch re-spells every field (source, has, locale, basePath) instead of spreading common like the server-eval branch above. Spreading ...common and adding destination would keep the two branches symmetric and avoid a future field being added to common but forgotten here:

Suggested change
return {
return {
...common,
destination: rewrite.destination,
};

basePathState,
);
if (destination === null) return null;
if (rewrite.requiresServerEvaluation || isExternalUrl(destination)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming the intent: for a requiresServerEvaluation rule with a client-safe has prefilter, matchRewrite runs the placeholder destination / through param substitution, but since the discriminator short-circuits to { kind: "server" } the substituted value is discarded. That's correct — the destination here is only ever used to detect a match, never surfaced. Worth a one-line comment noting the substituted / is intentionally thrown away for the server-eval case, since the current comment only explains why / is used, not that its output is unused.

@ask-bonk

ask-bonk Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2734.

Summary

I reviewed PR #2734 (fix(router): minimize client rewrite manifests) and posted a batch review with no blocking issues found. This is a well-designed, correctly fail-closed change.

Key correctness properties I verified:

  1. The client has prefilter is provably safe. checkHasConditions uses AND semantics (config-matchers.ts:565). Retaining only the host/query subset of has on a requiresServerEvaluation rule can only make the client more likely to hand off to the server, never less — so no navigation that the server would rewrite gets incorrectly kept on the client.

  2. External-URL parity preserved. matchClientRewrite still checks isExternalUrl(destination) after substitution, so a config-internal destination that becomes protocol-relative via decoded catch-all params still forces a document navigation.

  3. All consumers updated. The sync path (router.ts:1824) correctly folds the old rewrite.missing check into requiresServerEvaluation; length-only consumers need no changes.

  4. _buildManifest.js now shares toClientRewrites, resolving the earlier Codex P2 finding about protocol-relative destinations leaking into the publicly served manifest.

I ran the two most relevant test files locally — all 52 tests pass.

Three minor, non-blocking suggestions posted inline:

  • isClientHasCondition is invoked twice per condition (could be single-pass)
  • The client-resolvable branch re-spells fields instead of spreading common (symmetry/maintenance nit, with suggestion)
  • A comment clarifying that the placeholder / destination's substituted output is intentionally discarded for the server-eval case

github run

@james-elicx
james-elicx merged commit fb9be4b into cloudflare:main Jul 31, 2026
57 checks passed
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.

2 participants