From 869802126cd9b2cc206595d739568d317e94f971 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 15:37:11 +0200 Subject: [PATCH] Pin the 4xx branch of the error handler with a thrown error A mutant that put the message back into the non-validation 4xx answer survived: Fastify 5.8.5's own 4xx errors (invalid or empty JSON body, 415, 413) carry fixed messages, so the JSON-parse and media-type cases could not tell. The branch protects against any error thrown with a 4xx status, whose message is whatever its thrower wrote. A test route now throws one with the canary, and the answer must be the code alone. The handler's comment and the earlier commit's message said JSON.parse and the 415 quote input; in this Fastify they do not. The comment is corrected. Co-Authored-By: Claude Opus 5.5 --- src/app.ts | 12 ++++++------ tests/app.test.ts | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index 322c6e8..f6a72e9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -736,12 +736,12 @@ export async function buildApp(options: BuildAppOptions = {}): Promise { const status = error.statusCode !== undefined && error.statusCode >= 400 && error.statusCode < 600 ? error.statusCode : 500; const code = typeof error.code === "string" ? error.code : "error"; diff --git a/tests/app.test.ts b/tests/app.test.ts index 79c562c..de2a896 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -1224,7 +1224,7 @@ describe("Heimdall service", () => { // An answer never carries an input value. The canary stands in for whatever a // failure could quote: part of a misbound database URL in a pg or resolver -// error, a request body in a JSON parse error, a header or a path. +// error, a request body, a header, a path, or a 4xx error's own message. describe("error answers never echo an input", () => { const CANARY = "CANARY"; @@ -1276,6 +1276,19 @@ describe("error answers never echo an input", () => { expect(response.body).not.toContain(CANARY); }); + it("answers any other 4xx error by its code, not its message", async () => { + const app = await buildApp({ config: createTestConfig() }); + apps.push(app); + app.get("/v1/probe-4xx", async () => { + throw Object.assign(new Error(`refused ${CANARY}input`), { statusCode: 409, code: "PROBE_REFUSED" }); + }); + + const response = await app.inject({ method: "GET", url: "/v1/probe-4xx" }); + + expect(response.statusCode).toBe(409); + expect(response.json()).toEqual({ error: "invalid_request", code: "PROBE_REFUSED" }); + }); + it("answers an unsupported media type without quoting the header", async () => { const app = await buildApp({ config: createTestConfig() }); apps.push(app);