Fix 500 error logs still showing generic pino-http message

The previous fix (8151331) set res.err but pino-http wasn't picking it
up (likely Express 5 response object behavior). Switch to a custom
__errorContext property on the response that customErrorMessage and
customProps read directly, bypassing pino-http's unreliable res.err
check. Remove duplicate manual logger.error calls from the error
handler since pino-http now gets the full context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-06 11:41:06 -06:00
parent cf1ccd1e14
commit 2e7bf85e7a
2 changed files with 38 additions and 40 deletions

View File

@@ -1,8 +1,16 @@
import type { Request, Response, NextFunction } from "express";
import { ZodError } from "zod";
import { logger } from "./logger.js";
import { HttpError } from "../errors.js";
export interface ErrorContext {
error: { message: string; stack?: string; name?: string; details?: unknown; raw?: unknown };
method: string;
url: string;
reqBody?: unknown;
reqParams?: unknown;
reqQuery?: unknown;
}
export function errorHandler(
err: unknown,
req: Request,
@@ -11,22 +19,14 @@ export function errorHandler(
) {
if (err instanceof HttpError) {
if (err.status >= 500) {
(res as any).err = err;
logger.error(
{
err: { message: err.message, stack: err.stack, name: err.name, details: err.details },
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
},
"HttpError %d: %s %s — %s",
err.status,
req.method,
req.originalUrl,
err.message,
);
(res as any).__errorContext = {
error: { message: err.message, stack: err.stack, name: err.name, details: err.details },
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
} satisfies ErrorContext;
}
res.status(err.status).json({
error: err.message,
@@ -40,28 +40,16 @@ export function errorHandler(
return;
}
const errObj = err instanceof Error
? { message: err.message, stack: err.stack, name: err.name }
: { raw: err };
(res as any).__errorContext = {
error: err instanceof Error
? { message: err.message, stack: err.stack, name: err.name }
: { message: String(err), raw: err },
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
} satisfies ErrorContext;
// Attach the real error so pino-http uses it instead of its generic
// "failed with status code 500" message in the response-complete log
const realError = err instanceof Error ? err : Object.assign(new Error(String(err)), { raw: err });
(res as any).err = realError;
logger.error(
{
err: errObj,
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
},
"Unhandled error: %s %s — %s",
req.method,
req.originalUrl,
err instanceof Error ? err.message : String(err),
);
res.status(500).json({ error: "Internal server error" });
}

View File

@@ -53,11 +53,21 @@ export const httpLogger = pinoHttp({
return `${req.method} ${req.url} ${res.statusCode}`;
},
customErrorMessage(req, res, err) {
const errMsg = err?.message || (res as any).err?.message || "unknown error";
const ctx = (res as any).__errorContext;
const errMsg = ctx?.error?.message || err?.message || (res as any).err?.message || "unknown error";
return `${req.method} ${req.url} ${res.statusCode}${errMsg}`;
},
customProps(req, res) {
if (res.statusCode >= 400) {
const ctx = (res as any).__errorContext;
if (ctx) {
return {
err: ctx.error,
reqBody: ctx.reqBody,
reqParams: ctx.reqParams,
reqQuery: ctx.reqQuery,
};
}
const props: Record<string, unknown> = {};
const { body, params, query } = req as any;
if (body && typeof body === "object" && Object.keys(body).length > 0) {