Repository navigation
fix(config): match i18n headers() only in their locale-prefixed form - #4088
Conversation
Next.js processRoutes gives header rules only the `/:nextInternalLocale(...)` source and matches them against the default-locale-prefixed pathname. vinext also kept the unprefixed source, so `/((?!api).*)` applied to `/fr/api/x` and default-locale requests matched twice, appending Set-Cookie/Vary twice. Drop the unprefixed copy for headers (every header matcher caller already normalises the pathname) and keep the root's trailing slash when splicing in the default locale under `trailingSlash: true`, like resolve-routes.
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: 82e05c0 |
@vinext/cloudflare
create-vinext-app
@vinext/types
vinext
@cloudflare/workers-response-store
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 7 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
| const defaultLocale = domainLocale?.defaultLocale ?? i18n.defaultLocale; | ||
|
|
||
| if (pathname === "/") return `/${defaultLocale}`; | ||
| if (pathname === "/") return options.trailingSlash ? `/${defaultLocale}/` : `/${defaultLocale}`; |
There was a problem hiding this comment.
Warning: Next.js does not restore this slash from trailingSlash alone. In v16.2.7 resolve-routes.ts, it records whether the original request pathname ended in /, then calls maybeAddTrailingSlash only when that is true and skipProxyUrlNormalize is false. Here every basePath-stripped root becomes /en/ whenever trailingSlash is enabled. Consequently, with skipProxyUrlNormalize: true (or an exact /docs request under basePath: "/docs" plus skipTrailingSlashRedirect: true), a root header transformed to /:nextInternalLocale(...)/ applies in vinext even though Next.js matches /en and skips it. Please thread the original trailing-slash state and normalization gate through these header call sites rather than inferring preservation solely from the config flag.
|
Review: 1 findings. Posted 1 actionable inline comment. |
resolve-routes re-adds the root's trailing slash after splicing in the default locale only when the raw request pathname (basePath included) ended with `/`, trailingSlash is on, and skipProxyUrlNormalize is off. Header matching restored it whenever trailingSlash was set, so `/docs` under basePath "/docs" and skipProxyUrlNormalize requests matched as `/en/` instead of `/en`. Each header call site now computes the flag from its raw pathname with shouldRestoreLocaleRootTrailingSlash; skipProxyUrlNormalize is threaded into the App RSC handler.
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: 91b0fe4 |
| basePath: options.basePath, | ||
| configHeaders: options.configHeaders, | ||
| i18nConfig: options.i18nConfig, | ||
| restoreLocaleRootTrailingSlash: shouldRestoreLocaleRootTrailingSlash( |
There was a problem hiding this comment.
Warning: This derives the flag from the already-normalized request URL. In this hybrid Pages fallback path, lines 3079-3143 rewrite /_next/data/<buildId>/index.json to / before the finalizer runs, so with i18n and trailingSlash: true this returns true even when there is no middleware and applies a root rule as /en/. Next.js only performs the pre-header _next/data normalization in its middleware_next_data branch when middleware matchers exist; without middleware the locale-prefixed root header does not match the raw data URL. Please carry the pre-normalization decision/raw pathname through this path instead of recomputing it from request.url.
|
Since last review: 1 resolved, 0 still open, 1 new. Posted 1 actionable inline comment. |
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: 799d626 |
|
Since last review: 1 resolved, 0 still open, 0 new. |
Summary
With
i18nconfigured,applyLocaleToRoutesgave everyheaders()rule two sources: the/:nextInternalLocale(...)form and an unprefixed fallback copy. Next.js 16.2.7 (load-custom-routes.jsprocessRoutes) emits only the locale-prefixed form. It matches against the request path with the default locale spliced in (resolve-routes.js), so/aboutis matched as/en/about.vinext already splices the default locale into the path at every
matchHeaderscall site (normalizeDefaultLocalePathname), so the fallback could only over-match:/((?!api).*)matched/fr/api/xthrough the fallback, because the lookahead sawfr/api/x. Next.js excludes that path.Set-Cookiewere appended twice.This PR stops emitting the fallback for header rules. Redirects and rewrites are unchanged.
Next.js also re-adds the root's trailing slash after splicing in the locale (
/becomes/en/), but only when the raw request path (basePath included) ended with/,trailingSlashis on andskipProxyUrlNormalizeis off (hadTrailingSlash+maybeAddTrailingSlash). Header call sites now compute that same condition withshouldRestoreLocaleRootTrailingSlash, so root rules keep matching where Next.js applies them now that the fallback no longer covers it. Redirect and rewrite callers don't pass it.Behaviour changes, each matching real Next.js 16.2.7 (
next build --webpack+next startprobe):/fr/api/xand/en/api/xno longer get/((?!api).*)headers./about,/en/aboutand/fr/aboutget them exactly once.trailingSlashoff,/is matched as/en, so/((?!api).*)no longer applies to it. Alocale: false/enrule does./_next/*paths only matchlocale: falserules.Known limitations
These are out of scope for this PR and not changed:
/_next/datarequests match headers against the normalized page path (already the case before this PR), so their root never gets the trailing slash Next.js gives the data-normalized index undertrailingSlashwith middleware.Tests
tests/shims.test.ts("default-locale path normalisation"):matchHeadersparity for lookaheads under every locale;Set-Cookiefor default-locale and domain-locale paths;/,locale: falseand/_next/*cases;trailingSlash: true:/, a basePath root with and without a slash,skipProxyUrlNormalize: true.pages-i18n-public-rewritefixture (tests/e2e/pages-router/i18n-locale-case-config.spec.ts):/((?!api).*)rule with a header and aSet-Cookie;