Skip to content

feat(app-router): give shared App page dispatches a query-free cache identity - #3462

Draft
james-elicx wants to merge 3 commits into
isr-query/11-admission-safety-netfrom
isr-query/12-cache-identity
Draft

james-elicx wants to merge 3 commits into
isr-query/11-admission-safety-netfrom
isr-query/12-cache-identity

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Stacked on #3461. First half of plan PR 4 (transport contract).

Problem

A shared response-stage transport had to key App pages by the complete request URL, query included. Next.js keys App page ISR entries without the query, so every query of a static page shares one entry. Keying by the full URL meant one entry per query, which stores more than Next.js.

Change

  • VinextResponseStageDispatchOptions gains cacheIdentity?: { request, props }.
    • The identity drops the user query from the URL and from props.resolvedUrl.
    • It keeps what selects the representation: _rsc, the .rsc suffix and the render mode.
    • The dispatched request and props still carry the real query, so the page renders with it.
  • An adapter opts in with responseStageCacheIdentity: "query-free" on its CdnCacheAdapter. Core honours this only when the adapter also has requiresCompletedResponseAdmission. That admission step (feat(app-router): admit rendered App pages only with a negative searchParams proof #3461) refuses App pages without a negative searchParams proof, and it's what makes the shared key safe.
  • The identity is only set for shared App page GET/HEAD dispatches. It is omitted:
    • under a next.config public cache policy, which Next.js caches per URL;
    • for bypassed, probe and on-demand revalidation requests;
    • for interception and mounted-slot dispatches;
    • for route handlers.
  • The contract wording in multi-stage.ts is updated.
  • No adapter declares the capability in this PR. The Response Store does in the next PR. Workers Cache keeps full-URL keys until plan PR 6.

Tests

tests/app-rsc-handler.test.ts:

  • Where the identity appears, query-free:
    • HTML GET;
    • HEAD, sent as GET;
    • RSC, keeping ?_rsc, the loading-shell _rsc=<hash> and .rsc?_rsc.
  • Where it's absent:
    • without the declaration;
    • with the declaration but without completed-response admission;
    • for draft-mode bypass, probe, POST, a config policy, mounted slots, a real interception match and route handlers.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head c7673ae8e74247921682387f124277d461781e85 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@pkg-pr-new

pkg-pr-new Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@vinext/cloudflare

npm i https://pkg.pr.new/@vinext/cloudflare@3462

create-vinext-app

npm i https://pkg.pr.new/create-vinext-app@3462

@vinext/types

npm i https://pkg.pr.new/@vinext/types@3462

vinext

npm i https://pkg.pr.new/vinext@3462

@cloudflare/workers-response-store

npm i https://pkg.pr.new/@cloudflare/workers-response-store@3462

commit: 4a45af5

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 4a45af5 against base 6f63f80 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 143.1 KB 143.5 KB ⚫ +0.3%
Client entry size (gzip) vinext 131.0 KB 131.5 KB ⚫ +0.5%
Dev server cold start vinext 3.37 s 3.33 s ⚫ -1.2%
Production build time vinext 3.87 s 3.89 s ⚫ +0.3%
RSC entry closure size (gzip) vinext 130.8 KB 130.9 KB ⚫ +0.1%
Server bundle size (gzip) vinext 232.1 KB 232.7 KB ⚫ +0.2%

View detailed results and traces

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

/** Drop every search pair except the internal `_rsc` representation selector. */
export function keepOnlyRscCacheBustingSearchParam(url: URL): void {
const rawQuery = url.search.startsWith("?") ? url.search.slice(1) : url.search;
const pairs = rawQuery.split("&").filter((pair) => isRscCacheBustingSearchPair(pair));

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.

[P2] Canonicalize _rsc before using it in the query-free identity. This keeps every raw matching pair, including on non-RSC HTML requests where _rsc is not a representation selector at all. For example, repeated requests to a static page as /?_rsc=<random UUID> each get a distinct shared key and can be admitted because the page never reads searchParams; valid RSC requests can likewise append ignored duplicate/encoded _rsc pairs after the first validated value. That leaves cache cardinality attacker-controlled and defeats the query-free key. Drop _rsc for non-RSC identities, and retain one canonicalized validated pair for RSC identities.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in the commit "keep only the validated _rsc value in the query-free cache identity" (now 65062f2 after rebasing). HTML identities drop every _rsc pair. RSC identities keep exactly one _rsc: the value resolveInvalidRscCacheBustingRequest validated, read the same way (the first pair) and written back with setRscCacheBustingSearchParam. Duplicate and encoded pairs are ignored. Tests cover an HTML request carrying _rsc, a rewritten RSC request with duplicate and encoded pairs, and a table of helper cases.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Posted 1 actionable inline finding. Targeted tests and vp check pass.

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from c7673ae to 65062f2 Compare September 25, 2026 03:28
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 65062f2db71775a0c468269a035f0145a8b44401 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@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
response-store-demo preview production
workers-cache preview production
kv preview production
static-export preview production
web preview production

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor
  • [P2] Canonicalize RSC-only headers for HTML identities at packages/vinext/src/server/app-worker-stages.ts:139. The identity copies attacker-controlled Next-Url, router-state, and render-mode headers even though HTML ignores them. Opt-in transports key these Vary fields, allowing unbounded cache fragmentation. Strip RSC-only headers when props.isRscRequest is false.

Targeted tests and vp check pass.

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 65062f2 to f363914 Compare September 25, 2026 04:12
@james-elicx

Copy link
Copy Markdown
Member Author

Re the P2 in the last review (RSC-only headers in HTML identities): fixed in f363914. For HTML requests, createSharedAppPageCacheIdentity now drops every VINEXT_RSC_VARY_HEADER field (RSC, the Next-Router-* headers, Next-Url, interception, mounted slots, render mode and state fingerprint). The dispatched request keeps them all. None of them affects a non-RSC render: render mode is fixed to navigation, the interception context is null, the identity is skipped whenever the interception ID or mounted slots are set, and prefetch checks require RSC: 1. The new test in app-rsc-handler.test.ts fails without the fix.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head f3639143b799304ebf9e2142512c05e00657e714 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from f363914 to 9f29f44 Compare September 25, 2026 04:44
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 9f29f44257399a91ad0a99f009d3ddbbf3870cdd without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 9f29f44 to 93f840e Compare September 25, 2026 05:16
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 93f840efdf0e4f35e815939bfa64cdaf3e8428ad without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 93f840e to 14199f8 Compare September 25, 2026 06:05
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 14199f84741779f1eaa56ac23b13a74194e42f76 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 14199f8 to b7f746c Compare September 25, 2026 06:41
@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 1510945 to 8188454 Compare September 25, 2026 14:49
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 8188454ac748a1cf208fcc6c176195f5196a1920 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 8188454 to a9fa00f Compare September 25, 2026 15:31
@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from a9fa00f to 1b99f89 Compare September 25, 2026 18:07
@james-elicx
james-elicx removed this pull request from stack #3471 September 25, 2026 18:50
@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 1b99f89 to 217c83c Compare September 25, 2026 18:51
@james-elicx
james-elicx added this pull request to stack #3474 September 25, 2026 18:51
@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 217c83c to 7b5cd37 Compare September 25, 2026 19:18
@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch 2 times, most recently from 0ac6064 to 844a5b2 Compare September 25, 2026 20:46
@james-elicx
james-elicx removed this pull request from stack #3474 September 25, 2026 21:30
@james-elicx
james-elicx force-pushed the isr-query/12-cache-identity branch from 844a5b2 to ef1f5a2 Compare September 25, 2026 21:31
@james-elicx
james-elicx added this pull request to stack #3481 September 25, 2026 21:32
…identity

Adapters that declare responseStageCacheIdentity: "query-free" behind
completed-response admission receive options.cacheIdentity for shared App
page GET/HEAD dispatches. The identity drops the user query from the URL and
resolvedUrl, keeping _rsc, the .rsc suffix and the render mode, while the
dispatched request keeps the real query for the render. It is omitted under
a next.config public cache policy and for bypassed, interception and
mounted-slot dispatches.
… cache identity

HTML identities drop _rsc, which selects no representation there. RSC
identities keep the one value the cache-busting check validated, re-serialized,
so duplicate or encoded _rsc pairs can't grow the shared key space.
HTML dispatch fixes its render mode to navigation and reads no other RSC
selector outside the props, so an HTML identity no longer carries the Vary
fields transports key, such as Next-Url or router state.
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