Skip to content

Commit ea2a89d

Browse files
authored
fix: normalize trailing slashes (#1167)
* fix: strip-trailing-slashes * improve * no regex * make it a normalize function * fix prettier linting issue
1 parent 661edab commit ea2a89d

File tree

4 files changed

+88
-49
lines changed

4 files changed

+88
-49
lines changed

src/middleware/create-middleware.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { WebhookEventName } from "../generated/webhook-identifiers.ts";
22

33
import type { Webhooks } from "../index.ts";
4+
import { normalizeTrailingSlashes } from "../normalize-trailing-slashes.ts";
45
import type { WebhookEventHandlerError } from "../types.ts";
56
import type { MiddlewareOptions } from "./types.ts";
67

@@ -33,14 +34,19 @@ export function createMiddleware(options: CreateMiddlewareOptions) {
3334
webhooks: Webhooks,
3435
options: Required<MiddlewareOptions>,
3536
) {
37+
const middlewarePath = normalizeTrailingSlashes(options.path);
38+
3639
return async function octokitWebhooksMiddleware(
3740
request: IncomingMessage,
3841
response?: ServerResponse,
3942
next?: Function,
4043
) {
4144
let pathname: string;
4245
try {
43-
pathname = new URL(request.url as string, "http://localhost").pathname;
46+
pathname = new URL(
47+
normalizeTrailingSlashes(request.url) as string,
48+
"http://localhost",
49+
).pathname;
4450
} catch (error) {
4551
return handleResponse(
4652
JSON.stringify({
@@ -54,7 +60,7 @@ export function createMiddleware(options: CreateMiddlewareOptions) {
5460
);
5561
}
5662

57-
if (pathname !== options.path) {
63+
if (pathname !== middlewarePath) {
5864
next?.();
5965
return handleResponse(null);
6066
} else if (request.method !== "POST") {

src/normalize-trailing-slashes.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function normalizeTrailingSlashes(path: string) {
2+
let i = path.length;
3+
4+
if (i === 0) {
5+
return "/";
6+
}
7+
8+
while (i > 0) {
9+
if (path.charCodeAt(--i) !== 47 /* '/' */) {
10+
break;
11+
}
12+
}
13+
14+
if (i === -1) {
15+
return "/";
16+
}
17+
18+
return path.slice(0, i + 1);
19+
}

test/integration/middleware.test.ts

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -454,53 +454,6 @@ runtimes.forEach((runtimeCase) => {
454454
assert((await response.text()) === "still processing\n");
455455
});
456456

457-
it("Handles invalid URL", async () => {
458-
const webhooks = new Webhooks({
459-
secret: "mySecret",
460-
});
461-
462-
let middlewareWasRan: () => void;
463-
const untilMiddlewareIsRan = new Promise<void>(function (resolve) {
464-
middlewareWasRan = resolve;
465-
});
466-
const actualMiddleware = createNodeMiddleware(webhooks);
467-
const mockedMiddleware = async function (
468-
...[req, ...rest]: Parameters<typeof actualMiddleware>
469-
) {
470-
req.url = "//";
471-
await actualMiddleware(req, ...rest);
472-
middlewareWasRan();
473-
};
474-
475-
const server = createServer(mockedMiddleware).listen(await getPort());
476-
477-
const { port } = server.address() as AddressInfo;
478-
479-
const response = await fetch(
480-
`http://localhost:${port}/api/github/webhooks`,
481-
{
482-
method: "POST",
483-
headers: {
484-
"Content-Type": "application/json",
485-
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
486-
"X-GitHub-Event": "push",
487-
"X-Hub-Signature-256": signatureSha256,
488-
},
489-
body: pushEventPayload,
490-
},
491-
);
492-
493-
await untilMiddlewareIsRan;
494-
495-
assert(response.status === 422);
496-
assert(
497-
(await response.text()) ===
498-
'{"error":"Request URL could not be parsed: //"}',
499-
);
500-
501-
server.close();
502-
});
503-
504457
it("Handles invalid signature", async () => {
505458
const webhooks = new Webhooks({
506459
secret: "mySecret",
@@ -591,5 +544,45 @@ runtimes.forEach((runtimeCase) => {
591544

592545
closeTestServer();
593546
});
547+
548+
["/", "/api/github/webhooks"].forEach((webhooksPath) => {
549+
["", "/", "//", "///"].forEach((trailing) => {
550+
it(`Handles trailing slashes - webhooksPath "${webhooksPath}" with trailing "${trailing}"`, async () => {
551+
const webhooks = new Webhooks({
552+
secret: "mySecret",
553+
});
554+
555+
webhooks.on("push", (event) => {
556+
assert(event.id === "123e4567-e89b-12d3-a456-426655440000");
557+
});
558+
559+
const { port, closeTestServer } = await instantiateTestServer(
560+
runtime,
561+
target,
562+
webhooks,
563+
{ path: webhooksPath },
564+
);
565+
566+
const response = await fetch(
567+
`http://localhost:${port}${webhooksPath}${trailing}`,
568+
{
569+
method: "POST",
570+
headers: {
571+
"Content-Type": "application/json",
572+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
573+
"X-GitHub-Event": "push",
574+
"X-Hub-Signature-256": signatureSha256,
575+
},
576+
body: pushEventPayload,
577+
},
578+
);
579+
580+
assert(response.status === 200);
581+
assert((await response.text()) === "ok\n");
582+
583+
closeTestServer();
584+
});
585+
});
586+
});
594587
});
595588
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, it, assert } from "../testrunner.ts";
2+
import { normalizeTrailingSlashes } from "../../src/normalize-trailing-slashes.ts";
3+
4+
describe("normalizeTrailingSlashes", () => {
5+
[
6+
["", "/"],
7+
["/", "/"],
8+
["//", "/"],
9+
["///", "/"],
10+
["/a", "/a"],
11+
["/a/", "/a"],
12+
["/a//", "/a"],
13+
].forEach(([actual, expected]) => {
14+
it(`should normalize trailing slashes from ${actual} to ${expected}`, () => {
15+
assert(
16+
normalizeTrailingSlashes(actual) === expected,
17+
`Expected ${actual} to be stripped to ${expected}, but got ${normalizeTrailingSlashes(actual)}`,
18+
);
19+
});
20+
});
21+
});

0 commit comments

Comments
 (0)