Skip to content

fix(app-router): send the request's params and path on core RSC cache hits - #3460

Draft
james-elicx wants to merge 6 commits into
isr-query/08-regen-failure-backofffrom
isr-query/09-rsc-hit-headers
Draft

james-elicx wants to merge 6 commits into
isr-query/08-regen-failure-backofffrom
isr-query/09-rsc-hit-headers

Conversation

@james-elicx

@james-elicx james-elicx commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3459. Second part of plan PR 3: RSC hits on the core cache path.

Problem

The browser reads route params from X-Vinext-Params (falling back to {}) and the rendered path from X-Vinext-Rendered-Path-And-Search. A fresh RSC render sets both, and the Response Store and Workers Cache paths compose them at the request stage. But an RSC response served from core's cache (memory/KV) had neither. A soft navigation served from a core HIT or STALE entry therefore lost its dynamic params, and lost the rewritten path and query.

Change

buildAppPageCachedResponse now composes both headers on RSC HIT and STALE responses from the current request's params and rendered path and search. They aren't read from the stored entry, which every query shares. X-Vinext-Params is only sent when there are params, as on a fresh render. HTML responses are unchanged.

Tests

  • tests/app-page-cache.test.ts: HIT and STALE RSC responses carry both headers, HTML doesn't, and empty params omit X-Vinext-Params.
  • tests/app-page-dispatch.test.ts: a cached RSC hit sends the request's params and path. It fails without the change.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 297ad57f113cef38da48b62429f196facc77038a 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@3460

create-vinext-app

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

@vinext/types

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

vinext

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

@cloudflare/workers-response-store

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

commit: 3da2dd7

middlewareHeaders: options.middlewareContext.headers,
middlewareStatus: options.middlewareContext.status,
mountedSlotsHeader: options.mountedSlotsHeader,
params: options.params,

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.

Use the resolved navigation params here rather than the raw matched-route params. Fresh RSC responses pass navigationParams to buildAppPageRscResponse (app-page-render.ts:1090); those include active parallel-slot and interception params added by resolveAppPageNavigationParams. This cache-hit return happens before that resolution and now emits only options.params, so useParams() loses slot-local/intercepted params specifically on core HIT/STALE responses. Resolve the equivalent navigation params before serving the cache entry, or defer composing this header until they are known.

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 fff0257. A hit now resolves the same render target that the entry's regeneration uses: resolveAppPageInterceptionRerenderTarget, then resolveAppPageNavigationParams, which now sit in one shared helper. So it sends the navigation params with active slot and interception params, as a fresh render does. The dispatch test now uses a route with an active slot that contributes a param, and it fails without the fix.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Posted 1 actionable inline finding. Focused tests pass.

github run

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 3da2dd7 against base 75475a4 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.4 KB ⚫ +0.2%
Client entry size (gzip) vinext 131.0 KB 131.5 KB ⚫ +0.4%
Dev server cold start vinext 3.38 s 3.37 s ⚫ -0.5%
Production build time vinext 3.93 s 3.96 s ⚫ +0.8%
RSC entry closure size (gzip) vinext 130.7 KB 130.7 KB ⚫ +0.0%
Server bundle size (gzip) vinext 231.9 KB 232.4 KB ⚫ +0.2%

View detailed results and traces

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

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

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

