Skip to content

Commit 0c05726

Browse files
authored
fix: add missing data to middleware request object (#1634)
* fix: add missing data to middleware request object When the middleware runs on the edge, it's missing data that is hydrated by the NextServer when run in the context of the origin server. Also fix which property is accessed on nextURL in the context of the MiddlewareRequest. * docs: add limitations of edge middleware in local dev * test: add test coverage for MiddlewareRequest * fix: revert the change I made to focus on a test file * test: remove some of the hard coded strings
1 parent 4aa57ed commit 0c05726

File tree

9 files changed

+212
-22
lines changed

9 files changed

+212
-22
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ aware that this will result in slower performance, as all pages that match middl
5050

5151
For more details on Next.js Middleware with Netlify, see the [middleware docs](https://github.com/netlify/next-runtime/blob/main/docs/middleware.md).
5252

53+
### Limitations
54+
55+
Due to how the site configuration is handled when it's run using Netlify Edge Functions, data such as `locale` and `defaultLocale` will be missing on the `req.nextUrl` object when running `netlify dev`.
56+
57+
However, this data is available on `req.nextUrl` in a production environment.
58+
5359
## Monorepos
5460

5561
If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory,

jestSetup.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// eslint-disable-next-line n/no-unpublished-require
2+
require('jest-fetch-mock').enableMocks()

package-lock.json

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

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"eslint-plugin-unicorn": "^43.0.2",
6464
"husky": "^7.0.4",
6565
"jest": "^27.0.0",
66+
"jest-fetch-mock": "^3.0.3",
6667
"netlify-plugin-cypress": "^2.2.0",
6768
"npm-run-all": "^4.1.5",
6869
"prettier": "^2.1.2",
@@ -81,6 +82,9 @@
8182
"node": ">=16.0.0"
8283
},
8384
"jest": {
85+
"setupFiles": [
86+
"./jestSetup.js"
87+
],
8488
"testMatch": [
8589
"**/test/**/*.js",
8690
"**/test/**/*.ts",
@@ -106,4 +110,4 @@
106110
"demos/custom-routes",
107111
"demos/next-with-edge-functions"
108112
]
109-
}
113+
}

packages/next/src/middleware/request.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class MiddlewareRequest extends Request {
8383
}
8484

8585
get nextUrl() {
86-
return this.nextRequest.url
86+
return this.nextRequest.nextUrl
8787
}
8888

8989
get url() {

packages/next/test/request.spec.ts

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import Chance from 'chance'
2+
import { NextURL } from 'next/dist/server/web/next-url'
3+
import { NextCookies } from 'next/dist/server/web/spec-extension/cookies'
4+
import { NextRequest } from 'next/server'
5+
import { MiddlewareRequest } from '../src/middleware/request'
6+
7+
const chance = new Chance()
8+
9+
describe('MiddlewareRequest', () => {
10+
let nextRequest, mockHeaders, mockHeaderValue, requestId, geo, ip, url
11+
12+
beforeEach(() => {
13+
globalThis.Deno = {}
14+
globalThis.NFRequestContextMap = new Map()
15+
16+
ip = chance.ip()
17+
url = chance.url()
18+
19+
const context = {
20+
geo: {
21+
country: {
22+
code: chance.country(),
23+
},
24+
subdivision: {
25+
code: chance.province(),
26+
},
27+
city: chance.city(),
28+
},
29+
ip,
30+
}
31+
32+
geo = {
33+
country: context.geo.country?.code,
34+
region: context.geo.subdivision?.code,
35+
city: context.geo.city,
36+
}
37+
38+
const req = new URL(url)
39+
40+
requestId = chance.guid()
41+
globalThis.NFRequestContextMap.set(requestId, {
42+
request: req,
43+
context,
44+
})
45+
46+
mockHeaders = new Headers()
47+
mockHeaderValue = chance.word()
48+
49+
mockHeaders.append('foo', mockHeaderValue)
50+
mockHeaders.append('x-nf-request-id', requestId)
51+
52+
const request = {
53+
headers: mockHeaders,
54+
geo,
55+
method: 'GET',
56+
ip: context.ip,
57+
body: null,
58+
}
59+
60+
nextRequest = new NextRequest(req, request)
61+
})
62+
63+
afterEach(() => {
64+
nextRequest = null
65+
requestId = null
66+
delete globalThis.Deno
67+
delete globalThis.NFRequestContextMap
68+
})
69+
70+
it('throws an error when MiddlewareRequest is run outside of edge environment', () => {
71+
delete globalThis.Deno
72+
expect(() => new MiddlewareRequest(nextRequest)).toThrowError(
73+
'MiddlewareRequest only works in a Netlify Edge Function environment',
74+
)
75+
})
76+
77+
it('throws an error when x-nf-request-id header is missing', () => {
78+
nextRequest.headers.delete('x-nf-request-id')
79+
expect(() => new MiddlewareRequest(nextRequest)).toThrowError('Missing x-nf-request-id header')
80+
})
81+
82+
it('throws an error when request context is missing', () => {
83+
globalThis.NFRequestContextMap.delete(requestId)
84+
expect(() => new MiddlewareRequest(nextRequest)).toThrowError(
85+
`Could not find request context for request id ${requestId}`,
86+
)
87+
})
88+
89+
it('returns the headers object', () => {
90+
const mwRequest = new MiddlewareRequest(nextRequest)
91+
expect(mwRequest.headers).toStrictEqual(mockHeaders)
92+
})
93+
94+
it('returns the cookies object', () => {
95+
const mwRequest = new MiddlewareRequest(nextRequest)
96+
expect(mwRequest.cookies).toBeInstanceOf(NextCookies)
97+
})
98+
99+
it('returns the geo object', () => {
100+
const mwRequest = new MiddlewareRequest(nextRequest)
101+
expect(mwRequest.geo).toStrictEqual(geo)
102+
})
103+
104+
it('returns the ip object', () => {
105+
const mwRequest = new MiddlewareRequest(nextRequest)
106+
expect(mwRequest.ip).toStrictEqual(ip)
107+
})
108+
109+
it('returns the nextUrl object', () => {
110+
const mwRequest = new MiddlewareRequest(nextRequest)
111+
expect(mwRequest.nextUrl).toBeInstanceOf(NextURL)
112+
})
113+
114+
it('returns the url', () => {
115+
const mwRequest = new MiddlewareRequest(nextRequest)
116+
expect(mwRequest.url).toEqual(url)
117+
})
118+
})

packages/runtime/src/helpers/edge.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { copy, copyFile, emptyDir, ensureDir, readJSON, readJson, writeJSON, wri
77
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin'
88
import type { RouteHas } from 'next/dist/lib/load-custom-routes'
99

10+
import { getRequiredServerFiles } from './config'
11+
1012
// This is the format as of [email protected]
1113
interface EdgeFunctionDefinitionV1 {
1214
env: string[]
@@ -198,6 +200,11 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
198200

199201
await copy(getEdgeTemplatePath('../edge-shared'), join(edgeFunctionRoot, 'edge-shared'))
200202

203+
const { publish } = netlifyConfig.build
204+
const nextConfigFile = await getRequiredServerFiles(publish)
205+
const nextConfig = nextConfigFile.config
206+
await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig)
207+
201208
if (!process.env.NEXT_DISABLE_EDGE_IMAGES) {
202209
console.log(
203210
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',

packages/runtime/src/templates/edge-shared/nextConfig.json

Whitespace-only changes.

0 commit comments

Comments
 (0)