Skip to content

Commit 8fc84c5

Browse files
authoredJan 11, 2024
feat: add middleware routing (#146)
* refactor: create `src/build/functions/edge` directory * feat: remove lookaheads from matchers * feat: add routing logic to matchers * chore: add test * refactor: move things around and add test * refactor: remove check for Netlify Dev
1 parent 84b3a63 commit 8fc84c5

File tree

17 files changed

+1069
-13
lines changed

17 files changed

+1069
-13
lines changed
 

‎edge-runtime/lib/routing.ts

+451
Large diffs are not rendered by default.

‎edge-runtime/matchers.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

‎edge-runtime/middleware.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import type { Context } from '@netlify/edge-functions'
22

3+
import matchers from './matchers.json' assert { type: 'json' }
4+
35
import { buildNextRequest, RequestData } from './lib/next-request.ts'
46
import { buildResponse } from './lib/response.ts'
57
import { FetchEventResult } from './lib/response.ts'
8+
import {
9+
type MiddlewareRouteMatch,
10+
getMiddlewareRouteMatcher,
11+
searchParamsToUrlQuery,
12+
} from './lib/routing.ts'
613

714
type NextHandler = (params: { request: RequestData }) => Promise<FetchEventResult>
815

16+
const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
17+
918
/**
1019
* Runs a Next.js middleware as a Netlify Edge Function. It translates a web
1120
* platform Request into a NextRequest instance on the way in, and translates
@@ -20,13 +29,17 @@ export async function handleMiddleware(
2029
context: Context,
2130
nextHandler: NextHandler,
2231
) {
23-
// Don't run in dev
24-
if (Netlify.env.has('NETLIFY_DEV')) {
32+
const nextRequest = buildNextRequest(request, context)
33+
const url = new URL(request.url)
34+
35+
// While we have already checked the path when mapping to the edge function,
36+
// Next.js supports extra rules that we need to check here too, because we
37+
// might be running an edge function for a path we should not. If we find
38+
// that's the case, short-circuit the execution.
39+
if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
2540
return
2641
}
2742

28-
const nextRequest = buildNextRequest(request, context)
29-
3043
try {
3144
const result = await nextHandler({ request: nextRequest })
3245
const response = await buildResponse({ result, request: request, context })

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"url": "https://github.com/netlify/next-runtime-minimal/issues"
4040
},
4141
"homepage": "https://github.com/netlify/next-runtime-minimal#readme",
42-
"dependencies": {},
4342
"devDependencies": {
4443
"@fastly/http-compute-js": "1.1.1",
4544
"@netlify/blobs": "^6.4.0",

‎src/build/functions/edge.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,25 @@ const writeEdgeManifest = async (ctx: PluginContext, manifest: NetlifyManifest)
2323
await writeFile(join(ctx.edgeFunctionsDir, 'manifest.json'), JSON.stringify(manifest, null, 2))
2424
}
2525

26-
const writeHandlerFile = async (ctx: PluginContext, { name }: NextDefinition) => {
26+
const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefinition) => {
2727
const handlerName = getHandlerName({ name })
28+
const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
29+
const handlerRuntimeDirectory = join(handlerDirectory, 'edge-runtime')
2830

29-
await cp(
30-
join(ctx.pluginDir, 'edge-runtime'),
31-
join(ctx.edgeFunctionsDir, handlerName, 'edge-runtime'),
32-
{ recursive: true },
33-
)
31+
// Copying the runtime files. These are the compatibility layer between
32+
// Netlify Edge Functions and the Next.js edge runtime.
33+
await cp(join(ctx.pluginDir, 'edge-runtime'), handlerRuntimeDirectory, {
34+
recursive: true,
35+
})
36+
37+
// Writing a file with the matchers that should trigger this function. We'll
38+
// read this file from the function at runtime.
39+
await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
40+
41+
// Writing the function entry file. It wraps the middleware code with the
42+
// compatibility layer mentioned above.
3443
await writeFile(
35-
join(ctx.edgeFunctionsDir, handlerName, `${handlerName}.js`),
44+
join(handlerDirectory, `${handlerName}.js`),
3645
`
3746
import {handleMiddleware} from './edge-runtime/middleware.ts';
3847
import handler from './server/${name}.js';
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Home() {
2+
return (
3+
<main>
4+
<h1>Home</h1>
5+
</main>
6+
)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { headers } from 'next/headers'
2+
3+
export default function Page() {
4+
const headersList = headers()
5+
const message = headersList.get('x-hello-from-middleware-req')
6+
7+
return (
8+
<main>
9+
<h1>Message from middleware: {message}</h1>
10+
</main>
11+
)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Redirect() {
2+
return (
3+
<main>
4+
<h1>If middleware works, we shoudn't get here</h1>
5+
</main>
6+
)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Rewrite() {
2+
return (
3+
<main>
4+
<h1>If middleware works, we shoudn't get here</h1>
5+
</main>
6+
)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { NextRequest } from 'next/server'
2+
import { NextResponse } from 'next/server'
3+
4+
export function middleware(request: NextRequest) {
5+
const response: NextResponse = NextResponse.next()
6+
7+
response.headers.set('x-hello-from-middleware-res', 'hello')
8+
9+
return response
10+
}
11+
12+
export const config = {
13+
matcher: [
14+
{
15+
source: '/foo',
16+
missing: [{ type: 'header', key: 'x-custom-header', value: 'custom-value' }],
17+
},
18+
],
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
output: 'standalone',
4+
eslint: {
5+
ignoreDuringBuilds: true,
6+
},
7+
}
8+
9+
module.exports = nextConfig

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

+412
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,15 @@
1+
{
2+
"name": "middleware",
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": "13.5.2",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
}
15+
}

‎tests/integration/edge-handler.test.ts

+71-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { v4 } from 'uuid'
2-
import { beforeEach, expect, test, vi } from 'vitest'
2+
import { beforeEach, describe, expect, test, vi } from 'vitest'
33
import {
44
createFixture,
55
invokeEdgeFunction,
@@ -94,3 +94,73 @@ test<FixtureTestContext>('should rewrite to an external URL', async (ctx) => {
9494
expect(external.calls).toBe(1)
9595
expect(origin.calls).toBe(0)
9696
})
97+
98+
describe("aborts middleware execution when the matcher conditions don't match the request", () => {
99+
test<FixtureTestContext>('when the path is excluded', async (ctx) => {
100+
await createFixture('middleware', ctx)
101+
await runPlugin(ctx)
102+
103+
const origin = await LocalServer.run(async (req, res) => {
104+
expect(req.url).toBe('/_next/data')
105+
expect(req.headers['x-hello-from-middleware-req']).toBeUndefined()
106+
107+
res.write('Hello from origin!')
108+
res.end()
109+
})
110+
111+
ctx.cleanup?.push(() => origin.stop())
112+
113+
const response1 = await invokeEdgeFunction(ctx, {
114+
functions: ['___netlify-edge-handler-middleware'],
115+
origin,
116+
url: '/_next/data',
117+
})
118+
119+
expect(await response1.text()).toBe('Hello from origin!')
120+
expect(response1.status).toBe(200)
121+
expect(response1.headers.has('x-hello-from-middleware-res')).toBeFalsy()
122+
expect(origin.calls).toBe(1)
123+
})
124+
125+
test<FixtureTestContext>('when a request header matches a condition', async (ctx) => {
126+
await createFixture('middleware-conditions', ctx)
127+
await runPlugin(ctx)
128+
129+
const origin = await LocalServer.run(async (req, res) => {
130+
expect(req.url).toBe('/foo')
131+
expect(req.headers['x-hello-from-middleware-req']).toBeUndefined()
132+
133+
res.write('Hello from origin!')
134+
res.end()
135+
})
136+
137+
ctx.cleanup?.push(() => origin.stop())
138+
139+
// Request 1: Middleware should run because we're not sending the header.
140+
const response1 = await invokeEdgeFunction(ctx, {
141+
functions: ['___netlify-edge-handler-middleware'],
142+
origin,
143+
url: '/foo',
144+
})
145+
146+
expect(await response1.text()).toBe('Hello from origin!')
147+
expect(response1.status).toBe(200)
148+
expect(response1.headers.has('x-hello-from-middleware-res')).toBeTruthy()
149+
expect(origin.calls).toBe(1)
150+
151+
// Request 1: Middleware should not run because we're sending the header.
152+
const response2 = await invokeEdgeFunction(ctx, {
153+
headers: {
154+
'x-custom-header': 'custom-value',
155+
},
156+
functions: ['___netlify-edge-handler-middleware'],
157+
origin,
158+
url: '/foo',
159+
})
160+
161+
expect(await response2.text()).toBe('Hello from origin!')
162+
expect(response2.status).toBe(200)
163+
expect(response2.headers.has('x-hello-from-middleware-res')).toBeFalsy()
164+
expect(origin.calls).toBe(2)
165+
})
166+
})

‎tests/utils/fixture.ts

+6
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ export async function invokeEdgeFunction(
368368
*/
369369
url?: string
370370

371+
/**
372+
* Custom headers for the request
373+
*/
374+
headers?: Record<string, string>
375+
371376
/**
372377
* Whether to follow redirects
373378
*/
@@ -397,6 +402,7 @@ export async function invokeEdgeFunction(
397402
'x-nf-passthrough-host': passthroughHost,
398403
'x-nf-passthrough-proto': 'http:',
399404
'x-nf-request-id': v4(),
405+
...options.headers,
400406
},
401407
})
402408
}

0 commit comments

Comments
 (0)
Please sign in to comment.