diff --git a/src/infra/http-body.test.ts b/src/infra/http-body.test.ts index bfb14b92dca..b80169a0602 100644 --- a/src/infra/http-body.test.ts +++ b/src/infra/http-body.test.ts @@ -19,6 +19,49 @@ async function waitForMicrotaskTurn(): Promise { await new Promise((resolve) => queueMicrotask(resolve)); } +async function expectReadPayloadTooLarge(params: { + chunks?: string[]; + headers?: Record; + 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; + 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: { chunks?: string[]; headers?: Record; @@ -66,12 +109,19 @@ describe("http body limits", () => { await expect(readRequestBodyWithLimit(req, { maxBytes: 1024 })).resolves.toBe('{"ok":true}'); }); - it("rejects oversized body", async () => { - const req = createMockRequest({ chunks: ["x".repeat(512)] }); - await expect(readRequestBodyWithLimit(req, { maxBytes: 64 })).rejects.toMatchObject({ - message: "PayloadTooLarge", - }); - expect(req.__unhandledDestroyError).toBeUndefined(); + it.each([ + { + name: "rejects oversized streamed body", + chunks: ["x".repeat(512)], + maxBytes: 64, + }, + { + 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 () => { @@ -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 () => { const req = createMockRequest({ chunks: ["x".repeat(1024)] }); const result = await readJsonBodyWithLimit(req, { maxBytes: 10 }); expect(result).toEqual({ ok: false, code: "PAYLOAD_TOO_LARGE", error: "Payload too large" }); }); - it("guard rejects oversized declared content-length", () => { - const req = createMockRequest({ + it.each([ + { + name: "guard rejects oversized declared content-length", 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(); - 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(); + expect(res.body).toBe(expectedBody); }); 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 () => { - const req = createMockRequest({ chunks: ["ab"], emitEnd: false }); - const res = createMockServerResponse(); - const guard = installRequestBodyLimitGuard(req, res, { + const { res } = await expectGuardPayloadTooLarge({ + chunks: ["ab"], maxBytes: Number.NaN, responseFormat: "text", }); - await waitForMicrotaskTurn(); - expect(guard.isTripped()).toBe(true); - expect(guard.code()).toBe("PAYLOAD_TOO_LARGE"); - expect(res.statusCode).toBe(413); - expect(req.__unhandledDestroyError).toBeUndefined(); + expect(res.body).toBe("Payload too large"); }); - it("declared oversized content-length does not emit unhandled error", async () => { - const req = createMockRequest({ - headers: { "content-length": "9999" }, - emitEnd: false, - }); - await expect(readRequestBodyWithLimit(req, { maxBytes: 128 })).rejects.toMatchObject({ - message: "PayloadTooLarge", - }); - // Wait a tick for any async destroy(err) emission. - await waitForMicrotaskTurn(); - expect(req.__unhandledDestroyError).toBeUndefined(); + it("surfaces connection-closed as a typed limit error", async () => { + const req = createMockRequest({ emitEnd: false }); + const promise = readRequestBodyWithLimit(req, { maxBytes: 128 }); + queueMicrotask(() => req.emit("close")); + await expect(promise).rejects.toSatisfy((error: unknown) => + isRequestBodyLimitError(error, "CONNECTION_CLOSED"), + ); }); });