From 743422217e4e93e743edd68202b908e550abab2e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 9 Jul 2026 15:40:27 +0100 Subject: [PATCH] fix(qa): normalize malformed JSON errors (#102830) Co-authored-by: ZengWen-DT --- extensions/qa-lab/src/bus-server.test.ts | 13 +++++++++++++ extensions/qa-lab/src/bus-server.ts | 21 ++++++++++++++++++++- extensions/qa-lab/src/lab-server.test.ts | 22 +++++++++++++++++++++- extensions/qa-lab/src/lab-server.ts | 5 +++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/extensions/qa-lab/src/bus-server.test.ts b/extensions/qa-lab/src/bus-server.test.ts index 9a21894d872..c688d239307 100644 --- a/extensions/qa-lab/src/bus-server.test.ts +++ b/extensions/qa-lab/src/bus-server.test.ts @@ -172,6 +172,19 @@ describe("qa-bus server", () => { }); }); + it("returns a controlled error when a v1 POST body contains malformed JSON", async () => { + const state = createQaBusState(); + const bus = await startQaBusServer({ state }); + stops.push(bus["stop"]); + + const response = await postQaBusRawJson(bus.baseUrl, "/v1/reset", "{"); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + error: "Malformed JSON body", + }); + }); + it("keeps oversized numeric poll and search fields bounded", async () => { const state = createQaBusState(); const bus = await startQaBusServer({ state }); diff --git a/extensions/qa-lab/src/bus-server.ts b/extensions/qa-lab/src/bus-server.ts index 0c2e6adf800..be9d9c7f0d3 100644 --- a/extensions/qa-lab/src/bus-server.ts +++ b/extensions/qa-lab/src/bus-server.ts @@ -26,6 +26,18 @@ const QA_HTTP_JSON_BODY_TIMEOUT_MS = 5_000; const QA_BUS_POLL_TIMEOUT_MAX_MS = 30_000; const QA_BUS_POLL_LIMIT_MAX = 500; const QA_BUS_SEARCH_LIMIT_MAX = 100; +const QA_MALFORMED_JSON_BODY_MESSAGE = "Malformed JSON body"; + +class QaMalformedJsonBodyError extends Error { + constructor() { + super(QA_MALFORMED_JSON_BODY_MESSAGE); + this.name = "QaMalformedJsonBodyError"; + } +} + +export function isQaMalformedJsonBodyError(error: unknown): error is Error { + return error instanceof QaMalformedJsonBodyError; +} export async function readQaJsonBody(req: IncomingMessage): Promise { const text = ( @@ -34,7 +46,14 @@ export async function readQaJsonBody(req: IncomingMessage): Promise { timeoutMs: QA_HTTP_JSON_BODY_TIMEOUT_MS, }) ).trim(); - return text ? (JSON.parse(text) as unknown) : {}; + if (!text) { + return {}; + } + try { + return JSON.parse(text) as unknown; + } catch { + throw new QaMalformedJsonBodyError(); + } } export function writeJson(res: ServerResponse, statusCode: number, body: unknown) { diff --git a/extensions/qa-lab/src/lab-server.test.ts b/extensions/qa-lab/src/lab-server.test.ts index 34ce9c9e77c..230ade367a0 100644 --- a/extensions/qa-lab/src/lab-server.test.ts +++ b/extensions/qa-lab/src/lab-server.test.ts @@ -6,12 +6,12 @@ import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { readQaJsonBody } from "./bus-server.js"; +import { resolveUiAssetVersion } from "./lab-server-ui.js"; import { startQaLabServer, writeQaLabServerError, type QaLabServerStartParams, } from "./lab-server.js"; -import { resolveUiAssetVersion } from "./lab-server-ui.js"; const qaChannelMock = vi.hoisted(() => ({ resolveAccount: vi.fn(), @@ -639,6 +639,26 @@ describe("qa-lab server", () => { expect(JSON.parse(res.body)).toEqual({ error: "Payload too large" }); }); + it("returns controlled errors for malformed JSON body reads", async () => { + const lab = await startQaLabServerForTest(); + cleanups.push(async () => { + await lab.stop(); + }); + + const response = await fetch(`${lab.baseUrl}/api/inbound/message`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: "{", + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + error: "Malformed JSON body", + }); + }); + it("anchors direct self-check runs under the explicit repo root by default", async () => { const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-lab-self-check-root-")); cleanups.push(async () => { diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index e434acdfb8c..0c692bf1aa6 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -10,6 +10,7 @@ import { import { closeQaHttpServer, handleQaBusRequest, + isQaMalformedJsonBodyError, readQaJsonBody, writeError, writeJson, @@ -76,6 +77,10 @@ export function writeQaLabServerError(res: Parameters[0], err if (writeQaRequestBodyLimitError(res, error)) { return; } + if (isQaMalformedJsonBodyError(error)) { + writeError(res, 400, error.message); + return; + } if (error instanceof QaEvidenceGalleryError) { writeError(res, error.statusCode, error.message); return;