From 930e8098a265749f3119e0032ccfb1003e17ec72 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 22 Sep 2022 15:07:28 -0400 Subject: [PATCH 1/5] 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. --- packages/next/src/middleware/request.ts | 2 +- packages/runtime/src/helpers/edge.ts | 7 +++++++ packages/runtime/src/templates/edge-shared/nextConfig.json | 0 packages/runtime/src/templates/edge/runtime.ts | 6 ++++-- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 packages/runtime/src/templates/edge-shared/nextConfig.json diff --git a/packages/next/src/middleware/request.ts b/packages/next/src/middleware/request.ts index c323d607cd..f04554ad7d 100644 --- a/packages/next/src/middleware/request.ts +++ b/packages/next/src/middleware/request.ts @@ -83,7 +83,7 @@ export class MiddlewareRequest extends Request { } get nextUrl() { - return this.nextRequest.url + return this.nextRequest.nextUrl } get url() { diff --git a/packages/runtime/src/helpers/edge.ts b/packages/runtime/src/helpers/edge.ts index 2f6ec0ad40..5d65853ddd 100644 --- a/packages/runtime/src/helpers/edge.ts +++ b/packages/runtime/src/helpers/edge.ts @@ -7,6 +7,8 @@ import { copy, copyFile, emptyDir, ensureDir, readJSON, readJson, writeJSON, wri import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin' import type { RouteHas } from 'next/dist/lib/load-custom-routes' +import { getRequiredServerFiles } from './config' + // This is the format as of next@12.2 interface EdgeFunctionDefinitionV1 { env: string[] @@ -198,6 +200,11 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => { await copy(getEdgeTemplatePath('../edge-shared'), join(edgeFunctionRoot, 'edge-shared')) + const { publish } = netlifyConfig.build + const nextConfigFile = await getRequiredServerFiles(publish) + const nextConfig = nextConfigFile.config + await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig) + if (!process.env.NEXT_DISABLE_EDGE_IMAGES) { console.log( 'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.', diff --git a/packages/runtime/src/templates/edge-shared/nextConfig.json b/packages/runtime/src/templates/edge-shared/nextConfig.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/runtime/src/templates/edge/runtime.ts b/packages/runtime/src/templates/edge/runtime.ts index da7c5e3278..e5b1870184 100644 --- a/packages/runtime/src/templates/edge/runtime.ts +++ b/packages/runtime/src/templates/edge/runtime.ts @@ -1,6 +1,7 @@ import type { Context } from 'https://edge.netlify.com' // Available at build time import matchers from './matchers.json' assert { type: 'json' } +import nextConfig from '../edge-shared/nextConfig.json' assert { type: 'json' } import edgeFunction from './bundle.js' import { buildResponse } from '../edge-shared/utils.ts' import { getMiddlewareRouteMatcher, MiddlewareRouteMatch, searchParamsToUrlQuery } from '../edge-shared/next-utils.ts' @@ -32,7 +33,7 @@ export interface RequestData { name?: string params?: { [key: string]: string } } - url: string + url: URL body?: ReadableStream } @@ -81,10 +82,11 @@ const handler = async (req: Request, context: Context) => { const request: RequestData = { headers: Object.fromEntries(req.headers.entries()), geo, - url: url.toString(), + url, method: req.method, ip: context.ip, body: req.body ?? undefined, + nextConfig, } try { From b8fdb1dd44e84302a4a9900e4af0fe89e6cfc3c2 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 23 Sep 2022 12:17:23 -0400 Subject: [PATCH 2/5] docs: add limitations of edge middleware in local dev --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4ff09c1931..c56b0637ce 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ Next.js Middleware works out of the box on Netlify. By default, middleware runs support for running Middleware at the origin, set the environment variable `NEXT_DISABLE_NETLIFY_EDGE` to `true`. Be aware that this will result in slower performance, as all pages that match middleware must use SSR. +### Limitations + +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`. + +However, this data is available on `req.nextUrl` in a production environment. + ## Monorepos If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory, From 6dac0aecb8b2e0252352913749650f3c58661f80 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 23 Sep 2022 15:29:38 -0400 Subject: [PATCH 3/5] test: add test coverage for MiddlewareRequest --- jestSetup.js | 2 + package-lock.json | 87 ++++++++++++++++----- package.json | 8 +- packages/next/test/request.spec.ts | 118 +++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 jestSetup.js create mode 100644 packages/next/test/request.spec.ts diff --git a/jestSetup.js b/jestSetup.js new file mode 100644 index 0000000000..c9a73e7cfb --- /dev/null +++ b/jestSetup.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line n/no-unpublished-require +require('jest-fetch-mock').enableMocks() diff --git a/package-lock.json b/package-lock.json index 25205e5652..760cd4fee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "eslint-plugin-unicorn": "^43.0.2", "husky": "^7.0.4", "jest": "^27.0.0", + "jest-fetch-mock": "^3.0.3", "netlify-plugin-cypress": "^2.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", @@ -3979,9 +3980,9 @@ } }, "node_modules/@netlify/edge-bundler/node_modules/node-fetch": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz", - "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", + "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", "dev": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -9035,6 +9036,15 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -11963,9 +11973,9 @@ } }, "node_modules/fetch-blob": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", - "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, "funding": [ { @@ -14461,6 +14471,16 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", @@ -17378,17 +17398,17 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -19087,6 +19107,12 @@ "node": ">=0.4.0" } }, + "node_modules/promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -25936,9 +25962,9 @@ }, "dependencies": { "node-fetch": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz", - "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", + "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", "dev": true, "requires": { "data-uri-to-buffer": "^4.0.0", @@ -29699,6 +29725,15 @@ "cross-spawn": "^7.0.1" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -32002,9 +32037,9 @@ } }, "fetch-blob": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", - "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, "requires": { "node-domexception": "^1.0.0", @@ -33812,6 +33847,16 @@ "jest-util": "^27.5.1" } }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "jest-get-type": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", @@ -36160,17 +36205,17 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -37413,6 +37458,12 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", + "dev": true + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/package.json b/package.json index d73fae37d5..ef5da7a08d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "postinstall": "run-s build install-husky", "install-husky": "if-env CI=1 || husky install node_modules/@netlify/eslint-config-node/.husky", "test": "run-s build:demo test:jest", - "test:jest": "jest", + "test:jest": "jest --testMatch=**/test/request.spec.ts", "test:jest:update": "jest --updateSnapshot", "test:update": "run-s build build:demo test:jest:update" }, @@ -63,6 +63,7 @@ "eslint-plugin-unicorn": "^43.0.2", "husky": "^7.0.4", "jest": "^27.0.0", + "jest-fetch-mock": "^3.0.3", "netlify-plugin-cypress": "^2.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", @@ -81,6 +82,9 @@ "node": ">=16.0.0" }, "jest": { + "setupFiles": [ + "./jestSetup.js" + ], "testMatch": [ "**/test/**/*.js", "**/test/**/*.ts", @@ -106,4 +110,4 @@ "demos/custom-routes", "demos/next-with-edge-functions" ] -} \ No newline at end of file +} diff --git a/packages/next/test/request.spec.ts b/packages/next/test/request.spec.ts new file mode 100644 index 0000000000..407bb5bacf --- /dev/null +++ b/packages/next/test/request.spec.ts @@ -0,0 +1,118 @@ +import Chance from 'chance' +import { NextURL } from 'next/dist/server/web/next-url' +import { NextCookies } from 'next/dist/server/web/spec-extension/cookies' +import { NextRequest } from 'next/server' +import { MiddlewareRequest } from '../src/middleware/request' + +const chance = new Chance() + +describe('MiddlewareRequest', () => { + let nextRequest, mockHeaders, mockHeaderValue, requestId, geo, ip, url + + beforeEach(() => { + globalThis.Deno = {} + globalThis.NFRequestContextMap = new Map() + + ip = chance.ip() + url = chance.url() + + const context = { + geo: { + country: { + code: '', + }, + subdivision: { + code: '', + }, + city: '', + }, + ip, + } + + geo = { + country: context.geo.country?.code, + region: context.geo.subdivision?.code, + city: context.geo.city, + } + + const req = new URL(url) + + requestId = chance.guid() + globalThis.NFRequestContextMap.set(requestId, { + request: req, + context, + }) + + mockHeaders = new Headers() + mockHeaderValue = chance.word() + + mockHeaders.append('foo', mockHeaderValue) + mockHeaders.append('x-nf-request-id', requestId) + + const request = { + headers: mockHeaders, + geo, + method: 'GET', + ip: context.ip, + body: null, + } + + nextRequest = new NextRequest(req, request) + }) + + afterEach(() => { + nextRequest = null + requestId = null + delete globalThis.Deno + delete globalThis.NFRequestContextMap + }) + + it('throws an error when MiddlewareRequest is run outside of edge environment', () => { + delete globalThis.Deno + expect(() => new MiddlewareRequest(nextRequest)).toThrowError( + 'MiddlewareRequest only works in a Netlify Edge Function environment', + ) + }) + + it('throws an error when x-nf-request-id header is missing', () => { + nextRequest.headers.delete('x-nf-request-id') + expect(() => new MiddlewareRequest(nextRequest)).toThrowError('Missing x-nf-request-id header') + }) + + it('throws an error when request context is missing', () => { + globalThis.NFRequestContextMap.delete(requestId) + expect(() => new MiddlewareRequest(nextRequest)).toThrowError( + `Could not find request context for request id ${requestId}`, + ) + }) + + it('returns the headers object', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.headers).toStrictEqual(mockHeaders) + }) + + it('returns the cookies object', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.cookies).toBeInstanceOf(NextCookies) + }) + + it('returns the geo object', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.geo).toStrictEqual(geo) + }) + + it('returns the ip object', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.ip).toStrictEqual(ip) + }) + + it('returns the nextUrl object', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.nextUrl).toBeInstanceOf(NextURL) + }) + + it('returns the url', () => { + const mwRequest = new MiddlewareRequest(nextRequest) + expect(mwRequest.url).toEqual(url) + }) +}) From 5d7027d854817779b1a30e57581b1fde0c39401f Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 23 Sep 2022 15:39:41 -0400 Subject: [PATCH 4/5] fix: revert the change I made to focus on a test file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef5da7a08d..5665dba738 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "postinstall": "run-s build install-husky", "install-husky": "if-env CI=1 || husky install node_modules/@netlify/eslint-config-node/.husky", "test": "run-s build:demo test:jest", - "test:jest": "jest --testMatch=**/test/request.spec.ts", + "test:jest": "jest", "test:jest:update": "jest --updateSnapshot", "test:update": "run-s build build:demo test:jest:update" }, From 154bff2096f05224391d3da6eff6b42aaf579c79 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 23 Sep 2022 15:46:01 -0400 Subject: [PATCH 5/5] test: remove some of the hard coded strings --- packages/next/test/request.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/test/request.spec.ts b/packages/next/test/request.spec.ts index 407bb5bacf..da43029d7f 100644 --- a/packages/next/test/request.spec.ts +++ b/packages/next/test/request.spec.ts @@ -19,12 +19,12 @@ describe('MiddlewareRequest', () => { const context = { geo: { country: { - code: '', + code: chance.country(), }, subdivision: { - code: '', + code: chance.province(), }, - city: '', + city: chance.city(), }, ip, }