Skip to content

Commit 590db9b

Browse files
fix: match bare route for i18n site middleware (#288)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 0348fac commit 590db9b

File tree

7 files changed

+89
-3
lines changed

7 files changed

+89
-3
lines changed

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"os": "^0.1.2",
7777
"outdent": "^0.8.0",
7878
"p-limit": "^4.0.0",
79+
"path-to-regexp": "^6.2.1",
7980
"picomatch": "^3.0.1",
8081
"regexp-tree": "^0.1.27",
8182
"typescript": "^5.1.6",

src/build/functions/edge.ts

+35-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { dirname, join, relative } from 'node:path'
33

44
import { glob } from 'fast-glob'
55
import type { EdgeFunctionDefinition as NextDefinition } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
6+
import { pathToRegexp } from 'path-to-regexp'
67

78
import { EDGE_HANDLER_NAME, PluginContext } from '../plugin-context.js'
89

@@ -37,6 +38,33 @@ const copyRuntime = async (ctx: PluginContext, handlerDirectory: string): Promis
3738
)
3839
}
3940

41+
/**
42+
* When i18n is enabled the matchers assume that paths _always_ include the
43+
* locale. We manually add an extra matcher for the original path without
44+
* the locale to ensure that the edge function can handle it.
45+
* We don't need to do this for data routes because they always have the locale.
46+
*/
47+
const augmentMatchers = (
48+
matchers: NextDefinition['matchers'],
49+
ctx: PluginContext,
50+
): NextDefinition['matchers'] => {
51+
if (!ctx.buildConfig.i18n) {
52+
return matchers
53+
}
54+
return matchers.flatMap((matcher) => {
55+
if (matcher.originalSource && matcher.locale !== false) {
56+
return [
57+
matcher,
58+
{
59+
...matcher,
60+
regexp: pathToRegexp(matcher.originalSource).source,
61+
},
62+
]
63+
}
64+
return matcher
65+
})
66+
}
67+
4068
const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefinition) => {
4169
const nextConfig = ctx.buildConfig
4270
const handlerName = getHandlerName({ name })
@@ -49,7 +77,10 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
4977

5078
// Writing a file with the matchers that should trigger this function. We'll
5179
// read this file from the function at runtime.
52-
await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
80+
await writeFile(
81+
join(handlerRuntimeDirectory, 'matchers.json'),
82+
JSON.stringify(augmentMatchers(matchers, ctx)),
83+
)
5384

5485
// The config is needed by the edge function to match and normalize URLs. To
5586
// avoid shipping and parsing a large file at runtime, let's strip it down to
@@ -140,9 +171,10 @@ const buildHandlerDefinition = (
140171
const funName = name.endsWith('middleware')
141172
? 'Next.js Middleware Handler'
142173
: `Next.js Edge Handler: ${page}`
143-
const cache = name.endsWith('middleware') ? undefined : 'manual'
174+
const cache = name.endsWith('middleware') ? undefined : ('manual' as const)
144175
const generator = `${ctx.pluginName}@${ctx.pluginVersion}`
145-
return matchers.map((matcher) => ({
176+
177+
return augmentMatchers(matchers, ctx).map((matcher) => ({
146178
function: fun,
147179
name: funName,
148180
pattern: matcher.regexp,

tests/fixtures/middleware-conditions/middleware.ts

+7
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,12 @@ export const config = {
1515
source: '/foo',
1616
missing: [{ type: 'header', key: 'x-custom-header', value: 'custom-value' }],
1717
},
18+
{
19+
source: '/hello',
20+
},
21+
{
22+
source: '/nl-NL/about',
23+
locale: false,
24+
},
1825
],
1926
}

tests/fixtures/middleware-conditions/next.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
output: 'standalone',
4+
i18n: {
5+
locales: ['en', 'fr', 'nl', 'es'],
6+
defaultLocale: 'en',
7+
},
48
eslint: {
59
ignoreDuringBuilds: true,
610
},

tests/integration/edge-handler.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,45 @@ describe("aborts middleware execution when the matcher conditions don't match th
251251
expect(response2.headers.has('x-hello-from-middleware-res')).toBeFalsy()
252252
expect(origin.calls).toBe(2)
253253
})
254+
255+
test<FixtureTestContext>('should handle locale matching correctly', async (ctx) => {
256+
await createFixture('middleware-conditions', ctx)
257+
await runPlugin(ctx)
258+
259+
const origin = await LocalServer.run(async (req, res) => {
260+
expect(req.headers['x-hello-from-middleware-req']).toBeUndefined()
261+
262+
res.write('Hello from origin!')
263+
res.end()
264+
})
265+
266+
ctx.cleanup?.push(() => origin.stop())
267+
268+
for (const path of ['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about']) {
269+
const response = await invokeEdgeFunction(ctx, {
270+
functions: ['___netlify-edge-handler-middleware'],
271+
origin,
272+
url: path,
273+
})
274+
expect(response.headers.has('x-hello-from-middleware-res'), `does match ${path}`).toBeTruthy()
275+
expect(await response.text()).toBe('Hello from origin!')
276+
expect(response.status).toBe(200)
277+
}
278+
279+
for (const path of ['/hello/invalid', '/about', '/en/about']) {
280+
const response = await invokeEdgeFunction(ctx, {
281+
functions: ['___netlify-edge-handler-middleware'],
282+
origin,
283+
url: path,
284+
})
285+
expect(
286+
response.headers.has('x-hello-from-middleware-res'),
287+
`does not match ${path}`,
288+
).toBeFalsy()
289+
expect(await response.text()).toBe('Hello from origin!')
290+
expect(response.status).toBe(200)
291+
}
292+
})
254293
})
255294

256295
describe('should run middleware on data requests', () => {

tests/utils/fixture.ts

+2
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ export async function runPlugin(
259259
const result = await bundle([edgeSource], dist, [], {
260260
bootstrapURL,
261261
internalSrcFolder: edgeSource,
262+
// Temporary until https://github.com/netlify/edge-bundler/pull/580 rolls out
263+
featureFlags: { edge_bundler_pcre_regexp: true },
262264
importMapPaths: [],
263265
basePath: ctx.cwd,
264266
configPath: join(edgeSource, 'manifest.json'),

0 commit comments

Comments
 (0)