Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a9efa9c

Browse files
fix: middleware i18n normalization (#2483)
* fix: middleware i18n normalization * test: add new middleware i18n fixture for skipping normalization * test: more test coverage for middleware i18n and for skipping normalization * chore: remove test.only * chore: refactor detectedLocale assignment
1 parent 063027c commit a9efa9c

File tree

11 files changed

+298
-31
lines changed

11 files changed

+298
-31
lines changed

‎edge-runtime/lib/next-request.ts‎

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { Context } from '@netlify/edge-functions'
22

3-
import { addBasePath, normalizeDataUrl, normalizeLocalePath, removeBasePath } from './util.ts'
3+
import {
4+
addBasePath,
5+
addTrailingSlash,
6+
normalizeDataUrl,
7+
normalizeLocalePath,
8+
removeBasePath,
9+
} from './util.ts'
410

511
interface I18NConfig {
612
defaultLocale: string
@@ -41,43 +47,25 @@ const normalizeRequestURL = (
4147
): { url: string; detectedLocale?: string } => {
4248
const url = new URL(originalURL)
4349

44-
url.pathname = removeBasePath(url.pathname, nextConfig?.basePath)
45-
const didRemoveBasePath = url.toString() !== originalURL
50+
let pathname = removeBasePath(url.pathname, nextConfig?.basePath)
4651

47-
let detectedLocale: string | undefined
48-
49-
if (nextConfig?.i18n) {
50-
const { pathname, detectedLocale: detected } = normalizeLocalePath(
51-
url.pathname,
52-
nextConfig?.i18n?.locales,
53-
)
54-
if (!nextConfig?.skipMiddlewareUrlNormalize) {
55-
url.pathname = pathname || '/'
56-
}
57-
detectedLocale = detected
58-
}
52+
// If it exists, remove the locale from the URL and store it
53+
const { detectedLocale } = normalizeLocalePath(pathname, nextConfig?.i18n?.locales)
5954

6055
if (!nextConfig?.skipMiddlewareUrlNormalize) {
6156
// We want to run middleware for data requests and expose the URL of the
6257
// corresponding pages, so we have to normalize the URLs before running
6358
// the handler.
64-
url.pathname = normalizeDataUrl(url.pathname)
59+
pathname = normalizeDataUrl(pathname)
6560

6661
// Normalizing the trailing slash based on the `trailingSlash` configuration
6762
// property from the Next.js config.
68-
if (nextConfig?.trailingSlash&&url.pathname!=='/'&&!url.pathname.endsWith('/')) {
69-
url.pathname = `${url.pathname}/`
63+
if (nextConfig?.trailingSlash) {
64+
pathname = addTrailingSlash(pathname)
7065
}
7166
}
7267

73-
if (didRemoveBasePath) {
74-
url.pathname = addBasePath(url.pathname, nextConfig?.basePath)
75-
}
76-
77-
// keep the locale in the url for request.nextUrl object
78-
if (detectedLocale) {
79-
url.pathname = `/${detectedLocale}${url.pathname}`
80-
}
68+
url.pathname = addBasePath(pathname, nextConfig?.basePath)
8169

8270
return {
8371
url: url.toString(),
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export async function middleware(request) {
4+
const url = request.nextUrl
5+
6+
// this is needed for tests to get the BUILD_ID
7+
if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
8+
return NextResponse.next()
9+
}
10+
11+
if (url.pathname === '/old-home') {
12+
if (url.searchParams.get('override') === 'external') {
13+
return Response.redirect('https://example.vercel.sh')
14+
} else {
15+
url.pathname = '/new-home'
16+
return Response.redirect(url)
17+
}
18+
}
19+
20+
if (url.searchParams.get('foo') === 'bar') {
21+
url.pathname = '/new-home'
22+
url.searchParams.delete('foo')
23+
return Response.redirect(url)
24+
}
25+
26+
// Chained redirects
27+
if (url.pathname === '/redirect-me-alot') {
28+
url.pathname = '/redirect-me-alot-2'
29+
return Response.redirect(url)
30+
}
31+
32+
if (url.pathname === '/redirect-me-alot-2') {
33+
url.pathname = '/redirect-me-alot-3'
34+
return Response.redirect(url)
35+
}
36+
37+
if (url.pathname === '/redirect-me-alot-3') {
38+
url.pathname = '/redirect-me-alot-4'
39+
return Response.redirect(url)
40+
}
41+
42+
if (url.pathname === '/redirect-me-alot-4') {
43+
url.pathname = '/redirect-me-alot-5'
44+
return Response.redirect(url)
45+
}
46+
47+
if (url.pathname === '/redirect-me-alot-5') {
48+
url.pathname = '/redirect-me-alot-6'
49+
return Response.redirect(url)
50+
}
51+
52+
if (url.pathname === '/redirect-me-alot-6') {
53+
url.pathname = '/redirect-me-alot-7'
54+
return Response.redirect(url)
55+
}
56+
57+
if (url.pathname === '/redirect-me-alot-7') {
58+
url.pathname = '/new-home'
59+
return Response.redirect(url)
60+
}
61+
62+
// Infinite loop
63+
if (url.pathname === '/infinite-loop') {
64+
url.pathname = '/infinite-loop-1'
65+
return Response.redirect(url)
66+
}
67+
68+
if (url.pathname === '/infinite-loop-1') {
69+
url.pathname = '/infinite-loop'
70+
return Response.redirect(url)
71+
}
72+
73+
if (url.pathname === '/to') {
74+
url.pathname = url.searchParams.get('pathname')
75+
url.searchParams.delete('pathname')
76+
return Response.redirect(url)
77+
}
78+
79+
if (url.pathname === '/with-fragment') {
80+
console.log(String(new URL('/new-home#fragment', url)))
81+
return Response.redirect(new URL('/new-home#fragment', url))
82+
}
83+
84+
if (url.pathname.includes('/json')) {
85+
return NextResponse.json({
86+
requestUrlPathname: new URL(request.url).pathname,
87+
nextUrlPathname: request.nextUrl.pathname,
88+
nextUrlLocale: request.nextUrl.locale,
89+
})
90+
}
91+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
output: 'standalone',
3+
eslint: {
4+
ignoreDuringBuilds: true,
5+
},
6+
i18n: {
7+
locales: ['en', 'fr', 'nl', 'es'],
8+
defaultLocale: 'en',
9+
},
10+
skipMiddlewareUrlNormalize: true,
11+
experimental: {
12+
clientRouterFilter: true,
13+
clientRouterFilterRedirects: true,
14+
},
15+
redirects() {
16+
return [
17+
{
18+
source: '/to-new',
19+
destination: '/dynamic/new',
20+
permanent: false,
21+
},
22+
]
23+
},
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "middleware-pages",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"postinstall": "next build",
7+
"dev": "next dev",
8+
"build": "next build"
9+
},
10+
"dependencies": {
11+
"next": "latest",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/react": "18.2.47"
17+
}
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function App({ Component, pageProps }) {
2+
if (!pageProps || typeof pageProps !== 'object') {
3+
throw new Error(`Invariant: received invalid pageProps in _app, received ${pageProps}`)
4+
}
5+
return <Component {...pageProps} />
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function handler(req, res) {
2+
res.send('ok')
3+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function Account({ slug }) {
2+
return (
3+
<p id="dynamic" className="title">
4+
Welcome to a /dynamic/[slug]: {slug}
5+
</p>
6+
)
7+
}
8+
9+
export function getServerSideProps({ params }) {
10+
return {
11+
props: {
12+
slug: params.slug,
13+
},
14+
}
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Link from 'next/link'
2+
3+
export default function Home() {
4+
return (
5+
<div>
6+
<p className="title">Home Page</p>
7+
<Link href="/old-home" id="old-home">
8+
Redirect me to a new version of a page
9+
</Link>
10+
<div />
11+
<Link href="/old-home?override=external" id="old-home-external">
12+
Redirect me to an external site
13+
</Link>
14+
<div />
15+
<Link href="/blank-page?foo=bar">Redirect me with URL params intact</Link>
16+
<div />
17+
<Link href="/redirect-to-google">Redirect me to Google (with no body response)</Link>
18+
<div />
19+
<Link href="/redirect-to-google">Redirect me to Google (with no stream response)</Link>
20+
<div />
21+
<Link href="/redirect-me-alot">Redirect me alot (chained requests)</Link>
22+
<div />
23+
<Link href="/infinite-loop">Redirect me alot (infinite loop)</Link>
24+
<div />
25+
<Link href="/to?pathname=/api/ok" locale="nl" id="link-to-api-with-locale">
26+
Redirect me to api with locale
27+
</Link>
28+
<div />
29+
<Link href="/to?pathname=/old-home" id="link-to-to-old-home">
30+
Redirect me to a redirecting page of new version of page
31+
</Link>
32+
<div />
33+
</div>
34+
)
35+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Account() {
2+
return (
3+
<p id="new-home-title" className="title">
4+
Welcome to a new page
5+
</p>
6+
)
7+
}

‎tests/fixtures/middleware-i18n/middleware.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export async function middleware(request) {
8282
}
8383

8484
if (url.pathname.includes('/json')) {
85-
return NextResponse.json({ url: request.nextUrl.href, locale: request.nextUrl.locale })
85+
return NextResponse.json({
86+
requestUrlPathname: new URL(request.url).pathname,
87+
nextUrlPathname: request.nextUrl.pathname,
88+
nextUrlLocale: request.nextUrl.locale,
89+
})
8690
}
8791
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /