Skip to content

Commit 6bd9781

Browse files
committed
fix: ensure 2nd level middleware match respects locales
1 parent a118607 commit 6bd9781

File tree

6 files changed

+107
-100
lines changed

6 files changed

+107
-100
lines changed

edge-runtime/lib/next-request.ts

Lines changed: 75 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,110 @@ import type { Context } from '@netlify/edge-functions'
22

33
import {
44
addBasePath,
5-
addTrailingSlash,
5+
addLocale,
66
normalizeDataUrl,
77
normalizeLocalePath,
8+
normalizeTrailingSlash,
89
removeBasePath,
910
} from './util.ts'
1011

11-
interface I18NConfig {
12-
defaultLocale: string
13-
localeDetection?: false
14-
locales: string[]
15-
}
12+
import type { NextConfig } from 'next/dist/server/config-shared'
13+
import type { NextRequest } from 'next/server'
1614

17-
export interface RequestData {
18-
geo?: {
19-
city?: string
20-
country?: string
21-
region?: string
22-
latitude?: string
23-
longitude?: string
24-
timezone?: string
25-
}
26-
headers: Record<string, string>
27-
ip?: string
28-
method: string
29-
nextConfig?: {
30-
basePath?: string
31-
i18n?: I18NConfig | null
32-
trailingSlash?: boolean
33-
skipMiddlewareUrlNormalize?: boolean
34-
}
35-
page?: {
36-
name?: string
37-
params?: { [key: string]: string }
38-
}
39-
url: string
40-
body?: ReadableStream<Uint8Array>
15+
export type NetlifyNextRequest = Pick<
16+
NextRequest,
17+
'url' | 'headers' | 'geo' | 'ip' | 'method' | 'body'
18+
>
19+
20+
export type NetlifyNextContext = {
21+
localizedUrl: string
4122
detectedLocale?: string
23+
i18n?: NextConfig['i18n']
24+
basePath?: NextConfig['basePath']
25+
trailingSlash?: NextConfig['trailingSlash']
4226
}
4327

44-
const normalizeRequestURL = (
45-
originalURL: string,
46-
nextConfig?: RequestData['nextConfig'],
47-
): { url: string; detectedLocale?: string } => {
28+
const normalizeRequestURL = (originalURL: string, nextConfig?: NextConfig): string => {
4829
const url = new URL(originalURL)
4930

50-
let pathname = removeBasePath(url.pathname, nextConfig?.basePath)
31+
url.pathname = removeBasePath(url.pathname, nextConfig?.basePath)
5132

52-
// If it exists, remove the locale from the URL and store it
53-
const { detectedLocale } = normalizeLocalePath(pathname, nextConfig?.i18n?.locales)
33+
// We want to run middleware for data requests and expose the URL of the
34+
// corresponding pages, so we have to normalize the URLs before running
35+
// the handler.
36+
url.pathname = normalizeDataUrl(url.pathname)
5437

55-
if (!nextConfig?.skipMiddlewareUrlNormalize) {
56-
// We want to run middleware for data requests and expose the URL of the
57-
// corresponding pages, so we have to normalize the URLs before running
58-
// the handler.
59-
pathname = normalizeDataUrl(pathname)
38+
// Normalizing the trailing slash based on the `trailingSlash` configuration
39+
// property from the Next.js config.
40+
url.pathname = normalizeTrailingSlash(url.pathname, nextConfig?.trailingSlash)
6041

61-
// Normalizing the trailing slash based on the `trailingSlash` configuration
62-
// property from the Next.js config.
63-
if (nextConfig?.trailingSlash) {
64-
pathname = addTrailingSlash(pathname)
65-
}
66-
}
42+
url.pathname = addBasePath(url.pathname, nextConfig?.basePath)
43+
44+
return url.toString()
45+
}
46+
47+
const localizeRequestURL = (
48+
originalURL: string,
49+
nextConfig?: NextConfig,
50+
): { localizedUrl: string; detectedLocale?: string } => {
51+
const url = new URL(originalURL)
52+
53+
url.pathname = removeBasePath(url.pathname, nextConfig?.basePath)
6754

68-
url.pathname = addBasePath(pathname, nextConfig?.basePath)
55+
// Detect the locale from the URL
56+
const { detectedLocale } = normalizeLocalePath(url.pathname, nextConfig?.i18n?.locales)
57+
58+
// Add the locale to the URL if not already present
59+
url.pathname = addLocale(url.pathname, detectedLocale ?? nextConfig?.i18n?.defaultLocale)
60+
61+
url.pathname = addBasePath(url.pathname, nextConfig?.basePath)
6962

7063
return {
71-
url: url.toString(),
64+
localizedUrl: url.toString(),
7265
detectedLocale,
7366
}
7467
}
7568

7669
export const buildNextRequest = (
7770
request: Request,
7871
context: Context,
79-
nextConfig?: RequestData['nextConfig'],
80-
): RequestData => {
72+
nextConfig?: NextConfig,
73+
): { nextRequest: NetlifyNextRequest; nextContext: NetlifyNextContext } => {
8174
const { url, method, body, headers } = request
82-
const { country, subdivision, city, latitude, longitude, timezone } = context.geo
83-
const geo: RequestData['geo'] = {
84-
city,
85-
country: country?.code,
86-
region: subdivision?.code,
87-
latitude: latitude?.toString(),
88-
longitude: longitude?.toString(),
89-
timezone,
90-
}
75+
const { country, subdivision, city, latitude, longitude } = context.geo
76+
const { i18n, basePath, trailingSlash } = nextConfig ?? {}
9177

92-
const { detectedLocale, url: normalizedUrl } = normalizeRequestURL(url, nextConfig)
78+
const normalizedUrl = nextConfig?.skipMiddlewareUrlNormalize
79+
? url
80+
: normalizeRequestURL(url, nextConfig)
9381

94-
return {
95-
headers: Object.fromEntries(headers.entries()),
96-
geo,
82+
const { localizedUrl, detectedLocale } = localizeRequestURL(normalizedUrl, nextConfig)
83+
84+
const nextRequest: NetlifyNextRequest = {
9785
url: normalizedUrl,
98-
method,
86+
headers,
87+
geo: {
88+
city,
89+
country: country?.code,
90+
region: subdivision?.code,
91+
latitude: latitude?.toString(),
92+
longitude: longitude?.toString(),
93+
},
9994
ip: context.ip,
100-
body: body ?? undefined,
101-
nextConfig,
95+
method,
96+
body,
97+
}
98+
99+
const nextContext = {
100+
localizedUrl,
102101
detectedLocale,
102+
i18n,
103+
trailingSlash,
104+
basePath,
105+
}
106+
107+
return {
108+
nextRequest,
109+
nextContext,
103110
}
104111
}

edge-runtime/lib/response.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { HTMLRewriter } from '../vendor/deno.land/x/[email protected]/
44
import { updateModifiedHeaders } from './headers.ts'
55
import type { StructuredLogger } from './logging.ts'
66
import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts'
7-
import { RequestData } from './next-request.ts'
7+
import { NetlifyNextContext } from './next-request.ts'
88
import {
99
addBasePath,
1010
normalizeDataUrl,
@@ -20,20 +20,18 @@ export interface FetchEventResult {
2020

2121
interface BuildResponseOptions {
2222
context: Context
23-
logger: StructuredLogger
23+
nextContext?: NetlifyNextContext
2424
request: Request
2525
result: FetchEventResult
26-
nextConfig?: RequestData['nextConfig']
27-
requestLocale?: string
26+
logger: StructuredLogger
2827
}
2928

3029
export const buildResponse = async ({
3130
context,
32-
logger,
31+
nextContext,
3332
request,
3433
result,
35-
nextConfig,
36-
requestLocale,
34+
logger,
3735
}: BuildResponseOptions): Promise<Response | void> => {
3836
logger
3937
.withFields({ is_nextresponse_next: result.response.headers.has('x-middleware-next') })
@@ -185,9 +183,9 @@ export const buildResponse = async ({
185183
}
186184

187185
// respect trailing slash rules to prevent 308s
188-
rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextConfig?.trailingSlash)
186+
rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextContext?.trailingSlash)
189187

190-
const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextConfig })
188+
const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextContext })
191189
if (target === request.url) {
192190
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
193191
return
@@ -198,8 +196,8 @@ export const buildResponse = async ({
198196
}
199197

200198
// If we are redirecting a request that had a locale in the URL, we need to add it back in
201-
if (redirect && requestLocale) {
202-
redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig, requestLocale })
199+
if (redirect && nextContext?.detectedLocale) {
200+
redirect = normalizeLocalizedTarget({ target: redirect, request, nextContext })
203201
if (redirect === request.url) {
204202
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
205203
return
@@ -233,28 +231,26 @@ export const buildResponse = async ({
233231
function normalizeLocalizedTarget({
234232
target,
235233
request,
236-
nextConfig,
237-
requestLocale,
234+
nextContext,
238235
}: {
239236
target: string
240237
request: Request
241-
nextConfig?: RequestData['nextConfig']
242-
requestLocale?: string
238+
nextContext?: NetlifyNextContext
243239
}) {
244240
const targetUrl = new URL(target, request.url)
245241

246-
const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextConfig?.i18n?.locales)
242+
const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextContext?.i18n?.locales)
247243

248-
const locale = normalizedTarget.detectedLocale ?? requestLocale
244+
const locale = normalizedTarget.detectedLocale ?? nextContext?.detectedLocale
249245
if (
250246
locale &&
251247
!normalizedTarget.pathname.startsWith(`/api/`) &&
252248
!normalizedTarget.pathname.startsWith(`/_next/static/`)
253249
) {
254250
targetUrl.pathname =
255-
addBasePath(`/${locale}${normalizedTarget.pathname}`, nextConfig?.basePath) || `/`
251+
addBasePath(`/${locale}${normalizedTarget.pathname}`, nextContext?.basePath) || `/`
256252
} else {
257-
targetUrl.pathname = addBasePath(normalizedTarget.pathname, nextConfig?.basePath) || `/`
253+
targetUrl.pathname = addBasePath(normalizedTarget.pathname, nextContext?.basePath) || `/`
258254
}
259255
return targetUrl.toString()
260256
}

edge-runtime/lib/routing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import type { Key } from '../vendor/deno.land/x/[email protected]/index.ts'
99

10-
import { compile, pathToRegexp } from '../vendor/deno.land/x/[email protected]/index.ts'
1110
import { getCookies } from '../vendor/deno.land/[email protected]/http/cookie.ts'
11+
import { compile, pathToRegexp } from '../vendor/deno.land/x/[email protected]/index.ts'
1212

1313
/*
1414
┌─────────────────────────────────────────────────────────────────────────┐

edge-runtime/lib/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export const addBasePath = (path: string, basePath?: string) => {
2929
return path
3030
}
3131

32+
export const addLocale = (path: string, locale?: string) => {
33+
if (locale && path !== `/${locale}` && !path.startsWith(`/${locale}/`)) {
34+
return `/${locale}${path}`
35+
}
36+
return path
37+
}
38+
3239
// https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts
3340

3441
export interface PathLocale {

edge-runtime/middleware.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ 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, NetlifyNextRequest } from './lib/next-request.ts'
99
import { buildResponse, FetchEventResult } from './lib/response.ts'
1010
import {
1111
getMiddlewareRouteMatcher,
1212
searchParamsToUrlQuery,
1313
type MiddlewareRouteMatch,
1414
} from './lib/routing.ts'
1515

16-
type NextHandler = (params: { request: RequestData }) => Promise<FetchEventResult>
16+
type NextHandler = (params: { request: NetlifyNextRequest }) => Promise<FetchEventResult>
1717

1818
const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
1919

@@ -31,7 +31,6 @@ 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)
3635
const reqLogger = logger
3736
.withLogLevel(
@@ -40,25 +39,26 @@ export async function handleMiddleware(
4039
.withFields({ url_path: url.pathname })
4140
.withRequestID(request.headers.get(InternalHeaders.NFRequestID))
4241

42+
const { nextRequest, nextContext } = buildNextRequest(request, context, nextConfig)
43+
const localizedPath = new URL(nextContext.localizedUrl).pathname
44+
4345
// While we have already checked the path when mapping to the edge function,
4446
// Next.js supports extra rules that we need to check here too, because we
4547
// might be running an edge function for a path we should not. If we find
4648
// that's the case, short-circuit the execution.
47-
if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
49+
if (!matchesMiddleware(localizedPath, request, searchParamsToUrlQuery(url.searchParams))) {
4850
reqLogger.debug('Aborting middleware due to runtime rules')
49-
5051
return
5152
}
5253

5354
try {
5455
const result = await nextHandler({ request: nextRequest })
5556
const response = await buildResponse({
5657
context,
57-
logger: reqLogger,
58+
nextContext,
5859
request,
5960
result,
60-
requestLocale: nextRequest.detectedLocale,
61-
nextConfig,
61+
logger: reqLogger,
6262
})
6363

6464
return response

src/build/functions/edge.ts

Lines changed: 1 addition & 4 deletions
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

0 commit comments

Comments
 (0)