-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: createNodeMiddleware()
#509
Changes from all commits
4973ecc
c80480c
5a33184
7e8813d
d615cf9
7921a10
1579c59
bd1358e
c5ca127
ec736ac
6d2b594
107de04
89f202c
dc800a2
f000384
3ade8b3
7c4b8ba
7176f3a
35b96d2
a41def2
4ee5a6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { IncomingMessage } from "http"; | ||
|
||
const WEBHOOK_HEADERS = [ | ||
"x-github-event", | ||
"x-hub-signature-256", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I intentionally left out the |
||
"x-github-delivery", | ||
]; | ||
|
||
// https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#delivery-headers | ||
export function getMissingHeaders(request: IncomingMessage) { | ||
return WEBHOOK_HEADERS.filter((header) => !(header in request.headers)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { WebhookEvent } from "@octokit/webhooks-definitions/schema"; | ||
// @ts-ignore to address #245 | ||
import AggregateError from "aggregate-error"; | ||
import { IncomingMessage } from "http"; | ||
|
||
declare module "http" { | ||
interface IncomingMessage { | ||
body?: WebhookEvent; | ||
} | ||
} | ||
|
||
export function getPayload(request: IncomingMessage): Promise<WebhookEvent> { | ||
// If request.body already exists we can stop here | ||
// See https://github.com/octokit/webhooks.js/pull/23 | ||
|
||
if (request.body) return Promise.resolve(request.body); | ||
|
||
return new Promise((resolve, reject) => { | ||
let data = ""; | ||
|
||
request.setEncoding("utf8"); | ||
|
||
// istanbul ignore next | ||
request.on("error", (error) => reject(new AggregateError([error]))); | ||
request.on("data", (chunk) => (data += chunk)); | ||
request.on("end", () => { | ||
try { | ||
resolve(JSON.parse(data)); | ||
} catch (error) { | ||
error.message = "Invalid JSON"; | ||
error.status = 400; | ||
reject(new AggregateError([error])); | ||
} | ||
}); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { createLogger } from "../../createLogger"; | ||
import { Webhooks } from "../../index"; | ||
import { middleware } from "./middleware"; | ||
import { onUnhandledRequestDefault } from "./on-unhandled-request-default"; | ||
import { MiddlewareOptions } from "./types"; | ||
|
||
export function createNodeMiddleware( | ||
webhooks: Webhooks, | ||
{ | ||
path = "/api/github/webhooks", | ||
onUnhandledRequest = onUnhandledRequestDefault, | ||
log = createLogger(), | ||
}: MiddlewareOptions = {} | ||
) { | ||
return middleware.bind(null, webhooks, { | ||
path, | ||
onUnhandledRequest, | ||
log, | ||
} as Required<MiddlewareOptions>); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { IncomingMessage, ServerResponse } from "http"; | ||
|
||
import { WebhookEventName } from "@octokit/webhooks-definitions/schema"; | ||
|
||
import { Webhooks } from "../../index"; | ||
import { WebhookEventHandlerError } from "../../types"; | ||
import { MiddlewareOptions } from "./types"; | ||
import { onUnhandledRequestDefault } from "./on-unhandled-request-default"; | ||
import { getMissingHeaders } from "./get-missing-headers"; | ||
import { getPayload } from "./get-payload"; | ||
|
||
export async function middleware( | ||
webhooks: Webhooks, | ||
options: Required<MiddlewareOptions>, | ||
request: IncomingMessage, | ||
response: ServerResponse, | ||
next?: Function | ||
) { | ||
const { pathname } = new URL(request.url as string, "http://localhost"); | ||
|
||
const isUnknownRoute = request.method !== "POST" || pathname !== options.path; | ||
const isExpressMiddleware = typeof next === "function"; | ||
if (!isExpressMiddleware && isUnknownRoute) { | ||
options.log.debug(`not found: ${request.method} ${request.url}`); | ||
return onUnhandledRequestDefault(request, response); | ||
} | ||
|
||
const missingHeaders = getMissingHeaders(request).join(", "); | ||
|
||
if (missingHeaders) { | ||
response.writeHead(400, { | ||
"content-type": "application/json", | ||
}); | ||
response.end( | ||
JSON.stringify({ | ||
error: `Required headers missing: ${missingHeaders}`, | ||
}) | ||
); | ||
|
||
return; | ||
} | ||
|
||
const eventName = request.headers["x-github-event"] as WebhookEventName; | ||
const signatureSHA256 = request.headers["x-hub-signature-256"] as string; | ||
const id = request.headers["x-github-delivery"] as string; | ||
|
||
options.log.debug(`${eventName} event received (id: ${id})`); | ||
|
||
// GitHub will abort the request if it does not receive a response within 10s | ||
// See https://github.com/octokit/webhooks.js/issues/185 | ||
let didTimeout = false; | ||
const timeout = setTimeout(() => { | ||
didTimeout = true; | ||
response.statusCode = 202; | ||
response.end("still processing\n"); | ||
}, 9000).unref(); | ||
|
||
try { | ||
const payload = await getPayload(request); | ||
|
||
await webhooks.verifyAndReceive({ | ||
id: id, | ||
name: eventName as any, | ||
payload: payload as any, | ||
signature: signatureSHA256, | ||
}); | ||
clearTimeout(timeout); | ||
|
||
if (didTimeout) return; | ||
|
||
response.end("ok\n"); | ||
} catch (error) { | ||
clearTimeout(timeout); | ||
|
||
if (didTimeout) return; | ||
|
||
const statusCode = Array.from(error as WebhookEventHandlerError)[0].status; | ||
response.statusCode = typeof statusCode !== "undefined" ? statusCode : 500; | ||
response.end(error.toString()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { IncomingMessage, ServerResponse } from "http"; | ||
|
||
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}`, | ||
}) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { IncomingMessage, ServerResponse } from "http"; | ||
|
||
import { Logger } from "../../createLogger"; | ||
|
||
export type MiddlewareOptions = { | ||
path?: string; | ||
log?: Logger; | ||
onUnhandledRequest?: ( | ||
request: IncomingMessage, | ||
response: ServerResponse | ||
) => void; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't strictly related to this PR, but I get the changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah sorry for that, you are totally right, I've been lazy creating separate PRs for that. Same with the formatting changes in the README, I've to show
octokit
on Wednesday, I'm in a bit of a rush