mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Apply safeJson's opt-in maxBytes ceiling to the body-accepting batch and export routes that lacked one. The cap measures the raw text before JSON.parse, so an over-limit payload returns a clean 413 without buffering onto the heap. - workouts/batch: reuse the existing 5 MB ceiling so chunked uploads with no Content-Length are bounded too. - measurements/batch: 4 MB — an order of magnitude above a 500-entry batch's few-hundred-KB worst case. - export/health-record: 64 KB — the selection payload is small and bounded. Each cap is a DoS ceiling sized well above any legitimate payload, never a tight bound on a real batch.
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { safeJson } from "../api-response";
|
|
|
|
// `safeJson`'s opt-in `maxBytes` cap is the DoS ceiling the batch /
|
|
// export ingest routes lean on. The check runs against the raw text
|
|
// before `JSON.parse`, so an over-limit body never lands in heap and
|
|
// the caller gets a clean 413 envelope.
|
|
|
|
function jsonRequest(body: string): Request {
|
|
return new Request("https://example.test/api/x", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body,
|
|
});
|
|
}
|
|
|
|
describe("safeJson maxBytes", () => {
|
|
it("parses a body at or under the cap", async () => {
|
|
const payload = JSON.stringify({ entries: [1, 2, 3] });
|
|
const result = await safeJson(jsonRequest(payload), {
|
|
maxBytes: 1024,
|
|
});
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.data).toEqual({ entries: [1, 2, 3] });
|
|
});
|
|
|
|
it("rejects an over-limit body with a 413 envelope", async () => {
|
|
const payload = JSON.stringify({ blob: "x".repeat(2048) });
|
|
const result = await safeJson(jsonRequest(payload), {
|
|
maxBytes: 1024,
|
|
});
|
|
expect(result.data).toBeUndefined();
|
|
expect(result.error).toBeDefined();
|
|
expect(result.error!.status).toBe(413);
|
|
|
|
const body = (await result.error!.json()) as {
|
|
data: null;
|
|
error: string;
|
|
};
|
|
expect(body.data).toBeNull();
|
|
expect(body.error).toContain("1024");
|
|
});
|
|
|
|
it("returns 400 on invalid JSON within the cap", async () => {
|
|
const result = await safeJson(jsonRequest("{not json"), {
|
|
maxBytes: 1024,
|
|
});
|
|
expect(result.data).toBeUndefined();
|
|
expect(result.error!.status).toBe(400);
|
|
});
|
|
|
|
it("still rejects a non-JSON content-type before measuring bytes", async () => {
|
|
const request = new Request("https://example.test/api/x", {
|
|
method: "POST",
|
|
headers: { "content-type": "text/plain" },
|
|
body: "x".repeat(4096),
|
|
});
|
|
const result = await safeJson(request, { maxBytes: 1024 });
|
|
expect(result.error!.status).toBe(415);
|
|
});
|
|
|
|
it("parses unbounded when maxBytes is omitted", async () => {
|
|
const payload = JSON.stringify({ ok: true });
|
|
const result = await safeJson(jsonRequest(payload));
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.data).toEqual({ ok: true });
|
|
});
|
|
});
|