// the slot param merge. resolveAppPageNavigationParams preserves all
// base keys and overlays active slot params on top; when narrowed
// params were used here, non-slot extras were silently dropped.
const mergedNavigationParams = resolveAppPageNavigationParams(

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.

Hydrate revalidationTarget.route before resolving these navigation params. For a source-route interception, getSourceRoute() returns the lazy route table entry; its non-intercepted parallel slots still have page/default unset until ensureRouteLoaded runs. resolveAppPageNavigationParams therefore skips those active slots and a core HIT returns an incomplete X-Vinext-Params header. The existing ensureRouteLoaded in renderFreshPageForCache runs only after this helper and is never reached on a HIT. Moving that await into this helper before this call preserves the same slot params as the fresh intercept path.

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 3e1ad4f. The shared render-target helper now awaits ensureRouteLoaded(revalidationTarget.route) before resolving navigation params, so a hit sees the source route's lazy slots. The regeneration path reuses it. The new dispatch test "hydrates an intercepting source route before resolving a cached RSC hit's params" fails without the fix.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Posted 1 actionable inline finding. Focused tests and checks pass.

github run

@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

@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from fff0257 to 3e1ad4f Compare September 25, 2026 04:12
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 3e1ad4f56b282f398bd4beebf0427da1c5cd72b9 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/09-rsc-hit-headers branch from 3e1ad4f to 02276d2 Compare September 25, 2026 04:44
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

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

middlewareStatus: options.middlewareContext.status,
mountedSlotsHeader: options.mountedSlotsHeader,
params: options.isRscRequest
? (await resolveCacheRenderTarget()).navigationParams

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.

The resolved navigation params set here are overwritten in split response-stage deployments. After this response returns, app-rsc-handler.ts:1183-1195 unconditionally rebuilds X-Vinext-Params from props.params, which contains only the matched route params. Active-slot and source-interception params added by resolveCacheRenderTarget() are therefore still lost on the default transported cache path. Please pass the resolved params through the response-stage envelope or avoid replacing this header with the raw params.

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.

This one is outside this PR. In split response-stage deployments, the request stage has rebuilt X-Vinext-Params from props.params (matched-route params only) since 2978637 on main (2026-09-07). That applies to fresh renders as much as to hits, so active-slot and interception params already fail to reach the client there. This PR fixes the core path, where the hit is composed in dispatch. Carrying navigation params through the response-stage envelope would change that transport contract, so I've noted it as a follow-up rather than widening this PR.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Posted 1 actionable inline finding. Focused tests and checks pass.

github run

@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from 02276d2 to 904e1ce Compare September 25, 2026 05:16
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

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

middlewareStatus: options.middlewareContext.status,
mountedSlotsHeader: options.mountedSlotsHeader,
params: options.isRscRequest
? (await resolveCacheRenderTarget()).navigationParams

@ask-bonk ask-bonk Bot Sep 25, 2026 •

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.

Resolve this target only after the cache helper has confirmed a serveable HIT/STALE entry. Because this argument is awaited before readAppPageCacheResponse starts, every cache-eligible RSC request now runs interception loading and ensureRouteLoaded before the cache lookup and, importantly, before validateAppPageDynamicParams below. An invalid generateStaticParams request that should return the route’s 404 can therefore evaluate a lazy intercepted source module first (and become a 500 if that import throws). Please make params resolution lazy at the point a cached response is actually built, or otherwise defer source-route hydration until a usable entry is known.

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 652def2. readAppPageCacheResponse now takes resolveParams and calls it only while building a HIT/STALE response for an RSC entry with rscData. A miss reaches validateAppPageDynamicParams without loading the intercepted source.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Posted 1 actionable inline finding. Focused tests and checks pass.

github run

@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from 904e1ce to 652def2 Compare September 25, 2026 06:05
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 652def280dd3ec925dbf4d4f0fa824a571c11a91 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/09-rsc-hit-headers branch from 38ca6a6 to eca6016 Compare September 25, 2026 15:31
@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from eca6016 to 43927a7 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/09-rsc-hit-headers branch from 43927a7 to ffc6f09 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/09-rsc-hit-headers branch 2 times, most recently from 26830ad to a94d8c6 Compare September 25, 2026 20:04
@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from a94d8c6 to 0000e2a 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/09-rsc-hit-headers branch from 0000e2a to 8a724cb Compare September 25, 2026 21:31
@james-elicx
james-elicx added this pull request to stack #3481 September 25, 2026 21:32
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 8a724cb124295a660160baac9c5cc3e8f2778fd4 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

… hits

A core RSC cache hit sent neither X-Vinext-Params nor
X-Vinext-Rendered-Path-And-Search. The browser reads the params for
useParams() after a navigation and falls back to {} when the header is
missing, and the prefetch cache aliases entries by the rendered path. A
fresh render sends both, and the response-stage transport composes them
above the adapter for its hits.

Hits and stale hits now compose both from the current request, since they
describe the request rather than the shared RSC bytes.
A cache hit sent the matched route's params, while a fresh render sends the
navigation params, which add active parallel-slot and interception params.
Hits now resolve the same render target the entry's regeneration uses.
…g hit params

A source route's slots are lazy until the route is loaded, so a cache hit
for a source-route interception skipped its active slots' params.
…ntry

Resolving a hit's navigation params loads interception source routes, and
it ran before the cache lookup and the generated-params check. A request
outside generateStaticParams could load route modules before its 404.
The cache helper now resolves them only when building a HIT or STALE
RSC response.
…ilure

Resolving a cached RSC response's params can load interception and source
route modules. Its failure was caught as an ISR read error and turned into a
MISS, so the fresh-render path retried the load and the current request never
observed the failure. Rethrow it past the cache read's catch on HIT and STALE.
@james-elicx
james-elicx force-pushed the isr-query/09-rsc-hit-headers branch from 8a724cb to 3da2dd7 Compare September 25, 2026 21:56
@james-elicx
james-elicx removed this pull request from stack #3481 September 25, 2026 22:13
@james-elicx
james-elicx added this pull request to stack #3482 September 25, 2026 22:14
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