Skip to content

Commit d29808a

Browse files
committed
fix(commandboard-api): return 400 for malformed JSON bodies instead of 500
Malformed JSON on any POST endpoint previously bubbled a SyntaxError to the top-level handler, which mapped every throw to 500. Client input errors now respond 400 { "error": "Invalid JSON body" } via a typed InvalidJsonBodyError thrown from readJson(). Adds contract tests covering all four POST endpoints plus a control asserting schema-invalid (but well-formed) bodies still return 422. Fixes #11
1 parent 850cf5e commit d29808a

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

apps/commandboard-api/src/contract.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,34 @@ describe("CommandBoard API contracts", () => {
182182
expect(Array.isArray(body.errors)).toBe(true);
183183
});
184184
});
185+
186+
describe("invalid JSON request bodies", () => {
187+
const postEndpoints = [
188+
"/api/tasks",
189+
"/api/plugins/sh1pt/actions/publish",
190+
"/api/plugins/c0mpute/jobs/dispatch",
191+
"/api/plugins/c0mpute/quotes"
192+
];
193+
194+
it.each(postEndpoints)("returns 400 (not 500) for malformed JSON on %s", async (endpoint) => {
195+
const response = await fetch(`${baseUrl}${endpoint}`, {
196+
method: "POST",
197+
headers: { "content-type": "application/json" },
198+
body: "{invalid json"
199+
});
200+
const body = await response.json() as { error: string };
201+
202+
expect(response.status).toBe(400);
203+
expect(body).toEqual({ error: "Invalid JSON body" });
204+
});
205+
206+
it("still returns 422 for well-formed JSON that fails schema validation", async () => {
207+
const response = await fetch(`${baseUrl}/api/tasks`, {
208+
method: "POST",
209+
headers: { "content-type": "application/json" },
210+
body: JSON.stringify({ title: "missing required fields" })
211+
});
212+
213+
expect(response.status).toBe(422);
214+
});
215+
});

apps/commandboard-api/src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export function createCommandBoardServer() {
6060
try {
6161
await route(request, response);
6262
} catch (error) {
63+
if (error instanceof InvalidJsonBodyError) {
64+
json(response, 400, { error: error.message });
65+
return;
66+
}
6367
json(response, 500, { error: error instanceof Error ? error.message : String(error) });
6468
}
6569
});
@@ -281,13 +285,24 @@ function text(response: ServerResponse, status: number, contentType: string, bod
281285
response.end(body);
282286
}
283287

288+
class InvalidJsonBodyError extends Error {
289+
constructor() {
290+
super("Invalid JSON body");
291+
this.name = "InvalidJsonBodyError";
292+
}
293+
}
294+
284295
async function readJson(request: IncomingMessage) {
285296
const chunks: Buffer[] = [];
286297
for await (const chunk of request) {
287298
chunks.push(Buffer.from(chunk));
288299
}
289300

290-
return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
301+
try {
302+
return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
303+
} catch {
304+
throw new InvalidJsonBodyError();
305+
}
291306
}
292307

293308
function isRecord(value: unknown): value is Record<string, unknown> {

0 commit comments

Comments
 (0)