Skip to content

Commit 5155474

Browse files
feat: normalise URLs in edge middleware (#176)
* feat: normalise URLs in edge middleware * refactor: remove lib reference * chore: update fixtures * fix: remove unused import * refactor: name things better * refactor: rename parameter
1 parent 1eee565 commit 5155474

File tree

19 files changed

+640
-14
lines changed

19 files changed

+640
-14
lines changed

edge-runtime/lib/next-request.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Context } from '@netlify/edge-functions'
22

3+
import { normalizeDataUrl } from './util.ts'
4+
35
interface I18NConfig {
46
defaultLocale: string
57
localeDetection?: false
@@ -31,6 +33,23 @@ export interface RequestData {
3133
body?: ReadableStream<Uint8Array>
3234
}
3335

36+
const normalizeRequestURL = (originalURL: string, enforceTrailingSlash: boolean) => {
37+
const url = new URL(originalURL)
38+
39+
// We want to run middleware for data requests and expose the URL of the
40+
// corresponding pages, so we have to normalize the URLs before running
41+
// the handler.
42+
url.pathname = normalizeDataUrl(url.pathname)
43+
44+
// Normalizing the trailing slash based on the `trailingSlash` configuration
45+
// property from the Next.js config.
46+
if (enforceTrailingSlash && url.pathname !== '/' && !url.pathname.endsWith('/')) {
47+
url.pathname = `${url.pathname}/`
48+
}
49+
50+
return url.toString()
51+
}
52+
3453
export const buildNextRequest = (
3554
request: Request,
3655
context: Context,
@@ -50,7 +69,7 @@ export const buildNextRequest = (
5069
return {
5170
headers: Object.fromEntries(headers.entries()),
5271
geo,
53-
url,
72+
url: normalizeRequestURL(url, Boolean(nextConfig?.trailingSlash)),
5473
method,
5574
ip: context.ip,
5675
body: body ?? undefined,

edge-runtime/lib/response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const buildResponse = async ({ context, logger, request, result }: BuildR
106106
const isDataReq = request.headers.get('x-nextjs-data')
107107

108108
if (rewrite) {
109-
logger.withFields({ rewrite_url: rewrite }).debug('Is rewrite')
109+
logger.withFields({ rewrite_url: rewrite }).debug('Found middleware rewrite')
110110

111111
const rewriteUrl = new URL(rewrite, request.url)
112112
const baseUrl = new URL(request.url)

edge-runtime/lib/util.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
// If the redirect is a data URL, we need to normalize it.
1+
// If the URL path matches a data URL, we need to normalize it.
22
// https://github.com/vercel/next.js/blob/25e0988e7c9033cb1503cbe0c62ba5de2e97849c/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L69-L76
3-
export function normalizeDataUrl(redirect: string) {
4-
if (redirect.startsWith('/_next/data/') && redirect.includes('.json')) {
5-
const paths = redirect
3+
export function normalizeDataUrl(urlPath: string) {
4+
if (urlPath.startsWith('/_next/data/') && urlPath.includes('.json')) {
5+
const paths = urlPath
66
.replace(/^\/_next\/data\//, '')
77
.replace(/\.json/, '')
88
.split('/')
99

10-
redirect = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/'
10+
urlPath = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/'
1111
}
1212

13-
return redirect
13+
return urlPath
1414
}
1515

1616
/**

edge-runtime/middleware.ts

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

33
import matchers from './matchers.json' assert { type: 'json' }
4+
import nextConfig from './next.config.json' assert { type: 'json' }
45

56
import { InternalHeaders } from './lib/headers.ts'
67
import { logger, LogLevel } from './lib/logging.ts'
@@ -31,7 +32,7 @@ export async function handleMiddleware(
3132
context: Context,
3233
nextHandler: NextHandler,
3334
) {
34-
const nextRequest = buildNextRequest(request, context)
35+
const nextRequest = buildNextRequest(request, context, nextConfig)
3536
const url = new URL(request.url)
3637
const reqLogger = logger
3738
.withLogLevel(

edge-runtime/next.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

src/build/functions/edge.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const writeEdgeManifest = async (ctx: PluginContext, manifest: NetlifyManifest)
2424
}
2525

2626
const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefinition) => {
27+
const nextConfig = await ctx.getBuildConfig()
2728
const handlerName = getHandlerName({ name })
2829
const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
2930
const handlerRuntimeDirectory = join(handlerDirectory, 'edge-runtime')
@@ -38,6 +39,20 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
3839
// read this file from the function at runtime.
3940
await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
4041

42+
// The config is needed by the edge function to match and normalize URLs. To
43+
// avoid shipping and parsing a large file at runtime, let's strip it down to
44+
// just the properties that the edge function actually needs.
45+
const minimalNextConfig = {
46+
basePath: nextConfig.basePath,
47+
i18n: nextConfig.i18n,
48+
trailingSlash: nextConfig.trailingSlash,
49+
}
50+
51+
await writeFile(
52+
join(handlerRuntimeDirectory, 'next.config.json'),
53+
JSON.stringify(minimalNextConfig),
54+
)
55+
4156
// Writing the function entry file. It wraps the middleware code with the
4257
// compatibility layer mentioned above.
4358
await writeFile(

src/run/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ export const setRunConfig = (config: NextConfigComplete) => {
2929
export type TagsManifest = Record<string, string>
3030

3131
export const getTagsManifest = async (): Promise<TagsManifest> => {
32-
return JSON.parse(await readFile(resolve('.netlify/tags-manifest.json'), 'utf-8'))
32+
return JSON.parse(await readFile(resolve(PLUGIN_DIR, '.netlify/tags-manifest.json'), 'utf-8'))
3333
}

tests/fixtures/middleware-conditions/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "middleware",
2+
"name": "middleware-conditions",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Simple Next App',
3+
description: 'Description for Simple Next App',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return (
3+
<main>
4+
<h1>Other</h1>
5+
</main>
6+
)
7+
}

0 commit comments

Comments
 (0)