fix: sanitize unexpected API error responses

- Add a centralized Fastify error handler with server-side error logging
- Preserve valid 4xx status codes while normalizing other errors to HTTP 500
- Return generic error messages without exposing sensitive internal details
- Add a test endpoint and regression test for unexpected errors
This commit is contained in:
Brandon Wolfgang 2026-10-21 19:52:54 -05:00
parent c0ca16f166
commit 0115190510
2 changed files with 32 additions and 1 deletions

View file

@ -103,4 +103,17 @@ describe("API endpoints", () => {
assert.equal(response.statusCode, 404); assert.equal(response.statusCode, 404);
}); });
it("returns a generic 500 response for unexpected error", async () => {
const response = await app.inject({
method: "GET",
url: "/test-error",
});
assert.equal(response.statusCode, 500);
assert.deepEqual(response.json(), {
error: "Internal Server Error",
});
assert.equal(response.body.includes("Sensitive internal detail"), false);
});
}); });

View file

@ -1,4 +1,4 @@
import Fastify from "fastify"; import Fastify, { type FastifyError } from "fastify";
import { z } from "zod"; import { z } from "zod";
const echoSchema = z.object({ const echoSchema = z.object({
@ -10,6 +10,20 @@ export const buildApp = () => {
logger: true, logger: true,
}); });
app.setErrorHandler((error: FastifyError, request, reply) => {
request.log.error(error);
const candidateStatusCode = error.statusCode ?? 500;
const statusCode =
candidateStatusCode >= 400 && candidateStatusCode < 500
? candidateStatusCode
: 500;
return reply.code(statusCode).send({
error: statusCode === 500 ? "Internal Server Error" : "Request Error",
});
});
app.get("/health", async () => ({ app.get("/health", async () => ({
status: "ok", status: "ok",
})); }));
@ -35,6 +49,10 @@ export const buildApp = () => {
}; };
}); });
app.get("/test-error", async () => {
throw new Error("Sensitive internal detail");
});
return app; return app;
}; };