mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #1034] [MERGED] refactor(app-rsc): extract request normalization into server/app-rsc-request-normalization #1038
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#1038
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/1034
Author: @NathanDrake2406
Created: 5/3/2026
Status: ✅ Merged
Merged: 5/3/2026
Merged by: @james-elicx
Base:
main← Head:refactor/app-rsc-request-normalization📝 Commits (8)
01cf0ebrefactor(app-rsc): introduce app-rsc-request-normalization module9b027b5refactor(app-rsc): wire normalizeRscRequest into the entry and callsitesb1a40e2fix(app-rsc): remove orphaned url declaration from _handleRequestb0e5ffffix(test): update cleanPathname assertion for normalizeRscRequest destructuring940de65refactor(app-rsc): extract normalizeMountedSlotsHeader into neutral module and add null-byte testdcc1fdaMerge remote-tracking branch 'upstream/main' into refactor/app-rsc-request-normalizationa257332ci: retrigger CIb197314ci: retrigger CI 2📊 Changes
7 files changed (+502 additions, -65 deletions)
View changed files
📝
packages/vinext/src/entries/app-rsc-entry.ts(+13 -39)📝
packages/vinext/src/server/app-elements.ts(+1 -10)➕
packages/vinext/src/server/app-mounted-slots-header.ts(+19 -0)➕
packages/vinext/src/server/app-rsc-request-normalization.ts(+107 -0)📝
packages/vinext/src/server/isr-cache.ts(+2 -14)📝
tests/app-router.test.ts(+2 -2)➕
tests/app-rsc-request-normalization.test.ts(+358 -0)📄 Description
What this changes
Introduces
server/app-rsc-request-normalization.tsand wires it into the AppRouter RSC entry. The generated entry no longer owns the normalization logic
itself; it calls
normalizeRscRequestand destructures the typed result.Also extracts
normalizeMountedSlotsHeaderinto a neutral module(
server/app-mounted-slots-header.ts) used by all three consumers: requestnormalization (reads the incoming header), app-elements (outgoing header
construction), and isr-cache (RSC cache key generation). The generated RSC
entry no longer calls the cache-layer export directly; request normalization
now owns reading and canonicalising the incoming header.
Why
The normalization pipeline is the most security and compatibility-sensitive
part of the request lifecycle: protocol-relative URL open redirects,
%2F-encoded path segment boundaries, basePath bypass via/__vinext/,RSC content-type detection, and null-byte header injection all live here.
Having this logic inlined in a template string means:
normalizePathcollapses//) by reading a single filenormalizeMountedSlotsHeaderduplicated acrossisr-cache.tsandapp-elements.ts(with the copy inapp-elements.tsalready divergingfrom the canonical
isr-cache.tsversion)Approach
normalizeRscRequest(request, basePath)encodes early exits asResponsereturns (400/404) and success as
NormalizedRscRequest. The discriminant isinstanceof Response, which callers already use for every other early-exitcheck in the entry. No new control-flow patterns introduced.
Step ordering inside the function is documented with numbered comments that
explain the security constraint each ordering decision satisfies:
Step 2 (protocol-relative guard) must precede step 4 (
normalizePath):normalizePathcollapses//evil.comto/evil.com, causing the guardto miss it. Source: vercel/next.js —
server/lib/router-utils/guard-protocol-relative-url.tsStep 3 (strict percent-decode) must precede step 5 (basePath check):
a
%2F-encoded slash in the basePath position would decode to/andcreate a fake match. Source: vercel/next.js —
server/lib/router-utils/decode-path-params.tsStep 5 (
/__vinext/bypass): internal prerender endpoints must bereachable regardless of the configured basePath. These are
/__vinext/prerender/*endpoints consumed bywrangler unstable_startWorkerduring Cloudflare Workers builds.
normalizeMountedSlotsHeadermoves toserver/app-mounted-slots-header.tsandis re-exported from
isr-cache.tsfor backward compatibility. Only theapp-elements.tsprivate copy is removed — the generated entry was alreadyimporting it from
isr-cache.ts.Validation
44 behavior tests in
tests/app-rsc-request-normalization.test.tscover://,/\,/%5C,/%2FnormalizePath(tested explicitly)%GG, truncated%, single-digit%A/__vinext/bypass under a configured basePath%2Fpreservation (not decoded to path separator).rscsuffix andAccept: text/x-componentcleanPathnamestripping395 tests pass across the 6 affected test files (new + isr-cache, app-elements,
request-pipeline, entry-templates, app-router).
Risks / follow-ups
The
app-rsc-entry.tsgenerator now has one fewer resolved path variable(
normalizePathnameForRouteMatchStrictandguardProtocolRelativeUrlnolonger need separate
resolveEntryPathentries). ThenormalizePathModulePathand
routingUtilsPathconstants are retained because__normalizePathand__normalizePathnameForRouteMatchare still used in thehandler()outerfunction for
applyConfigHeadersToResponse.No behavior change. The generated RSC entry produces the same request handling
semantics; only the source of the logic has moved.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.