Mirrors Next.js's server-side default-locale path normalisation in
`packages/next/src/server/lib/router-utils/resolve-routes.ts` (lines
~250-263): every request that arrives without a locale prefix is
spliced with `/${defaultLocale}` before any `next.config.js`
redirect / rewrite / header rule (or filesystem route) match runs.
Without this, rules like `source: '/:locale/to-sv'` with
`locale: false` failed to match requests for `/to-sv` because there
was no segment for the `:locale` placeholder. After normalisation the
matcher sees `/en/to-sv` and the rule matches with `:locale=en`.
Per the merged PR #1382's description, the issue text's call for a 308
redirect from `/en/page` to `/page` is incorrect: Next.js serves
the same content under both URLs and normalises the path internally.
This change implements that internal normalisation; no redirect.
The new helper lives next to the existing pages i18n helpers so both
routers can use it. Wired into:
- `prod-server.ts` (Pages Router prod)
- `app-rsc-handler.ts` (App Router dev + prod)
- `index.ts` (Pages Router dev plugin)
- `deploy.ts` (Cloudflare Workers entry template)
The domain-mapped default locale (`i18n.domains[].defaultLocale`) is
preferred over the global default when the inbound host matches,
matching Next.js's `domainLocale.defaultLocale` branch in
resolve-routes.ts. `/_next/*` and `/__vinext/*` paths are excluded
to mirror Next.js's exception for build assets and prerender manifests.
The fallback "retain the original unprefixed source" branch in
`applyLocaleToRoutes` is intentionally kept as belt-and-braces: the
new normalisation pass renders it dead-code for ordinary requests, but
it harmlessly preserves backwards compatibility with any matcher
caller that still passes raw, unnormalised pathnames.
Refs #1336 (item 4)
App Router responses can vary by RSC navigation headers. Route handlers returned custom responses through the outer finalizer without the shared RSC Vary set, so caches could reuse response variants across navigation modes.
The finalizer now merges the shared VINEXT_RSC_VARY_HEADER for non-redirect, non-static-file App Router responses. Existing page helpers that already set it are deduped by the shared Vary merge helper.
Tests cover route handler finalization, custom Vary preservation, duplicate avoidance, wildcard Vary, and static-file signal exclusion.
* 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): extract response finalisation into typed helper
Config header application runs inline in the generated RSC entry handler()
as a template string with no tests. Every response variant (page, route
handler, server action, not-found) goes through handler(), but the shared
finalisation logic cannot be tested or reused — a classic rot factory.
Create server/app-rsc-response-finalizer.ts that owns:
- Skip 3xx redirect responses. Response.redirect() produces immutable Headers
that throw on write; Next.js also deliberately excludes config headers from
redirects.
- Normalize the request pathname (segment-wise percent-decode + collapse)
before matching config header source patterns.
- Strip basePath at a segment boundary before matching. The old inline code
used pathname.startsWith(basePath) which incorrectly matched /app2/page when
basePath is /app; stripBasePath() enforces the separator boundary.
- Apply matching config headers via applyConfigHeadersToResponse, preserving
middleware-set header precedence.
The generated entry now calls __finalizeAppRscResponse(response, request, {...})
in place of ~15 lines of template logic, and drops the now-unused
applyConfigHeadersToResponse and normalizePathnameForRouteMatch imports from
the generated code.
Tests cover: header applied to 200, empty configHeaders no-op, source
pattern miss, immutable 307 no throw, mutable 308 header skipped, basePath
strip, basePath segment-boundary (regression for startsWith bug), nested
basePath, has-condition match, has-condition miss.