diff --git a/apps/commandboard-api/src/contract.test.ts b/apps/commandboard-api/src/contract.test.ts index 2b9629d..ec2048e 100644 --- a/apps/commandboard-api/src/contract.test.ts +++ b/apps/commandboard-api/src/contract.test.ts @@ -181,4 +181,16 @@ describe("CommandBoard API contracts", () => { expect(response.status).toBe(422); expect(Array.isArray(body.errors)).toBe(true); }); + + it("rejects malformed JSON request bodies as client errors", async () => { + const response = await fetch(`${baseUrl}/api/tasks`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{bad" + }); + const body = await response.json() as { error: string }; + + expect(response.status).toBe(400); + expect(body).toEqual({ error: "Invalid JSON body" }); + }); }); diff --git a/apps/commandboard-api/src/index.ts b/apps/commandboard-api/src/index.ts index 58b6166..3e58460 100644 --- a/apps/commandboard-api/src/index.ts +++ b/apps/commandboard-api/src/index.ts @@ -55,11 +55,22 @@ const c0mputeWorkers = [ { id: "worker_pool_1", region: "us-west", status: "preview", capacity: "wip" } ]; +class InvalidJsonBodyError extends Error { + constructor() { + super("Invalid JSON body"); + this.name = "InvalidJsonBodyError"; + } +} + export function createCommandBoardServer() { return createServer(async (request, response) => { try { await route(request, response); } catch (error) { + if (error instanceof InvalidJsonBodyError) { + json(response, 400, { error: error.message }); + return; + } json(response, 500, { error: error instanceof Error ? error.message : String(error) }); } }); @@ -287,7 +298,14 @@ async function readJson(request: IncomingMessage) { chunks.push(Buffer.from(chunk)); } - return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown; + try { + return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown; + } catch (error) { + if (error instanceof SyntaxError) { + throw new InvalidJsonBodyError(); + } + throw error; + } } function isRecord(value: unknown): value is Record {