Skip to content

Commit fe00834

Browse files
authored
fix: prevent splitting multibyte characters in getPayload() (#939)
If you expect that the payload is using utf8 or utf16 or any other multybyte encoding, then you have to concat the buffers first and in the final step you can serialize the buffer to a string with the desired encoding. The issue will arise, when with some "luck" a multibyte character is split between two chunks.
1 parent c328004 commit fe00834

File tree

3 files changed

+91
-8
lines changed

3 files changed

+91
-8
lines changed

src/middleware/node/get-payload.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ export function getPayload(request: IncomingMessage): Promise<string> {
1818
if (request.body) return Promise.resolve(request.body);
1919

2020
return new Promise((resolve, reject) => {
21-
let data = "";
21+
let data: Buffer[] = [];
2222

23-
request.setEncoding("utf8");
24-
25-
// istanbul ignore next
2623
request.on("error", (error: Error) => reject(new AggregateError([error])));
27-
request.on("data", (chunk: string) => (data += chunk));
28-
request.on("end", () => resolve(data));
24+
request.on("data", (chunk: Buffer) => data.push(chunk));
25+
request.on("end", () =>
26+
// setImmediate improves the throughput by reducing the pressure from
27+
// the event loop
28+
setImmediate(
29+
resolve,
30+
data.length === 1
31+
? data[0].toString("utf8")
32+
: Buffer.concat(data).toString("utf8"),
33+
),
34+
);
2935
});
3036
}

test/integration/get-payload.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import EventEmitter from "node:events";
2+
import { getPayload } from "../../src/middleware/node/get-payload.ts";
3+
4+
describe("getPayload", () => {
5+
it("returns a promise", () => {
6+
const request = new EventEmitter();
7+
const promise = getPayload(request);
8+
9+
expect(promise).toBeInstanceOf(Promise);
10+
});
11+
12+
it("resolves with a string when only receiving no chunk", async () => {
13+
const request = new EventEmitter();
14+
const promise = getPayload(request);
15+
16+
request.emit("end");
17+
18+
expect(await promise).toEqual("");
19+
});
20+
21+
it("resolves with a string when only receiving one chunk", async () => {
22+
const request = new EventEmitter();
23+
const promise = getPayload(request);
24+
25+
request.emit("data", Buffer.from("foobar"));
26+
request.emit("end");
27+
28+
expect(await promise).toEqual("foobar");
29+
});
30+
31+
it("resolves with a string when receiving multiple chunks", async () => {
32+
const request = new EventEmitter();
33+
const promise = getPayload(request);
34+
35+
request.emit("data", Buffer.from("foo"));
36+
request.emit("data", Buffer.from("bar"));
37+
request.emit("end");
38+
39+
expect(await promise).toEqual("foobar");
40+
});
41+
42+
it("rejects with an error", async () => {
43+
const request = new EventEmitter();
44+
const promise = getPayload(request);
45+
46+
request.emit("error", new Error("test"));
47+
48+
await expect(promise).rejects.toThrow("test");
49+
});
50+
51+
it("resolves with a string with respecting the utf-8 encoding", async () => {
52+
const request = new EventEmitter();
53+
const promise = getPayload(request);
54+
55+
const doubleByteBuffer = Buffer.from("ݔ");
56+
request.emit("data", doubleByteBuffer.subarray(0, 1));
57+
request.emit("data", doubleByteBuffer.subarray(1, 2));
58+
request.emit("end");
59+
60+
expect(await promise).toEqual("ݔ");
61+
});
62+
63+
it("resolves with the body, if passed via the request", async () => {
64+
const request = new EventEmitter();
65+
// @ts-ignore body is not part of EventEmitter, which we are using
66+
// to mock the request object
67+
request.body = "foo";
68+
69+
const promise = getPayload(request);
70+
71+
// we emit data, to ensure that the body attribute is preferred
72+
request.emit("data", "bar");
73+
request.emit("end");
74+
75+
expect(await promise).toEqual("foo");
76+
});
77+
});

test/integration/node-middleware.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ describe("createNodeMiddleware(webhooks)", () => {
372372
});
373373

374374
test("Handles timeout", async () => {
375-
jest.useFakeTimers();
375+
jest.useFakeTimers({ doNotFake: ["setImmediate"] });
376376

377377
const webhooks = new Webhooks({
378378
secret: "mySecret",
@@ -407,7 +407,7 @@ describe("createNodeMiddleware(webhooks)", () => {
407407
});
408408

409409
test("Handles timeout with error", async () => {
410-
jest.useFakeTimers();
410+
jest.useFakeTimers({ doNotFake: ["setImmediate"] });
411411

412412
const webhooks = new Webhooks({
413413
secret: "mySecret",

0 commit comments

Comments
 (0)