Skip to content

Commit c4c4214

Browse files
authoredFeb 19, 2024
fix: preserve locale in redirects (#276)
1 parent df3b3a6 commit c4c4214

File tree

11 files changed

+647
-2
lines changed

11 files changed

+647
-2
lines changed
 

‎edge-runtime/lib/response.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export const buildResponse = async ({
195195

196196
// If we are redirecting a request that had a locale in the URL, we need to add it back in
197197
if (redirect && requestLocale) {
198-
redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig })
198+
redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig, requestLocale })
199199
if (redirect === request.url) {
200200
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
201201
return
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
experimental: {
11+
clientRouterFilter: true,
12+
clientRouterFilterRedirects: true,
13+
},
14+
redirects() {
15+
return [
16+
{
17+
source: '/to-new',
18+
destination: '/dynamic/new',
19+
permanent: false,
20+
},
21+
]
22+
},
23+
}

‎tests/fixtures/middleware-i18n/package-lock.json

+422
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
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": "^14.0.4",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/react": "18.2.47"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function App({ Component, pageProps }) {
2+
if (!pageProps || typeof pageProps !== 'object') {
3+
throw new Error(
4+
`Invariant: received invalid pageProps in _app, received ${pageProps}`
5+
)
6+
}
7+
return <Component {...pageProps} />
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function handler(req, res) {
2+
res.send('ok')
3+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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">
18+
Redirect me to Google (with no body response)
19+
</Link>
20+
<div />
21+
<Link href="/redirect-to-google">
22+
Redirect me to Google (with no stream response)
23+
</Link>
24+
<div />
25+
<Link href="/redirect-me-alot">Redirect me alot (chained requests)</Link>
26+
<div />
27+
<Link href="/infinite-loop">Redirect me alot (infinite loop)</Link>
28+
<div />
29+
<Link
30+
href="/to?pathname=/api/ok"
31+
locale="nl"
32+
id="link-to-api-with-locale"
33+
>
34+
Redirect me to api with locale
35+
</Link>
36+
<div />
37+
<Link href="/to?pathname=/old-home" id="link-to-to-old-home">
38+
Redirect me to a redirecting page of new version of page
39+
</Link>
40+
<div />
41+
</div>
42+
)
43+
}
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/integration/edge-handler.test.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -411,12 +411,35 @@ describe('page router', () => {
411411
url: `/_next/data/build-id/blog/first.json?slug=first`,
412412
})
413413
const res = await response.json()
414-
console.log(res)
415414
const url = new URL(res.url, 'http://n/')
416415
expect(url.pathname).toBe('/blog/first/')
417416
expect(url.searchParams.get('__nextDataReq')).toBe('1')
418417
expect(url.searchParams.get('slug')).toBe('first')
419418
expect(res.headers['x-nextjs-data']).toBe('1')
420419
expect(response.status).toBe(200)
421420
})
421+
422+
test<FixtureTestContext>('should preserve locale in redirects', async (ctx) => {
423+
await createFixture('middleware-i18n', ctx)
424+
await runPlugin(ctx)
425+
const origin = await LocalServer.run(async (req, res) => {
426+
res.write(
427+
JSON.stringify({
428+
url: req.url,
429+
headers: req.headers,
430+
}),
431+
)
432+
res.end()
433+
})
434+
ctx.cleanup?.push(() => origin.stop())
435+
const response = await invokeEdgeFunction(ctx, {
436+
functions: ['___netlify-edge-handler-middleware'],
437+
origin,
438+
url: `/fr/old-home`,
439+
redirect: 'manual',
440+
})
441+
const url = new URL(response.headers.get('location') ?? '', 'http://n/')
442+
expect(url.pathname).toBe('/fr/new-home')
443+
expect(response.status).toBe(302)
444+
})
422445
})

0 commit comments

Comments
 (0)
Please sign in to comment.