Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
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"),
);
});
});
¤ Dauer der Verarbeitung: 0.19 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|