2 Commits

Author SHA1 Message Date
Nathan Nguyen 9d5d1ce50d fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params

App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request.

Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions.

Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration.

* fix(app-router): hide RSC cache tokens from navigation URLs

* fix(app-router): hide RSC cache token from searchParams

* fix(app-router): address RSC cache-busting review

* fix(app-router): preserve HTML redirect destination for direct .rsc navigations

Config redirects from next.config.js were unconditionally converted to
.rsc destinations for any request whose URL ended in .rsc. This broke
direct browser navigations to .rsc URLs (e.g. a bookmark or external
link) because the browser was redirected to an RSC payload it could not
render.

Client-side RSC fetches always send the RSC: 1 header, so restrict the
.rsc destination conversion to requests that carry that header. Direct
browser hits without the header continue to redirect to the canonical
HTML URL, matching the pre-PR behaviour for config redirects.
2026-05-04 18:55:43 +01:00
Nathan Nguyen fb342f5ed6 refactor(app-rsc): move request lifecycle into typed handler (#1036)
* refactor(app-rsc): introduce app-rsc-request-normalization module

The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.

Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.

The ordering within the function is security-critical and documented step by
step:
  1. Parse URL
  2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
  3. Strict segment-wise percent-decode (malformed encoding -> 400)
  4. normalizePath (collapse //, resolve . and .. segments)
  5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
  6/7. RSC detection and cleanPathname
  8. Sanitize X-Vinext-Interception-Context (strip null bytes)
  9. Normalize x-vinext-mounted-slots (dedup, sort)

Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.

43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.

* refactor(app-rsc): wire normalizeRscRequest into the entry and callsites

Replace the open-coded normalization block in the generated _handleRequest
function with a single normalizeRscRequest call. The entry now delegates
all security-sensitive preprocessing to the typed module rather than
repeating it in a template string.

Remove the local normalizeMountedSlotsHeader implementation from isr-cache.ts
(was a standalone export) and remove the private copy from app-elements.ts.
Both now import from app-rsc-request-normalization.ts. isr-cache.ts re-exports
the function so existing callers that import from there are unaffected.

Generated entry changes:
- Add import for normalizeRscRequest from the new module
- Remove guardProtocolRelativeUrl and stripBasePath from requestPipeline import
- Remove normalizePathnameForRouteMatchStrict from routingUtils import
- Remove normalizeMountedSlotsHeader from isrCache import
- Replace ~20-line normalization block with 4-line call + destructure

* refactor(app-rsc): move request lifecycle into typed handler

The generated App Router RSC entry still owned request normalization, config routing, middleware handoff, public-file serving, action dispatch, fallback routing, and response finalization. That made lifecycle behavior hard to test directly and forced generator assertions to reach into runtime sequencing.

The violated invariant was that codegen should describe the app shape while normal modules should implement behavior. This extracts the request lifecycle into createAppRscHandler, leaves the generated entry responsible for route-specific manifests and dispatch closures, and adds behavior tests for redirects, rewrites, public files, actions, route handlers, and fallback cleanup.
2026-05-03 19:32:45 +01:00