From 311f618f219375d80fefcdfd5f2d8cfa18fc39da Mon Sep 17 00:00:00 2001 From: Baoshan Sheng Date: Fri, 9 Sep 2022 17:12:36 +0800 Subject: [PATCH] refactor(middleware)!: remove onUnhandledRequest middleware option BREAKING CHANGE: onUnhandledRequest middleware option is removed --- README.md | 36 ++----- package-lock.json | 77 +++++++++++---- package.json | 4 +- src/middleware/node/index.ts | 54 +++++----- .../node/on-unhandled-request-default.ts | 19 ---- test/node-middleware.test.ts | 99 ++++++++++++++++--- 6 files changed, 178 insertions(+), 111 deletions(-) delete mode 100644 src/middleware/node/on-unhandled-request-default.ts diff --git a/README.md b/README.md index 1910373c7..2dcb2f83b 100644 --- a/README.md +++ b/README.md @@ -334,11 +334,18 @@ const app = new App({ }); const middleware = createNodeMiddleware(app); - -require("http").createServer(middleware).listen(3000); +createServer(async (req, res) => { + // `middleware` returns `false` when `req` is unhandled (beyond `/api/github`) + if (await middleware(req, res)) return; + res.writeHead(404); + res.end(); +}).listen(3000); // can now receive user authorization callbacks at /api/github/* ``` +The middleware returned from `createNodeMiddleware` can also serve as an +`Express.js` middleware directly. + @@ -391,31 +398,6 @@ Used for internal logging. Defaults to [`console`](https://developer.mozilla.org - - - -
- options.onUnhandledRequest - - function - - -Defaults to - -```js -function onUnhandledRequest(request, response) { - response.writeHead(400, { - "content-type": "application/json", - }); - response.end( - JSON.stringify({ - error: error.message, - }) - ); -} -``` - -
diff --git a/package-lock.json b/package-lock.json index 63e43f029..8e6e8a1bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,10 @@ "@octokit/auth-app": "^4.0.0", "@octokit/auth-unauthenticated": "^3.0.0", "@octokit/core": "^4.0.0", - "@octokit/oauth-app": "^4.0.7", + "@octokit/oauth-app": "5.0.0-beta.1", "@octokit/plugin-paginate-rest": "^4.0.0", "@octokit/types": "^7.0.0", - "@octokit/webhooks": "^10.0.0" + "@octokit/webhooks": "11.0.0-beta.1" }, "devDependencies": { "@pika/pack": "^0.3.7", @@ -38,6 +38,43 @@ "node": ">= 14" } }, + "../oauth-app.js": { + "name": "@octokit/oauth-app", + "version": "0.0.0-development", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^5.0.0", + "@octokit/auth-oauth-user": "^2.0.0", + "@octokit/auth-unauthenticated": "^3.0.0", + "@octokit/core": "^4.0.0", + "@octokit/oauth-authorization-url": "^5.0.0", + "@octokit/oauth-methods": "^2.0.0", + "@types/aws-lambda": "^8.10.83", + "fromentries": "^1.3.1", + "universal-user-agent": "^6.0.0" + }, + "devDependencies": { + "@pika/pack": "^0.3.7", + "@pika/plugin-build-node": "^0.9.2", + "@pika/plugin-ts-standard-pkg": "^0.9.2", + "@types/jest": "^28.0.0", + "@types/node": "^16.0.0", + "@types/node-fetch": "^2.5.4", + "express": "^4.17.1", + "fetch-mock": "^9.0.0", + "jest": "^28.0.0", + "nock": "^13.0.0", + "node-fetch": "^2.6.0", + "prettier": "2.7.1", + "semantic-release-plugin-update-version-in-files": "^1.0.0", + "ts-jest": "^28.0.0", + "typescript": "^4.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -2726,9 +2763,9 @@ } }, "node_modules/@octokit/oauth-app": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-4.0.8.tgz", - "integrity": "sha512-bPZNtV2KD8x9deUa+gLX1o6IhIMFwt6UlVZNJTnS9zSCbTpnmWzHhrZr86nM3fKOvU/KPyz40zJMGsoLtvaOgw==", + "version": "5.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-5.0.0-beta.1.tgz", + "integrity": "sha512-s3gMhBPIc+CiQX2ktUAwqn6LelT7AUVp448cfnDSqlslbxOumKwbouTK4nrs0Qp3EKAFKyzo4QvWRY/ASRA50g==", "dependencies": { "@octokit/auth-oauth-app": "^5.0.0", "@octokit/auth-oauth-user": "^2.0.0", @@ -2824,9 +2861,9 @@ } }, "node_modules/@octokit/webhooks": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-10.1.5.tgz", - "integrity": "sha512-sQkxM6l9HdG1vsHFj2T/o8SnCPDDxovcs0rsSd4UR5jJFNPCPIBRmFNVHfM37nncLKuTwIpmMeePphNf1k6Waw==", + "version": "11.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-11.0.0-beta.1.tgz", + "integrity": "sha512-wwOdpGPKI1TUHPBZxZRqC9eJDPZd/4gu8BRgLXCLULf6F0+FUyQ32UXudsGwNP+1PSJ3E4g8On4gVIUFnjZnsA==", "dependencies": { "@octokit/request-error": "^3.0.0", "@octokit/webhooks-methods": "^3.0.0", @@ -3121,9 +3158,9 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.102", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.102.tgz", - "integrity": "sha512-BT05v46n9KtSHa9SgGuOvm49eSruJ9utD8iNXpdpuUVYk8wOcqmm1LEzpNRkrXxD0CULc38sdLpk6q3Wa2WOwg==" + "version": "8.10.103", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.103.tgz", + "integrity": "sha512-mYWsrM5YPmnyJru7kMDX8RYSc486sDqVOP1kUdotthD3YjJ57iTBN3N7MMtL1qdVoPW2YmCnNnWscyidmPe6Gw==" }, "node_modules/@types/babel__core": { "version": "7.1.19", @@ -11214,9 +11251,9 @@ } }, "@octokit/oauth-app": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-4.0.8.tgz", - "integrity": "sha512-bPZNtV2KD8x9deUa+gLX1o6IhIMFwt6UlVZNJTnS9zSCbTpnmWzHhrZr86nM3fKOvU/KPyz40zJMGsoLtvaOgw==", + "version": "5.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-5.0.0-beta.1.tgz", + "integrity": "sha512-s3gMhBPIc+CiQX2ktUAwqn6LelT7AUVp448cfnDSqlslbxOumKwbouTK4nrs0Qp3EKAFKyzo4QvWRY/ASRA50g==", "requires": { "@octokit/auth-oauth-app": "^5.0.0", "@octokit/auth-oauth-user": "^2.0.0", @@ -11291,9 +11328,9 @@ } }, "@octokit/webhooks": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-10.1.5.tgz", - "integrity": "sha512-sQkxM6l9HdG1vsHFj2T/o8SnCPDDxovcs0rsSd4UR5jJFNPCPIBRmFNVHfM37nncLKuTwIpmMeePphNf1k6Waw==", + "version": "11.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-11.0.0-beta.1.tgz", + "integrity": "sha512-wwOdpGPKI1TUHPBZxZRqC9eJDPZd/4gu8BRgLXCLULf6F0+FUyQ32UXudsGwNP+1PSJ3E4g8On4gVIUFnjZnsA==", "requires": { "@octokit/request-error": "^3.0.0", "@octokit/webhooks-methods": "^3.0.0", @@ -11531,9 +11568,9 @@ } }, "@types/aws-lambda": { - "version": "8.10.102", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.102.tgz", - "integrity": "sha512-BT05v46n9KtSHa9SgGuOvm49eSruJ9utD8iNXpdpuUVYk8wOcqmm1LEzpNRkrXxD0CULc38sdLpk6q3Wa2WOwg==" + "version": "8.10.103", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.103.tgz", + "integrity": "sha512-mYWsrM5YPmnyJru7kMDX8RYSc486sDqVOP1kUdotthD3YjJ57iTBN3N7MMtL1qdVoPW2YmCnNnWscyidmPe6Gw==" }, "@types/babel__core": { "version": "7.1.19", diff --git a/package.json b/package.json index fa3ebd021..93b48bf5d 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "@octokit/auth-app": "^4.0.0", "@octokit/auth-unauthenticated": "^3.0.0", "@octokit/core": "^4.0.0", - "@octokit/oauth-app": "^4.0.7", + "@octokit/oauth-app": "5.0.0-beta.1", "@octokit/plugin-paginate-rest": "^4.0.0", "@octokit/types": "^7.0.0", - "@octokit/webhooks": "^10.0.0" + "@octokit/webhooks": "11.0.0-beta.1" }, "devDependencies": { "@pika/pack": "^0.3.7", diff --git a/src/middleware/node/index.ts b/src/middleware/node/index.ts index f71d3bd79..7a152bdbb 100644 --- a/src/middleware/node/index.ts +++ b/src/middleware/node/index.ts @@ -4,20 +4,19 @@ type IncomingMessage = any; type ServerResponse = any; -import { createNodeMiddleware as oauthNodeMiddleware } from "@octokit/oauth-app"; +import { + createNodeMiddleware as oauthNodeMiddleware, + sendNodeResponse, + unknownRouteResponse, +} from "@octokit/oauth-app"; import { createNodeMiddleware as webhooksNodeMiddleware } from "@octokit/webhooks"; import { App } from "../../index"; -import { onUnhandledRequestDefault } from "./on-unhandled-request-default"; import { Options } from "../../types"; export type MiddlewareOptions = { pathPrefix?: string; log?: Options["log"]; - onUnhandledRequest?: ( - request: IncomingMessage, - response: ServerResponse - ) => void; }; function noop() {} @@ -37,7 +36,6 @@ export function createNodeMiddleware( ); const optionsWithDefaults = { - onUnhandledRequest: onUnhandledRequestDefault, pathPrefix: "/api/github", ...options, log, @@ -46,41 +44,41 @@ export function createNodeMiddleware( const webhooksMiddleware = webhooksNodeMiddleware(app.webhooks, { path: optionsWithDefaults.pathPrefix + "/webhooks", log, - onUnhandledRequest: optionsWithDefaults.onUnhandledRequest, }); const oauthMiddleware = oauthNodeMiddleware(app.oauth, { pathPrefix: optionsWithDefaults.pathPrefix + "/oauth", - onUnhandledRequest: optionsWithDefaults.onUnhandledRequest, }); - return middleware.bind(null, optionsWithDefaults, { + return middleware.bind( + null, + optionsWithDefaults.pathPrefix, webhooksMiddleware, - oauthMiddleware, - }); + oauthMiddleware + ); } export async function middleware( - options: Required, - { webhooksMiddleware, oauthMiddleware }: any, + pathPrefix: string, + webhooksMiddleware: any, + oauthMiddleware: any, request: IncomingMessage, response: ServerResponse, next?: Function -) { +): Promise { const { pathname } = new URL(request.url as string, "http://localhost"); - if (pathname === `${options.pathPrefix}/webhooks`) { - return webhooksMiddleware(request, response, next); - } - if (pathname.startsWith(`${options.pathPrefix}/oauth/`)) { - return oauthMiddleware(request, response, next); + if (pathname.startsWith(`${pathPrefix}/`)) { + if (pathname === `${pathPrefix}/webhooks`) { + webhooksMiddleware(request, response); + } else if (pathname.startsWith(`${pathPrefix}/oauth/`)) { + oauthMiddleware(request, response); + } else { + sendNodeResponse(unknownRouteResponse(request), response); + } + return true; + } else { + next?.(); + return false; } - - const isExpressMiddleware = typeof next === "function"; - if (isExpressMiddleware) { - // @ts-ignore `next` must be a function as we check two lines above - return next(); - } - - return options.onUnhandledRequest(request, response); } diff --git a/src/middleware/node/on-unhandled-request-default.ts b/src/middleware/node/on-unhandled-request-default.ts deleted file mode 100644 index 3664190fc..000000000 --- a/src/middleware/node/on-unhandled-request-default.ts +++ /dev/null @@ -1,19 +0,0 @@ -// remove type imports from http for Deno compatibility -// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 -// import { IncomingMessage, ServerResponse } from "http"; -type IncomingMessage = any; -type ServerResponse = any; - -export function onUnhandledRequestDefault( - request: IncomingMessage, - response: ServerResponse -) { - response.writeHead(404, { - "content-type": "application/json", - }); - response.end( - JSON.stringify({ - error: `Unknown route: ${request.method} ${request.url}`, - }) - ); -} diff --git a/test/node-middleware.test.ts b/test/node-middleware.test.ts index cdd45a1c8..a218f5112 100644 --- a/test/node-middleware.test.ts +++ b/test/node-middleware.test.ts @@ -36,30 +36,99 @@ x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w== -----END RSA PRIVATE KEY-----`; describe("createNodeMiddleware()", () => { - test("unknown route", async () => { - const app = new App({ - appId: APP_ID, - privateKey: PRIVATE_KEY, - webhooks: { - secret: "mysecret", - }, - oauth: { - clientId: "", - clientSecret: "", - }, + test("unknown route (outside pathPrefix)", async () => { + const middleware = createNodeMiddleware({} as unknown as App); + const server = createServer(async (req, res) => { + if (!(await middleware(req, res))) { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.write("Not found."); + res.end(); + } + }).listen(); + // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface + const { port } = server.address(); + + const response = await fetch(`http://localhost:${port}/unknown-route`); + + expect(response.status).toEqual(404); + expect(response.headers.get("Content-Type")).toBe("text/plain"); + await expect(response.text()).resolves.toBe("Not found."); + + server.close(); + }); + + test("unknown route (within pathPrefix)", async () => { + const middleware = createNodeMiddleware({} as unknown as App); + const server = createServer(async (req, res) => { + if (!(await middleware(req, res))) { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.write("Not found."); + res.end(); + } + }).listen(); + // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface + const { port } = server.address(); + + const response = await fetch( + `http://localhost:${port}/api/github/unknown-route` + ); + + expect(response.status).toEqual(404); + expect(response.headers.get("Content-Type")).toBe("application/json"); + await expect(response.json()).resolves.toEqual({ + error: "Unknown route: GET /api/github/unknown-route", }); - const server = createServer(createNodeMiddleware(app)).listen(); + server.close(); + }); + + test("unknown route (within webhooks path)", async () => { + const middleware = createNodeMiddleware({} as unknown as App); + const server = createServer(async (req, res) => { + if (!(await middleware(req, res))) { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.write("Not found."); + res.end(); + } + }).listen(); // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface const { port } = server.address(); - const response = await fetch(`http://localhost:${port}/unknown-route`); + const response = await fetch( + `http://localhost:${port}/api/github/webhooks` + ); expect(response.status).toEqual(404); - await expect(response.text()).resolves.toMatch( - /Unknown route: GET \/unknown-route/ + expect(response.headers.get("Content-Type")).toBe("application/json"); + await expect(response.json()).resolves.toEqual({ + error: "Unknown route: GET /api/github/webhooks", + }); + + server.close(); + }); + + test("unknown route (within oauth pathPrefix)", async () => { + const middleware = createNodeMiddleware({} as unknown as App); + const server = createServer(async (req, res) => { + if (!(await middleware(req, res))) { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.write("Not found."); + res.end(); + } + }).listen(); + // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface + const { port } = server.address(); + + const response = await fetch( + `http://localhost:${port}/api/github/oauth/unknown-route` ); + expect(response.status).toEqual(404); + expect(response.headers.get("Content-Type")).toBe("application/json"); + await expect(response.json()).resolves.toEqual({ + error: "Unknown route: GET /api/github/oauth/unknown-route", + }); + server.close(); });