Skip to content

Commit f02ef88

Browse files
piehorinokai
andauthored
fix: edge-middleware i18n matching (#2555)
* fix: edge-middleware i18n matching * test: update edge-runtime test * test: do not localize data requests --------- Co-authored-by: Rob Stanford <[email protected]>
1 parent 4ee9ef0 commit f02ef88

File tree

6 files changed

+58
-12
lines changed

6 files changed

+58
-12
lines changed

edge-runtime/lib/next-request.ts

+28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Context } from '@netlify/edge-functions'
22

33
import {
44
addBasePath,
5+
addLocale,
56
addTrailingSlash,
67
normalizeDataUrl,
78
normalizeLocalePath,
@@ -73,6 +74,33 @@ const normalizeRequestURL = (
7374
}
7475
}
7576

77+
export const localizeRequest = (
78+
url: URL,
79+
nextConfig?: {
80+
basePath?: string
81+
i18n?: I18NConfig | null
82+
},
83+
): { localizedUrl: URL; locale?: string } => {
84+
const localizedUrl = new URL(url)
85+
localizedUrl.pathname = removeBasePath(localizedUrl.pathname, nextConfig?.basePath)
86+
87+
// Detect the locale from the URL
88+
const { detectedLocale } = normalizeLocalePath(localizedUrl.pathname, nextConfig?.i18n?.locales)
89+
90+
// Add the locale to the URL if not already present
91+
localizedUrl.pathname = addLocale(
92+
localizedUrl.pathname,
93+
detectedLocale ?? nextConfig?.i18n?.defaultLocale,
94+
)
95+
96+
localizedUrl.pathname = addBasePath(localizedUrl.pathname, nextConfig?.basePath)
97+
98+
return {
99+
localizedUrl,
100+
locale: detectedLocale,
101+
}
102+
}
103+
76104
export const buildNextRequest = (
77105
request: Request,
78106
context: Context,

edge-runtime/lib/util.ts

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ export const addBasePath = (path: string, basePath?: string) => {
2929
return path
3030
}
3131

32+
// add locale prefix if not present, allowing for locale fallbacks
33+
export const addLocale = (path: string, locale?: string) => {
34+
if (
35+
locale &&
36+
path.toLowerCase() !== `/${locale.toLowerCase()}` &&
37+
!path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) &&
38+
!path.startsWith(`/api/`) &&
39+
!path.startsWith(`/_next/`)
40+
) {
41+
return `/${locale}${path}`
42+
}
43+
return path
44+
}
45+
3246
// https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts
3347

3448
export interface PathLocale {

edge-runtime/middleware.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import nextConfig from './next.config.json' with { type: 'json' }
55

66
import { InternalHeaders } from './lib/headers.ts'
77
import { logger, LogLevel } from './lib/logging.ts'
8-
import { buildNextRequest, RequestData } from './lib/next-request.ts'
8+
import { buildNextRequest, localizeRequest, RequestData } from './lib/next-request.ts'
99
import { buildResponse, FetchEventResult } from './lib/response.ts'
1010
import {
1111
getMiddlewareRouteMatcher,
@@ -31,25 +31,29 @@ export async function handleMiddleware(
3131
context: Context,
3232
nextHandler: NextHandler,
3333
) {
34-
const nextRequest = buildNextRequest(request, context, nextConfig)
3534
const url = new URL(request.url)
35+
3636
const reqLogger = logger
3737
.withLogLevel(
3838
request.headers.has(InternalHeaders.NFDebugLogging) ? LogLevel.Debug : LogLevel.Log,
3939
)
4040
.withFields({ url_path: url.pathname })
4141
.withRequestID(request.headers.get(InternalHeaders.NFRequestID))
4242

43+
const { localizedUrl } = localizeRequest(url, nextConfig)
4344
// While we have already checked the path when mapping to the edge function,
4445
// Next.js supports extra rules that we need to check here too, because we
4546
// might be running an edge function for a path we should not. If we find
4647
// that's the case, short-circuit the execution.
47-
if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
48+
if (
49+
!matchesMiddleware(localizedUrl.pathname, request, searchParamsToUrlQuery(url.searchParams))
50+
) {
4851
reqLogger.debug('Aborting middleware due to runtime rules')
4952

5053
return
5154
}
5255

56+
const nextRequest = buildNextRequest(request, context, nextConfig)
5357
try {
5458
const result = await nextHandler({ request: nextRequest })
5559
const response = await buildResponse({

src/build/functions/edge.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
6565

6666
// Writing a file with the matchers that should trigger this function. We'll
6767
// read this file from the function at runtime.
68-
await writeFile(
69-
join(handlerRuntimeDirectory, 'matchers.json'),
70-
JSON.stringify(augmentMatchers(matchers, ctx)),
71-
)
68+
await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
7269

7370
// The config is needed by the edge function to match and normalize URLs. To
7471
// avoid shipping and parsing a large file at runtime, let's strip it down to

tests/fixtures/middleware-conditions/middleware.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const config = {
1919
source: '/hello',
2020
},
2121
{
22-
source: '/nl-NL/about',
22+
source: '/nl/about',
2323
locale: false,
2424
},
2525
],

tests/integration/edge-handler.test.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -261,26 +261,29 @@ describe("aborts middleware execution when the matcher conditions don't match th
261261

262262
ctx.cleanup?.push(() => origin.stop())
263263

264-
for (const path of ['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about']) {
264+
for (const path of ['/hello', '/en/hello', '/nl/hello', '/nl/about']) {
265265
const response = await invokeEdgeFunction(ctx, {
266266
functions: ['___netlify-edge-handler-middleware'],
267267
origin,
268268
url: path,
269269
})
270-
expect(response.headers.has('x-hello-from-middleware-res'), `does match ${path}`).toBeTruthy()
270+
expect(
271+
response.headers.has('x-hello-from-middleware-res'),
272+
`should match ${path}`,
273+
).toBeTruthy()
271274
expect(await response.text()).toBe('Hello from origin!')
272275
expect(response.status).toBe(200)
273276
}
274277

275-
for (const path of ['/hello/invalid', '/about', '/en/about']) {
278+
for (const path of ['/invalid/hello', '/hello/invalid', '/about', '/en/about']) {
276279
const response = await invokeEdgeFunction(ctx, {
277280
functions: ['___netlify-edge-handler-middleware'],
278281
origin,
279282
url: path,
280283
})
281284
expect(
282285
response.headers.has('x-hello-from-middleware-res'),
283-
`does not match ${path}`,
286+
`should not match ${path}`,
284287
).toBeFalsy()
285288
expect(await response.text()).toBe('Hello from origin!')
286289
expect(response.status).toBe(200)

0 commit comments

Comments
 (0)