Migrate locale redirect handling to router-server (#62606)
This moves the locale redirect handling out of `base-server` as it shouldn't be handled here and should be at the routing level. This avoids the duplicate handling with middleware that causes the incorrect detection/infinite looping. Test case from separate PR was carried over to prevent regression. Fixes: https://github.com/vercel/next.js/issues/55648 Closes: https://github.com/vercel/next.js/pull/62435 Closes: NEXT-2627 Closes: NEXT-2628 --------- Co-authored-by: Nourman Hajar <nourmanhajar@gmail.com> Co-authored-by: samcx <sam@vercel.com>
This commit is contained in:
parent
69d1edf6d0
commit
26de5ca269
7 changed files with 174 additions and 31 deletions
|
@ -50,7 +50,6 @@ import {
|
||||||
PAGES_MANIFEST,
|
PAGES_MANIFEST,
|
||||||
STATIC_STATUS_PAGES,
|
STATIC_STATUS_PAGES,
|
||||||
} from '../shared/lib/constants'
|
} from '../shared/lib/constants'
|
||||||
import { RedirectStatusCode } from '../client/components/redirect-status-code'
|
|
||||||
import { isDynamicRoute } from '../shared/lib/router/utils'
|
import { isDynamicRoute } from '../shared/lib/router/utils'
|
||||||
import { checkIsOnDemandRevalidate } from './api-utils'
|
import { checkIsOnDemandRevalidate } from './api-utils'
|
||||||
import { setConfig } from '../shared/lib/runtime-config.external'
|
import { setConfig } from '../shared/lib/runtime-config.external'
|
||||||
|
@ -1167,36 +1166,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
// Edge runtime always has minimal mode enabled.
|
|
||||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
|
||||||
!this.minimalMode &&
|
|
||||||
defaultLocale
|
|
||||||
) {
|
|
||||||
const { getLocaleRedirect } =
|
|
||||||
require('../shared/lib/i18n/get-locale-redirect') as typeof import('../shared/lib/i18n/get-locale-redirect')
|
|
||||||
const redirect = getLocaleRedirect({
|
|
||||||
defaultLocale,
|
|
||||||
domainLocale,
|
|
||||||
headers: req.headers,
|
|
||||||
nextConfig: this.nextConfig,
|
|
||||||
pathLocale: pathnameInfo.locale,
|
|
||||||
urlParsed: {
|
|
||||||
...url,
|
|
||||||
pathname: pathnameInfo.locale
|
|
||||||
? `/${pathnameInfo.locale}${url.pathname}`
|
|
||||||
: url.pathname,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirect) {
|
|
||||||
return res
|
|
||||||
.redirect(redirect, RedirectStatusCode.TemporaryRedirect)
|
|
||||||
.body(redirect)
|
|
||||||
.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addRequestMeta(req, 'isLocaleDomain', Boolean(domainLocale))
|
addRequestMeta(req, 'isLocaleDomain', Boolean(domainLocale))
|
||||||
|
|
||||||
if (pathnameInfo.locale) {
|
if (pathnameInfo.locale) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import setupCompression from 'next/dist/compiled/compression'
|
||||||
import { NoFallbackError } from '../base-server'
|
import { NoFallbackError } from '../base-server'
|
||||||
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
|
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
|
||||||
import { isPostpone } from './router-utils/is-postpone'
|
import { isPostpone } from './router-utils/is-postpone'
|
||||||
|
import { parseUrl as parseUrlUtil } from '../../shared/lib/router/utils/parse-url'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PHASE_PRODUCTION_SERVER,
|
PHASE_PRODUCTION_SERVER,
|
||||||
|
@ -36,6 +37,9 @@ import { RedirectStatusCode } from '../../client/components/redirect-status-code
|
||||||
import { DevBundlerService } from './dev-bundler-service'
|
import { DevBundlerService } from './dev-bundler-service'
|
||||||
import { type Span, trace } from '../../trace'
|
import { type Span, trace } from '../../trace'
|
||||||
import { ensureLeadingSlash } from '../../shared/lib/page-path/ensure-leading-slash'
|
import { ensureLeadingSlash } from '../../shared/lib/page-path/ensure-leading-slash'
|
||||||
|
import { getNextPathnameInfo } from '../../shared/lib/router/utils/get-next-pathname-info'
|
||||||
|
import { getHostname } from '../../shared/lib/get-hostname'
|
||||||
|
import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'
|
||||||
|
|
||||||
const debug = setupDebug('next:router-server:main')
|
const debug = setupDebug('next:router-server:main')
|
||||||
const isNextFont = (pathname: string | null) =>
|
const isNextFont = (pathname: string | null) =>
|
||||||
|
@ -142,6 +146,57 @@ export async function initialize(opts: {
|
||||||
require('./render-server') as typeof import('./render-server')
|
require('./render-server') as typeof import('./render-server')
|
||||||
|
|
||||||
const requestHandlerImpl: WorkerRequestHandler = async (req, res) => {
|
const requestHandlerImpl: WorkerRequestHandler = async (req, res) => {
|
||||||
|
if (
|
||||||
|
!opts.minimalMode &&
|
||||||
|
config.i18n &&
|
||||||
|
config.i18n.localeDetection !== false
|
||||||
|
) {
|
||||||
|
const urlParts = (req.url || '').split('?', 1)
|
||||||
|
let urlNoQuery = urlParts[0] || ''
|
||||||
|
|
||||||
|
if (config.basePath) {
|
||||||
|
urlNoQuery = removePathPrefix(urlNoQuery, config.basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathnameInfo = getNextPathnameInfo(urlNoQuery, {
|
||||||
|
nextConfig: config,
|
||||||
|
})
|
||||||
|
|
||||||
|
const domainLocale = detectDomainLocale(
|
||||||
|
config.i18n.domains,
|
||||||
|
getHostname({ hostname: urlNoQuery }, req.headers)
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultLocale =
|
||||||
|
domainLocale?.defaultLocale || config.i18n.defaultLocale
|
||||||
|
|
||||||
|
const { getLocaleRedirect } =
|
||||||
|
require('../../shared/lib/i18n/get-locale-redirect') as typeof import('../../shared/lib/i18n/get-locale-redirect')
|
||||||
|
|
||||||
|
const parsedUrl = parseUrlUtil((req.url || '')?.replace(/^\/+/, '/'))
|
||||||
|
|
||||||
|
const redirect = getLocaleRedirect({
|
||||||
|
defaultLocale,
|
||||||
|
domainLocale,
|
||||||
|
headers: req.headers,
|
||||||
|
nextConfig: config,
|
||||||
|
pathLocale: pathnameInfo.locale,
|
||||||
|
urlParsed: {
|
||||||
|
...parsedUrl,
|
||||||
|
pathname: pathnameInfo.locale
|
||||||
|
? `/${pathnameInfo.locale}${urlNoQuery}`
|
||||||
|
: urlNoQuery,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
res.setHeader('Location', redirect)
|
||||||
|
res.statusCode = RedirectStatusCode.TemporaryRedirect
|
||||||
|
res.end(redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (compress) {
|
if (compress) {
|
||||||
// @ts-expect-error not express req/res
|
// @ts-expect-error not express req/res
|
||||||
compress(req, res, () => {})
|
compress(req, res, () => {})
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export async function middleware() {
|
||||||
|
const noop = () => {}
|
||||||
|
noop()
|
||||||
|
}
|
10
test/e2e/i18n-preferred-locale-detection/app/next.config.js
Normal file
10
test/e2e/i18n-preferred-locale-detection/app/next.config.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
i18n: {
|
||||||
|
locales: ['en', 'id'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
clientRouterFilter: true,
|
||||||
|
clientRouterFilterRedirects: true,
|
||||||
|
},
|
||||||
|
}
|
21
test/e2e/i18n-preferred-locale-detection/app/pages/index.js
Normal file
21
test/e2e/i18n-preferred-locale-detection/app/pages/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export const getServerSideProps = async ({ locale }) => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
locale,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({ locale }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="index">Index</div>
|
||||||
|
<div id="current-locale">{locale}</div>
|
||||||
|
<Link href="/new" id="to-new">
|
||||||
|
To new
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
21
test/e2e/i18n-preferred-locale-detection/app/pages/new.js
Normal file
21
test/e2e/i18n-preferred-locale-detection/app/pages/new.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export const getServerSideProps = async ({ locale }) => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
locale,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function New({ locale }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="new">New</div>
|
||||||
|
<div id="current-locale">{locale}</div>
|
||||||
|
<Link href="/" id="to-index">
|
||||||
|
To index (No Locale Specified)
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { Request } from 'playwright'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { FileRef, nextTestSetup } from 'e2e-utils'
|
||||||
|
|
||||||
|
describe('i18-preferred-locale-redirect', () => {
|
||||||
|
const { next } = nextTestSetup({
|
||||||
|
files: new FileRef(join(__dirname, './app/')),
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should request a path prefixed with my preferred detected locale when accessing index', async () => {
|
||||||
|
const browser = await next.browser('/new', {
|
||||||
|
locale: 'id',
|
||||||
|
})
|
||||||
|
|
||||||
|
let requestedPreferredLocalePathCount = 0
|
||||||
|
browser.on('request', (request: Request) => {
|
||||||
|
if (new URL(request.url(), 'http://n').pathname === '/id') {
|
||||||
|
requestedPreferredLocalePathCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToIndex = async () => {
|
||||||
|
await browser.get(next.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(goToIndex()).resolves.not.toThrow(/ERR_TOO_MANY_REDIRECTS/)
|
||||||
|
|
||||||
|
await browser.waitForElementByCss('#index')
|
||||||
|
|
||||||
|
expect(await browser.elementByCss('#index').text()).toBe('Index')
|
||||||
|
expect(await browser.elementByCss('#current-locale').text()).toBe('id')
|
||||||
|
|
||||||
|
expect(requestedPreferredLocalePathCount).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not request a path prefixed with my preferred detected locale when clicking link to index from a non-locale-prefixed path', async () => {
|
||||||
|
const browser = await next.browser('/new', {
|
||||||
|
locale: 'id',
|
||||||
|
})
|
||||||
|
|
||||||
|
await browser
|
||||||
|
.waitForElementByCss('#to-index')
|
||||||
|
.click()
|
||||||
|
.waitForElementByCss('#index')
|
||||||
|
|
||||||
|
expect(await browser.elementByCss('#index').text()).toBe('Index')
|
||||||
|
expect(await browser.elementByCss('#current-locale').text()).toBe('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should request a path prefixed with my preferred detected locale when clicking link to index from a locale-prefixed path', async () => {
|
||||||
|
const browser = await next.browser('/id/new', {
|
||||||
|
locale: 'id',
|
||||||
|
})
|
||||||
|
|
||||||
|
await browser
|
||||||
|
.waitForElementByCss('#to-index')
|
||||||
|
.click()
|
||||||
|
.waitForElementByCss('#index')
|
||||||
|
|
||||||
|
expect(await browser.elementByCss('#index').text()).toBe('Index')
|
||||||
|
expect(await browser.elementByCss('#current-locale').text()).toBe('id')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue