Skip to content

Commit 553b1d4

Browse files
committed
feat(middleware): original request passed down + regExp path match
1 parent 053fa19 commit 553b1d4

File tree

6 files changed

+160
-22
lines changed

6 files changed

+160
-22
lines changed

README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const webhooks = new Webhooks({
4848
secret: "mysecret",
4949
});
5050

51-
webhooks.onAny(({ id, name, payload }) => {
51+
webhooks.onAny(({ id, name, payload, extraData }) => {
5252
console.log(name, "event received");
5353
});
5454

@@ -223,7 +223,7 @@ The `verify` method can be imported as static method from [`@octokit/webhooks-me
223223
### webhooks.verifyAndReceive()
224224

225225
```js
226-
webhooks.verifyAndReceive({ id, name, payload, signature });
226+
webhooks.verifyAndReceive({ id, name, payload, extraData, signature });
227227
```
228228

229229
<table width="100%">
@@ -316,7 +316,7 @@ eventHandler
316316
### webhooks.receive()
317317

318318
```js
319-
webhooks.receive({ id, name, payload });
319+
webhooks.receive({ id, name, payload, extraData });
320320
```
321321

322322
<table width="100%">
@@ -370,6 +370,8 @@ Returns a promise. Runs all handlers set with [`webhooks.on()`](#webhookson) in
370370

371371
The `.receive()` method belongs to the `event-handler` module which can be used [standalone](src/event-handler/).
372372

373+
The `extraData` is an optional parameter, if it is set, it will be available in the `on` functions.
374+
373375
### webhooks.on()
374376

375377
```js
@@ -420,7 +422,7 @@ webhooks.on(eventNames, handler);
420422
<strong>Required.</strong>
421423
Method to be run each time the event with the passed name is received.
422424
the <code>handler</code> function can be an async function, throw an error or
423-
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
425+
return a Promise. The handler is called with an event object: <code>{id, name, payload, extraData}</code>.
424426
</td>
425427
</tr>
426428
</tbody>
@@ -449,7 +451,7 @@ webhooks.onAny(handler);
449451
<strong>Required.</strong>
450452
Method to be run each time any event is received.
451453
the <code>handler</code> function can be an async function, throw an error or
452-
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
454+
return a Promise. The handler is called with an event object: <code>{id, name, payload, extraData}</code>.
453455
</td>
454456
</tr>
455457
</tbody>
@@ -482,7 +484,7 @@ Asynchronous `error` event handler are not blocking the `.receive()` method from
482484
<strong>Required.</strong>
483485
Method to be run each time a webhook event handler throws an error or returns a promise that rejects.
484486
The <code>handler</code> function can be an async function,
485-
return a Promise. The handler is called with an error object that has a .event property which has all the information on the event: <code>{id, name, payload}</code>.
487+
return a Promise. The handler is called with an error object that has a .event property which has all the information on the event: <code>{id, name, payload, extraData}</code>.
486488
</td>
487489
</tr>
488490
</tbody>
@@ -579,21 +581,32 @@ createServer(middleware).listen(3000);
579581
<td>
580582
<code>path</code>
581583
<em>
582-
string
584+
string | RegEx
583585
</em>
584586
</td>
585587
<td>
586588
Custom path to match requests against. Defaults to <code>/api/github/webhooks</code>.
587-
</td>
588-
</tr>
589-
<tr>
590-
<td>
591-
<code>log</code>
592-
<em>
593-
object
594-
</em>
595-
</td>
596-
<td>
589+
590+
Can be used as a regular expression;
591+
592+
```js
593+
const middleware = createNodeMiddleware(webhooks, {
594+
path: /^\/api\/github\/webhooks/,
595+
});
596+
```
597+
598+
Test the regex before usage, the `g` and `y` flags [makes it stateful](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)!
599+
600+
</td>
601+
</tr>
602+
<tr>
603+
<td>
604+
<code>log</code>
605+
<em>
606+
object
607+
</em>
608+
</td>
609+
<td>
597610

598611
Used for internal logging. Defaults to [`console`](https://developer.mozilla.org/en-US/docs/Web/API/console) with `debug` and `info` doing nothing.
599612

@@ -721,7 +734,7 @@ A union of all possible events and event/action combinations supported by the ev
721734

722735
### `EmitterWebhookEvent`
723736

724-
The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties.
737+
The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties, with an optional `extraData`.
725738
An optional generic parameter can be passed to narrow the type of the `name` and `payload` properties based on event names or event/action combinations, e.g. `EmitterWebhookEvent<"check_run" | "code_scanning_alert.fixed">`.
726739

727740
## License

src/middleware/node/middleware.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ export async function middleware(
3333
);
3434
return;
3535
}
36-
37-
const isUnknownRoute = request.method !== "POST" || pathname !== options.path;
36+
const pathMatch =
37+
options.path instanceof RegExp
38+
? options.path.test(pathname)
39+
: pathname === options.path;
40+
const isUnknownRoute = request.method !== "POST" || !pathMatch;
3841
const isExpressMiddleware = typeof next === "function";
3942
if (isUnknownRoute) {
4043
if (isExpressMiddleware) {
@@ -72,7 +75,12 @@ export async function middleware(
7275
didTimeout = true;
7376
response.statusCode = 202;
7477
response.end("still processing\n");
75-
}, 9000).unref();
78+
}, 9000);
79+
80+
/* istanbul ignore else */
81+
if (typeof timeout.unref === "function") {
82+
timeout.unref();
83+
}
7684

7785
try {
7886
const payload = await getPayload(request);
@@ -82,6 +90,7 @@ export async function middleware(
8290
name: eventName as any,
8391
payload: payload as any,
8492
signature: signatureSHA256,
93+
extraData: request,
8594
});
8695
clearTimeout(timeout);
8796

src/middleware/node/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type ServerResponse = any;
77
import { Logger } from "../../createLogger";
88

99
export type MiddlewareOptions = {
10-
path?: string;
10+
path?: string | RegExp;
1111
log?: Logger;
1212
onUnhandledRequest?: (
1313
request: IncomingMessage,

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type EmitterWebhookEvent<
1212
> = TEmitterEvent extends `${infer TWebhookEvent}.${infer TAction}`
1313
? BaseWebhookEvent<Extract<TWebhookEvent, WebhookEventName>> & {
1414
payload: { action: TAction };
15+
} & {
16+
extraData?: any;
1517
}
1618
: BaseWebhookEvent<Extract<TEmitterEvent, WebhookEventName>>;
1719

@@ -20,6 +22,7 @@ export type EmitterWebhookEventWithStringPayloadAndSignature = {
2022
name: EmitterWebhookEventName;
2123
payload: string;
2224
signature: string;
25+
extraData?: any;
2326
};
2427

2528
export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & {
@@ -30,6 +33,7 @@ interface BaseWebhookEvent<TName extends WebhookEventName> {
3033
id: string;
3134
name: TName;
3235
payload: WebhookEventMap[TName];
36+
extraData?: any;
3337
}
3438

3539
export interface Options<TTransformed = unknown> {

src/verify-and-receive.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ export async function verifyAndReceive(
3939
typeof event.payload === "string"
4040
? JSON.parse(event.payload)
4141
: event.payload,
42+
extraData: event.extraData,
4243
});
4344
}

test/integration/node-middleware.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,117 @@ describe("createNodeMiddleware(webhooks)", () => {
6262
server.close();
6363
});
6464

65+
test("path match with regex", async () => {
66+
expect.assertions(7);
67+
68+
const webhooks = new Webhooks({
69+
secret: "mySecret",
70+
});
71+
72+
const server = createServer(
73+
createNodeMiddleware(webhooks, {
74+
path: /^\/api\/github\/webhooks/,
75+
})
76+
).listen();
77+
78+
// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
79+
const { port } = server.address();
80+
81+
webhooks.on("push", (event) => {
82+
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
83+
});
84+
85+
const response1 = await fetch(
86+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
87+
{
88+
method: "POST",
89+
headers: {
90+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
91+
"X-GitHub-Event": "push",
92+
"X-Hub-Signature-256": signatureSha256,
93+
},
94+
body: pushEventPayload,
95+
}
96+
);
97+
98+
expect(response1.status).toEqual(200);
99+
await expect(response1.text()).resolves.toBe("ok\n");
100+
101+
const response2 = await fetch(
102+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
103+
{
104+
method: "POST",
105+
headers: {
106+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
107+
"X-GitHub-Event": "push",
108+
"X-Hub-Signature-256": signatureSha256,
109+
},
110+
body: pushEventPayload,
111+
}
112+
);
113+
114+
expect(response2.status).toEqual(200);
115+
await expect(response2.text()).resolves.toBe("ok\n");
116+
117+
const response3 = await fetch(`http://localhost:${port}/api/github/web`, {
118+
method: "POST",
119+
headers: {
120+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
121+
"X-GitHub-Event": "push",
122+
"X-Hub-Signature-256": signatureSha256,
123+
},
124+
body: pushEventPayload,
125+
});
126+
127+
expect(response3.status).toEqual(404);
128+
129+
server.close();
130+
});
131+
132+
test("original request passed by as intended", async () => {
133+
expect.assertions(6);
134+
135+
const webhooks = new Webhooks({
136+
secret: "mySecret",
137+
});
138+
139+
const server = createServer(
140+
createNodeMiddleware(webhooks, {
141+
path: /^\/api\/github\/webhooks/,
142+
})
143+
).listen();
144+
145+
// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
146+
const { port } = server.address();
147+
148+
webhooks.on("push", (event) => {
149+
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
150+
const r = event.extraData;
151+
expect(r).toBeDefined();
152+
expect(r?.headers["my-custom-header"]).toBe("customHeader");
153+
expect(r?.url).toBe(`/api/github/webhooks/0001/testurl`);
154+
});
155+
156+
const response = await fetch(
157+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
158+
{
159+
method: "POST",
160+
headers: {
161+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
162+
"X-GitHub-Event": "push",
163+
"X-Hub-Signature-256": signatureSha256,
164+
"my-custom-header": "customHeader",
165+
},
166+
body: pushEventPayload,
167+
}
168+
);
169+
170+
expect(response.status).toEqual(200);
171+
await expect(response.text()).resolves.toBe("ok\n");
172+
173+
server.close();
174+
});
175+
65176
test("request.body already parsed (e.g. Lambda)", async () => {
66177
expect.assertions(3);
67178

0 commit comments

Comments
 (0)