Skip to content

Commit

Permalink
[next]: ensure defaultLocale redirect doesn't conflict with user redi…
Browse files Browse the repository at this point in the history
…rects (#12916)

When `localeDetection` is turned off, usually the user intends to handle
redirects themselves, or doesn't want them at all. However because of
the rewrites that we insert for `defaultLocale` handling, it can clobber
the user-provided redirects. eg the rewrite to `/${defaultLocale}` would
then flow through to the user's redirect handling to result in
`/${defaultLocale}/${redirectLocale}` which wouldn't have been the
intention.

When `localeDetection` is turned off, we shouldn't only attempt the
defaultLocale rewrite handling when we've already processed all other
routes (ie in the `handle: miss` case)

Closes NEXT-3975

---------

Co-authored-by: JJ Kasper <[email protected]>
  • Loading branch information
ztanner and ijjk authored Jan 28, 2025
1 parent ea836e1 commit f65ea22
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-seas-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/next': patch
---

ensure defaultLocale rewrite doesn't conflict with user-defined redirects
93 changes: 62 additions & 31 deletions packages/next/src/server-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1941,39 +1941,37 @@ export async function serverBuild({
},
continue: true,
},
{
src: `^${path.posix.join('/', entryDirectory)}$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}`,
continue: true,
},
// Auto-prefix non-locale path with default locale
// note for prerendered pages this will cause
// x-now-route-matches to contain the path minus the locale
// e.g. for /de/posts/[slug] x-now-route-matches would have
// 1=posts%2Fpost-1
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/'
)}(?!(?:_next/.*|${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(?:/.*|$))(.*)$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}/$1`,
continue: true,
},
]
: []),

{
src: `^${path.posix.join('/', entryDirectory)}$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}`,
continue: true,
},

// Auto-prefix non-locale path with default locale
// note for prerendered pages this will cause
// x-now-route-matches to contain the path minus the locale
// e.g. for /de/posts/[slug] x-now-route-matches would have
// 1=posts%2Fpost-1
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/'
)}(?!(?:_next/.*|${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(?:/.*|$))(.*)$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}/$1`,
continue: true,
},
]
: []),

Expand Down Expand Up @@ -2256,6 +2254,39 @@ export async function serverBuild({
// to allow checking non-prefixed lambda outputs
...(i18n
? [
...(i18n.localeDetection === false
? [
{
src: `^${path.posix.join('/', entryDirectory)}$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}`,
check: true,
},
// Auto-prefix non-locale path with default locale
// note for prerendered pages this will cause
// x-now-route-matches to contain the path minus the locale
// e.g. for /de/posts/[slug] x-now-route-matches would have
// 1=posts%2Fpost-1
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/'
)}(?!(?:_next/.*|${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(?:/.*|$))(.*)$`,
dest: `${path.posix.join(
'/',
entryDirectory,
i18n.defaultLocale
)}/$1`,
check: true,
},
]
: []),
{
src: path.posix.join(
'/',
Expand Down
29 changes: 23 additions & 6 deletions packages/next/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,14 @@ export function localizeDynamicRoutes(
isCorrectLocaleAPIRoutes?: boolean,
inversedAppPathRoutesManifest?: Record<string, string>
): RouteWithSrc[] {
return dynamicRoutes.map((route: RouteWithSrc) => {
// i18n is already handled for middleware
if (route.middleware !== undefined || route.middlewarePath !== undefined)
return route;
const finalDynamicRoutes: RouteWithSrc[] = [];

for (const route of dynamicRoutes as RouteWithSrc[]) {
// i18n is already handled for middleware
if (route.middleware !== undefined || route.middlewarePath !== undefined) {
finalDynamicRoutes.push(route);
continue;
}
const { i18n } = routesManifest || {};

if (i18n) {
Expand All @@ -577,6 +580,18 @@ export function localizeDynamicRoutes(
const isLocalePrefixed =
isFallback || isBlocking || isAutoExport || isServerMode;

// when locale detection is disabled we don't add the default locale
// to the path while resolving routes so we need to be able to match
// without it being present
if (isLocalePrefixed && routesManifest?.i18n?.localeDetection === false) {
const nonLocalePrefixedRoute = JSON.parse(JSON.stringify(route));
nonLocalePrefixedRoute.src = nonLocalePrefixedRoute.src.replace(
'^',
`^${dynamicPrefix || ''}[/]?`
);
finalDynamicRoutes.push(nonLocalePrefixedRoute);
}

route.src = route.src.replace(
'^',
`^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${
Expand All @@ -599,8 +614,10 @@ export function localizeDynamicRoutes(
} else {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
}
return route;
});
finalDynamicRoutes.push(route);
}

return finalDynamicRoutes;
}

type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@ module.exports = {
generateBuildId() {
return 'testing-build-id';
},
async redirects() {
return [
{
source: '/',
has: [
{
type: 'header',
key: 'x-redirect-me',
},
],
destination: '/fr-BE',
permanent: false,
locale: false,
},
{
source: '/:path+',
has: [
{
type: 'header',
key: 'x-redirect-me',
},
],
destination: '/fr-BE/:path+',
permanent: false,
locale: false,
},
];
},
i18n: {
localeDetection: false,
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,19 @@
"path": "/_next/data/testing-build-id/fr/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
},
{
"path": "/",
"status": 307,
"headers": {
"x-redirect-me": "1"
},
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"location": "//fr-BE/"
}
}
]
}

0 comments on commit f65ea22

Please sign in to comment.