import { EventEmitter } from "node:events" ;
import type { IncomingMessage } from "node:http" ;
import { beforeEach, describe, expect, it, vi } from "vitest" ;
import { createMockServerResponse } from "../test-utils/mock-http-response.js" ;
import {
installRequestBodyLimitGuard,
isRequestBodyLimitError,
readJsonBodyWithLimit,
readRequestBodyWithLimit,
} from "./http-body.js" ;
type MockIncomingMessage = IncomingMessage & {
destroyed?: boolean ;
destroy: (error?: Error) => MockIncomingMessage;
__unhandledDestroyError?: unknown;
};
async function waitForMicrotaskTurn(): Promise<void > {
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 };
}
async function readJsonBody(params: {
chunks?: string[];
maxBytes: number;
emptyObjectOnEmpty?: boolean ;
}) {
const req = createMockRequest({ chunks: params.chunks });
return await readJsonBodyWithLimit(req, {
maxBytes: params.maxBytes,
...(params.emptyObjectOnEmpty === undefined
? {}
: { emptyObjectOnEmpty: params.emptyObjectOnEmpty }),
});
}
function createMockRequest(params: {
chunks?: string[];
headers?: Record<string, string>;
emitEnd?: boolean ;
}): MockIncomingMessage {
const req = new EventEmitter() as MockIncomingMessage;
req.destroyed = false ;
req.headers = params.headers ?? {};
req.destroy = ((error?: Error) => {
req.destroyed = true ;
if (error) {
// Simulate Node's async 'error' emission on destroy(err). If no listener is
// present at that time, EventEmitter throws; capture that as "unhandled".
queueMicrotask(() => {
try {
req.emit("error" , error);
} catch (err) {
req.__unhandledDestroyError = err;
}
});
}
return req;
}) as MockIncomingMessage["destroy" ];
if (params.chunks) {
void Promise.resolve().then(() => {
for (const chunk of params.chunks ?? []) {
req.emit("data" , Buffer.from(chunk, "utf-8" ));
if (req.destroyed) {
return ;
}
}
if (params.emitEnd !== false ) {
req.emit("end" );
}
});
}
return req;
}
describe("http body limits" , () => {
beforeEach(() => {
vi.useRealTimers();
});
it("reads body within max bytes" , async () => {
const req = createMockRequest({ chunks: ['{"ok":true}' ] });
await expect(readRequestBodyWithLimit(req, { maxBytes: 1024 })).resolves.toBe('{"ok":true}' );
});
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.each([
{
name: "returns json parse error when body is invalid" ,
params: { chunks: ["{bad json" ], maxBytes: 1024 , emptyObjectOnEmpty: false },
assertResult: (result: Awaited<ReturnType<typeof readJsonBody>>) => {
expect(result.ok).toBe(false );
if (!result.ok) {
expect(result.code).toBe("INVALID_JSON" );
}
},
},
{
name: "returns empty object for an empty body by default" ,
params: { chunks: [" " ], maxBytes: 1024 },
assertResult: (result: Awaited<ReturnType<typeof readJsonBody>>) => {
expect(result).toEqual({ ok: true , value: {} });
},
},
{
name: "returns payload-too-large for json body" ,
params: { chunks: ["x" .repeat(1024 )], maxBytes: 10 },
assertResult: (result: Awaited<ReturnType<typeof readJsonBody>>) => {
expect(result).toEqual({
ok: false ,
code: "PAYLOAD_TOO_LARGE" ,
error: "Payload too large" ,
});
},
},
])("$name" , async ({ params, assertResult }) => {
const result = await readJsonBody(params);
assertResult(result);
});
it.each([
{
name: "guard rejects oversized declared content-length" ,
headers: { "content-length" : "9999" },
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 } : {}),
});
expect(res.body).toBe(expectedBody);
});
it("timeout surfaces typed error when timeoutMs is clamped" , async () => {
const req = createMockRequest({ emitEnd: false });
const promise = readRequestBodyWithLimit(req, { maxBytes: 128 , timeoutMs: 0 });
await expect(promise).rejects.toSatisfy((error: unknown) =>
isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT" ),
);
expect(req.__unhandledDestroyError).toBeUndefined();
});
it("guard clamps invalid maxBytes to one byte" , async () => {
const { res } = await expectGuardPayloadTooLarge({
chunks: ["ab" ],
maxBytes: Number.NaN,
responseFormat: "text" ,
});
expect(res.body).toBe("Payload too large" );
});
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" ),
);
});
});
Messung V0.5 in Prozent C=97 H=95 G=95
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland