From 30ed478e61ddbc9e9bf4fce634f70441d92ba9a8 Mon Sep 17 00:00:00 2001 From: San Phan Date: Mon, 1 Jun 2026 11:58:57 +0700 Subject: [PATCH] fix: redact production error logs --- apps/api/src/middleware/errorHandler.js | 13 ++++++- apps/api/src/tests/errorHandler.test.js | 52 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/tests/errorHandler.test.js diff --git a/apps/api/src/middleware/errorHandler.js b/apps/api/src/middleware/errorHandler.js index 4beefc164f..2fdc3572d8 100644 --- a/apps/api/src/middleware/errorHandler.js +++ b/apps/api/src/middleware/errorHandler.js @@ -1,5 +1,16 @@ +import { env } from "../config/env.js"; + +function toProductionLogPayload(err) { + return { + name: err?.name ?? "Error", + status: err?.status ?? err?.statusCode ?? 500 + }; +} + export function errorHandler(err, req, res, next) { - console.error("Unhandled API error:", err); + const logPayload = env.nodeEnv === "production" ? toProductionLogPayload(err) : err; + console.error("Unhandled API error:", logPayload); + if (res.headersSent) { return next(err); } diff --git a/apps/api/src/tests/errorHandler.test.js b/apps/api/src/tests/errorHandler.test.js new file mode 100644 index 0000000000..19b492b05d --- /dev/null +++ b/apps/api/src/tests/errorHandler.test.js @@ -0,0 +1,52 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { env } from "../config/env.js"; +import { errorHandler } from "../middleware/errorHandler.js"; + +function createResponse() { + return { + headersSent: false, + statusCode: 200, + body: undefined, + status(code) { + this.statusCode = code; + return this; + }, + json(payload) { + this.body = payload; + return this; + } + }; +} + +test("production error logs omit raw secret-bearing error messages", () => { + const originalNodeEnv = env.nodeEnv; + const originalConsoleError = console.error; + const calls = []; + + env.nodeEnv = "production"; + console.error = (...args) => calls.push(args); + + try { + const res = createResponse(); + const secret = "sk_live_hidden_token"; + const err = new Error(`database auth failed for ${secret}`); + + errorHandler(err, {}, res, () => assert.fail("next should not be called")); + + assert.equal(res.statusCode, 500); + assert.deepEqual(res.body, { + success: false, + message: "Unexpected server error" + }); + + const serializedLogs = JSON.stringify(calls); + assert.equal(serializedLogs.includes(secret), false); + assert.equal(serializedLogs.includes("database auth failed"), false); + assert.match(serializedLogs, /"name":"Error"/); + assert.match(serializedLogs, /"status":500/); + } finally { + env.nodeEnv = originalNodeEnv; + console.error = originalConsoleError; + } +});