test: refine http body limit coverage

This commit is contained in:
Peter Steinberger 2026-03-13 18:13:39 +00:00
parent 29b9e21b7b
commit 584e3c2916
1 changed files with 101 additions and 45 deletions

View File

@ -19,6 +19,49 @@ async function waitForMicrotaskTurn(): Promise<void> {
await new Promise<void>((resolve) => queueMicrotask(resolve)); await new Promise<void>((resolve) => queueMicrotask(resolve));
} }
async function expectReadPayloadTooLarge(params: {
chunks?: string[];
headers?: Record<string, string>;
maxBytes: number;
}) {
const req = createMockRequest({
chunks: params.chunks,
headers: params.headers,
emitEnd: false,
});
await expect(readRequestBodyWithLimit(req, { maxBytes: params.maxBytes })).rejects.toMatchObject({
message: "PayloadTooLarge",
});
await waitForMicrotaskTurn();
expect(req.__unhandledDestroyError).toBeUndefined();
}
async function expectGuardPayloadTooLarge(params: {
chunks?: string[];
headers?: Record<string, string>;
maxBytes: number;
responseFormat?: "json" | "text";
responseText?: { PAYLOAD_TOO_LARGE?: string };
}) {
const req = createMockRequest({
chunks: params.chunks,
headers: params.headers,
emitEnd: false,
});
const res = createMockServerResponse();
const guard = installRequestBodyLimitGuard(req, res, {
maxBytes: params.maxBytes,
...(params.responseFormat ? { responseFormat: params.responseFormat } : {}),
...(params.responseText ? { responseText: params.responseText } : {}),
});
await waitForMicrotaskTurn();
expect(guard.isTripped()).toBe(true);
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
expect(res.statusCode).toBe(413);
expect(req.__unhandledDestroyError).toBeUndefined();
return { req, res, guard };
}
function createMockRequest(params: { function createMockRequest(params: {
chunks?: string[]; chunks?: string[];
headers?: Record<string, string>; headers?: Record<string, string>;
@ -66,12 +109,19 @@ describe("http body limits", () => {
await expect(readRequestBodyWithLimit(req, { maxBytes: 1024 })).resolves.toBe('{"ok":true}'); await expect(readRequestBodyWithLimit(req, { maxBytes: 1024 })).resolves.toBe('{"ok":true}');
}); });
it("rejects oversized body", async () => { it.each([
const req = createMockRequest({ chunks: ["x".repeat(512)] }); {
await expect(readRequestBodyWithLimit(req, { maxBytes: 64 })).rejects.toMatchObject({ name: "rejects oversized streamed body",
message: "PayloadTooLarge", chunks: ["x".repeat(512)],
}); maxBytes: 64,
expect(req.__unhandledDestroyError).toBeUndefined(); },
{
name: "declared oversized content-length does not emit unhandled error",
headers: { "content-length": "9999" },
maxBytes: 128,
},
])("$name", async ({ chunks, headers, maxBytes }) => {
await expectReadPayloadTooLarge({ chunks, headers, maxBytes });
}); });
it("returns json parse error when body is invalid", async () => { it("returns json parse error when body is invalid", async () => {
@ -83,34 +133,49 @@ describe("http body limits", () => {
} }
}); });
it("returns empty object for an empty body by default", async () => {
const req = createMockRequest({ chunks: [" "] });
const result = await readJsonBodyWithLimit(req, { maxBytes: 1024 });
expect(result).toEqual({ ok: true, value: {} });
});
it("returns payload-too-large for json body", async () => { it("returns payload-too-large for json body", async () => {
const req = createMockRequest({ chunks: ["x".repeat(1024)] }); const req = createMockRequest({ chunks: ["x".repeat(1024)] });
const result = await readJsonBodyWithLimit(req, { maxBytes: 10 }); const result = await readJsonBodyWithLimit(req, { maxBytes: 10 });
expect(result).toEqual({ ok: false, code: "PAYLOAD_TOO_LARGE", error: "Payload too large" }); expect(result).toEqual({ ok: false, code: "PAYLOAD_TOO_LARGE", error: "Payload too large" });
}); });
it("guard rejects oversized declared content-length", () => { it.each([
const req = createMockRequest({ {
name: "guard rejects oversized declared content-length",
headers: { "content-length": "9999" }, headers: { "content-length": "9999" },
emitEnd: false, maxBytes: 128,
expectedBody: '{"error":"Payload too large"}',
},
{
name: "guard rejects streamed oversized body",
chunks: ["small", "x".repeat(256)],
maxBytes: 128,
responseFormat: "text" as const,
expectedBody: "Payload too large",
},
{
name: "guard uses custom response text for payload-too-large",
chunks: ["small", "x".repeat(256)],
maxBytes: 128,
responseFormat: "text" as const,
responseText: { PAYLOAD_TOO_LARGE: "Too much" },
expectedBody: "Too much",
},
])("$name", async ({ chunks, headers, maxBytes, responseFormat, responseText, expectedBody }) => {
const { res } = await expectGuardPayloadTooLarge({
chunks,
headers,
maxBytes,
...(responseFormat ? { responseFormat } : {}),
...(responseText ? { responseText } : {}),
}); });
const res = createMockServerResponse(); expect(res.body).toBe(expectedBody);
const guard = installRequestBodyLimitGuard(req, res, { maxBytes: 128 });
expect(guard.isTripped()).toBe(true);
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
expect(res.statusCode).toBe(413);
});
it("guard rejects streamed oversized body", async () => {
const req = createMockRequest({ chunks: ["small", "x".repeat(256)], emitEnd: false });
const res = createMockServerResponse();
const guard = installRequestBodyLimitGuard(req, res, { maxBytes: 128, responseFormat: "text" });
await waitForMicrotaskTurn();
expect(guard.isTripped()).toBe(true);
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
expect(res.statusCode).toBe(413);
expect(res.body).toBe("Payload too large");
expect(req.__unhandledDestroyError).toBeUndefined();
}); });
it("timeout surfaces typed error when timeoutMs is clamped", async () => { it("timeout surfaces typed error when timeoutMs is clamped", async () => {
@ -123,29 +188,20 @@ describe("http body limits", () => {
}); });
it("guard clamps invalid maxBytes to one byte", async () => { it("guard clamps invalid maxBytes to one byte", async () => {
const req = createMockRequest({ chunks: ["ab"], emitEnd: false }); const { res } = await expectGuardPayloadTooLarge({
const res = createMockServerResponse(); chunks: ["ab"],
const guard = installRequestBodyLimitGuard(req, res, {
maxBytes: Number.NaN, maxBytes: Number.NaN,
responseFormat: "text", responseFormat: "text",
}); });
await waitForMicrotaskTurn(); expect(res.body).toBe("Payload too large");
expect(guard.isTripped()).toBe(true);
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
expect(res.statusCode).toBe(413);
expect(req.__unhandledDestroyError).toBeUndefined();
}); });
it("declared oversized content-length does not emit unhandled error", async () => { it("surfaces connection-closed as a typed limit error", async () => {
const req = createMockRequest({ const req = createMockRequest({ emitEnd: false });
headers: { "content-length": "9999" }, const promise = readRequestBodyWithLimit(req, { maxBytes: 128 });
emitEnd: false, queueMicrotask(() => req.emit("close"));
}); await expect(promise).rejects.toSatisfy((error: unknown) =>
await expect(readRequestBodyWithLimit(req, { maxBytes: 128 })).rejects.toMatchObject({ isRequestBodyLimitError(error, "CONNECTION_CLOSED"),
message: "PayloadTooLarge", );
});
// Wait a tick for any async destroy(err) emission.
await waitForMicrotaskTurn();
expect(req.__unhandledDestroyError).toBeUndefined();
}); });
}); });