fix(qa): reject malformed mock OpenAI JSON

This commit is contained in:
Vincent Koc
2026-06-21 12:53:27 +02:00
parent b796890b97
commit f69ba12a37
2 changed files with 60 additions and 3 deletions

View File

@@ -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

View File

@@ -228,6 +228,23 @@ function readBody(req: IncomingMessage): Promise<string> {
});
}
function parseOpenAiJsonBody(raw: string): Record<string, unknown> | null {
try {
return raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
} 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<string, unknown>) : {};
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<string, unknown>) : {};
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<string, unknown>) : {};
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 : "";