[PR #991] [MERGED] fix(app-router): validate RSC cache-busting params #1005

Closed
opened 2026-05-06 13:11:31 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/991
Author: @NathanDrake2406
Created: 4/30/2026
Status: Merged
Merged: 5/4/2026
Merged by: @james-elicx

Base: mainHead: nathan/issue-988-rsc-cache-busting


📝 Commits (5)

  • 56f24bb fix(app-router): validate RSC cache-busting params
  • d53bfeb fix(app-router): hide RSC cache tokens from navigation URLs
  • d699a96 fix(app-router): hide RSC cache token from searchParams
  • cf80770 fix(app-router): address RSC cache-busting review
  • bbdc1d1 fix(app-router): preserve HTML redirect destination for direct .rsc navigations

📊 Changes

31 files changed (+887 additions, -167 deletions)

View changed files

📝 packages/vinext/src/server/app-browser-entry.ts (+38 -22)
📝 packages/vinext/src/server/app-browser-state.ts (+3 -4)
📝 packages/vinext/src/server/app-page-boundary.ts (+2 -1)
📝 packages/vinext/src/server/app-page-cache.ts (+2 -1)
📝 packages/vinext/src/server/app-page-dispatch.ts (+6 -3)
📝 packages/vinext/src/server/app-page-execution.ts (+7 -2)
📝 packages/vinext/src/server/app-page-response.ts (+3 -2)
📝 packages/vinext/src/server/app-page-stream.ts (+2 -1)
packages/vinext/src/server/app-rsc-cache-busting.ts (+218 -0)
📝 packages/vinext/src/server/app-rsc-handler.ts (+20 -1)
📝 packages/vinext/src/server/app-rsc-request-normalization.ts (+5 -5)
📝 packages/vinext/src/server/app-server-action-execution.ts (+3 -2)
📝 packages/vinext/src/server/middleware-response-headers.ts (+33 -0)
📝 packages/vinext/src/shims/link.tsx (+45 -41)
📝 packages/vinext/src/shims/navigation.ts (+47 -28)
📝 packages/vinext/src/shims/next-shims.d.ts (+4 -0)
📝 tests/app-browser-entry.test.ts (+3 -2)
📝 tests/app-page-boundary-render.test.ts (+4 -3)
📝 tests/app-page-execution.test.ts (+30 -3)
📝 tests/app-page-response.test.ts (+43 -2)

...and 11 more files

📄 Description

What this changes

Adds a shared App Router RSC cache-busting helper and wires generated RSC entries to reject malformed RSC requests by redirecting them to the canonical _rsc URL. Client-side RSC fetches now send RSC: 1 plus a cache-busting _rsc search param derived from the request headers that can change the RSC payload.

The shared helper also centralizes the App Router RSC Vary header set and includes both Next-compatible headers and Vinext-specific variant headers:

  • RSC
  • Accept
  • Next-Router-State-Tree
  • Next-Router-Prefetch
  • Next-Router-Segment-Prefetch
  • Next-Url
  • X-Vinext-Interception-Context
  • X-Vinext-Mounted-Slots

Fixes #988.

Why

Next.js now treats the URL as the defensive cache key for RSC requests because some CDNs do not respect Vary. The relevant upstream behavior is:

Vinext uses .rsc URLs rather than the exact same visible URL for HTML and RSC, so the HTML-vs-RSC variant is already separated by pathname. The gap is still real because Vinext's .rsc response can vary by request headers such as mounted slots and interception context. A CDN that keys only by URL could serve the wrong RSC payload for another slot or interception context.

Approach

  • Add server/app-rsc-cache-busting.ts as the normal module that owns request headers, _rsc hashing, canonical redirect validation, and the shared RSC Vary value.
  • Keep entries/app-rsc-entry.ts as app-shape codegen: it imports the helper, computes isRscRequest, and delegates validation.
  • Update browser navigation, link prefetch, router prefetch, initial hydration fetches, HMR fetches, and server-action fetches to build canonical RSC request URLs through the helper.
  • Update App Router response helpers to use the shared Vary value.
  • De-duplicate Vary tokens when merging middleware headers, so middleware can add its own Vary values without duplicating the base App Router set.

Validation

  • vp check
  • vp run knip --no-progress
  • vp test run tests/app-rsc-cache-busting.test.ts tests/app-browser-entry.test.ts tests/prefetch-cache.test.ts tests/entry-templates.test.ts
  • vp test run tests/app-rsc-cache-busting.test.ts tests/app-browser-entry.test.ts tests/app-page-cache.test.ts tests/app-page-response.test.ts tests/app-page-boundary-render.test.ts tests/app-page-stream.test.ts tests/app-server-action-execution.test.ts tests/entry-templates.test.ts
  • vp test run tests/app-router.test.ts -t "RSC"

Risks / follow-ups

This intentionally adapts the Next.js invariant to Vinext's current .rsc URL model instead of switching Vinext to same-URL Flight fetches. The hash includes Next-compatible RSC variant headers for forward compatibility and the Vinext-specific headers that currently affect payload shape.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/991 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/30/2026 **Status:** ✅ Merged **Merged:** 5/4/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/issue-988-rsc-cache-busting` --- ### 📝 Commits (5) - [`56f24bb`](https://github.com/cloudflare/vinext/commit/56f24bbee767725a1a64f3da441063bd993841d8) fix(app-router): validate RSC cache-busting params - [`d53bfeb`](https://github.com/cloudflare/vinext/commit/d53bfeb3d09143b2d2c2f44909d5233868184442) fix(app-router): hide RSC cache tokens from navigation URLs - [`d699a96`](https://github.com/cloudflare/vinext/commit/d699a9698f0e031f162cda49da3fc75c8c2cd044) fix(app-router): hide RSC cache token from searchParams - [`cf80770`](https://github.com/cloudflare/vinext/commit/cf807702c75d67f78fe3f141153183402d2cd09e) fix(app-router): address RSC cache-busting review - [`bbdc1d1`](https://github.com/cloudflare/vinext/commit/bbdc1d1eaa561c80e0c7d017295a83225380fd2c) fix(app-router): preserve HTML redirect destination for direct .rsc navigations ### 📊 Changes **31 files changed** (+887 additions, -167 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/server/app-browser-entry.ts` (+38 -22) 📝 `packages/vinext/src/server/app-browser-state.ts` (+3 -4) 📝 `packages/vinext/src/server/app-page-boundary.ts` (+2 -1) 📝 `packages/vinext/src/server/app-page-cache.ts` (+2 -1) 📝 `packages/vinext/src/server/app-page-dispatch.ts` (+6 -3) 📝 `packages/vinext/src/server/app-page-execution.ts` (+7 -2) 📝 `packages/vinext/src/server/app-page-response.ts` (+3 -2) 📝 `packages/vinext/src/server/app-page-stream.ts` (+2 -1) ➕ `packages/vinext/src/server/app-rsc-cache-busting.ts` (+218 -0) 📝 `packages/vinext/src/server/app-rsc-handler.ts` (+20 -1) 📝 `packages/vinext/src/server/app-rsc-request-normalization.ts` (+5 -5) 📝 `packages/vinext/src/server/app-server-action-execution.ts` (+3 -2) 📝 `packages/vinext/src/server/middleware-response-headers.ts` (+33 -0) 📝 `packages/vinext/src/shims/link.tsx` (+45 -41) 📝 `packages/vinext/src/shims/navigation.ts` (+47 -28) 📝 `packages/vinext/src/shims/next-shims.d.ts` (+4 -0) 📝 `tests/app-browser-entry.test.ts` (+3 -2) 📝 `tests/app-page-boundary-render.test.ts` (+4 -3) 📝 `tests/app-page-execution.test.ts` (+30 -3) 📝 `tests/app-page-response.test.ts` (+43 -2) _...and 11 more files_ </details> ### 📄 Description ## What this changes Adds a shared App Router RSC cache-busting helper and wires generated RSC entries to reject malformed RSC requests by redirecting them to the canonical `_rsc` URL. Client-side RSC fetches now send `RSC: 1` plus a cache-busting `_rsc` search param derived from the request headers that can change the RSC payload. The shared helper also centralizes the App Router RSC `Vary` header set and includes both Next-compatible headers and Vinext-specific variant headers: - `RSC` - `Accept` - `Next-Router-State-Tree` - `Next-Router-Prefetch` - `Next-Router-Segment-Prefetch` - `Next-Url` - `X-Vinext-Interception-Context` - `X-Vinext-Mounted-Slots` Fixes #988. ## Why Next.js now treats the URL as the defensive cache key for RSC requests because some CDNs do not respect `Vary`. The relevant upstream behavior is: - Next sends `RSC: 1` on Flight fetches: https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/fetch-server-response.ts#L147-L155 - Next appends a cache-busting `_rsc` search param before fetching: https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/fetch-server-response.ts#L532-L537 - Next computes a compact SHA-256-derived header hash: https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts#L47-L77 - Next validates the incoming hash and redirects mismatches to the canonical URL: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/base-server.ts#L2051-L2123 - Next includes the RSC variant headers in `Vary`, with `Next-Url` added for interception-sensitive routes: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/base-server.ts#L2003-L2023 and https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/app-page/module.ts#L188-L195 Vinext uses `.rsc` URLs rather than the exact same visible URL for HTML and RSC, so the HTML-vs-RSC variant is already separated by pathname. The gap is still real because Vinext's `.rsc` response can vary by request headers such as mounted slots and interception context. A CDN that keys only by URL could serve the wrong RSC payload for another slot or interception context. ## Approach - Add `server/app-rsc-cache-busting.ts` as the normal module that owns request headers, `_rsc` hashing, canonical redirect validation, and the shared RSC `Vary` value. - Keep `entries/app-rsc-entry.ts` as app-shape codegen: it imports the helper, computes `isRscRequest`, and delegates validation. - Update browser navigation, link prefetch, router prefetch, initial hydration fetches, HMR fetches, and server-action fetches to build canonical RSC request URLs through the helper. - Update App Router response helpers to use the shared `Vary` value. - De-duplicate `Vary` tokens when merging middleware headers, so middleware can add its own Vary values without duplicating the base App Router set. ## Validation - `vp check` - `vp run knip --no-progress` - `vp test run tests/app-rsc-cache-busting.test.ts tests/app-browser-entry.test.ts tests/prefetch-cache.test.ts tests/entry-templates.test.ts` - `vp test run tests/app-rsc-cache-busting.test.ts tests/app-browser-entry.test.ts tests/app-page-cache.test.ts tests/app-page-response.test.ts tests/app-page-boundary-render.test.ts tests/app-page-stream.test.ts tests/app-server-action-execution.test.ts tests/entry-templates.test.ts` - `vp test run tests/app-router.test.ts -t "RSC"` ## Risks / follow-ups This intentionally adapts the Next.js invariant to Vinext's current `.rsc` URL model instead of switching Vinext to same-URL Flight fetches. The hash includes Next-compatible RSC variant headers for forward compatibility and the Vinext-specific headers that currently affect payload shape. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:31 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#1005
No description provided.