diff --git a/extensions/qa-lab/src/providers/mock-openai/server.test.ts b/extensions/qa-lab/src/providers/mock-openai/server.test.ts index 2ef78728a7f..beec8f7f268 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.test.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.test.ts @@ -4265,6 +4265,34 @@ describe("qa mock openai server", () => { expect(body.error.message).toContain("Malformed JSON body"); }); + it("rejects malformed OpenAI-compatible JSON without crashing the mock server", async () => { + const server = await startQaMockOpenAiServer({ + host: "127.0.0.1", + port: 0, + }); + cleanups.push(async () => { + await server.stop(); + }); + + for (const path of ["/v1/responses", "/v1/embeddings", "/v1/images/generations"]) { + const response = await fetch(`${server.baseUrl}${path}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{bad", + }); + + expect(response.status).toBe(400); + const body = (await response.json()) as { + error: { type: string; message: string }; + }; + expect(body.error.type).toBe("invalid_request_error"); + expect(body.error.message).toContain("Malformed JSON body"); + } + + const health = await fetch(`${server.baseUrl}/healthz`); + expect(health.status).toBe(200); + }); + it("defaults empty-string Anthropic /v1/messages model to claude-opus-4-8", async () => { // Regression for the loop-7 Copilot finding: a bare `typeof // body.model === "string"` check lets an empty-string model leak diff --git a/extensions/qa-lab/src/providers/mock-openai/server.ts b/extensions/qa-lab/src/providers/mock-openai/server.ts index 288d40b2db1..660a31ba1d3 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.ts @@ -228,6 +228,23 @@ function readBody(req: IncomingMessage): Promise { }); } +function parseOpenAiJsonBody(raw: string): Record | null { + try { + return raw ? (JSON.parse(raw) as Record) : {}; + } catch { + return null; + } +} + +function writeOpenAiMalformedJsonError(res: ServerResponse, label: string) { + writeJson(res, 400, { + error: { + type: "invalid_request_error", + message: `Malformed JSON body for ${label} request.`, + }, + }); +} + function transcriptionTextForAudioRequest(rawBody: string) { if (rawBody.length >= QA_GROUP_AUDIO_MIN_MULTIPART_BODY_CHARS) { return QA_GROUP_AUDIO_TRANSCRIPTION_TEXT; @@ -3418,7 +3435,11 @@ export async function startQaMockOpenAiServer(params?: { host?: string; port?: n } if (req.method === "POST" && url.pathname === "/v1/images/generations") { const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; + const body = parseOpenAiJsonBody(raw); + if (!body) { + writeOpenAiMalformedJsonError(res, "OpenAI Images"); + return; + } imageGenerationRequests.push(body); if (imageGenerationRequests.length > 20) { imageGenerationRequests.splice(0, imageGenerationRequests.length - 20); @@ -3442,7 +3463,11 @@ export async function startQaMockOpenAiServer(params?: { host?: string; port?: n } if (req.method === "POST" && url.pathname === "/v1/embeddings") { const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; + const body = parseOpenAiJsonBody(raw); + if (!body) { + writeOpenAiMalformedJsonError(res, "OpenAI Embeddings"); + return; + } const inputs = extractEmbeddingInputTexts(body.input); writeJson(res, 200, { object: "list", @@ -3464,7 +3489,11 @@ export async function startQaMockOpenAiServer(params?: { host?: string; port?: n } if (req.method === "POST" && url.pathname === "/v1/responses") { const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; + const body = parseOpenAiJsonBody(raw); + if (!body) { + writeOpenAiMalformedJsonError(res, "OpenAI Responses"); + return; + } const input = Array.isArray(body.input) ? (body.input as ResponsesInputItem[]) : []; const events = await buildResponsesPayload(body, scenarioState); const resolvedModel = typeof body.model === "string" ? body.model : "";