File size: 3,186 Bytes
96e86e5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | import type { Request, Response, NextFunction } from "express";
import { ZodError } from "zod";
import { trackErrorHandlerCrash } from "@penclipai/shared/telemetry";
import { HttpError } from "../errors.js";
import { translate as translateServer } from "../i18n.js";
import { getTelemetryClient } from "../telemetry.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;
}
function attachErrorContext(
req: Request,
res: Response,
payload: ErrorContext["error"],
rawError?: Error,
) {
(res as any).__errorContext = {
error: payload,
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
} satisfies ErrorContext;
if (rawError) {
(res as any).err = rawError;
}
}
export function errorHandler(
err: unknown,
req: Request,
res: Response,
next: NextFunction,
) {
// Express can still route an error here after another layer already committed
// a response (for example a 304). In that case we must delegate instead of
// trying to write a second response.
if (res.headersSent) {
next(err);
return;
}
const translate = typeof req.t === "function"
? req.t
: ((key: string, params?: Record<string, string | number | boolean | null | undefined>) =>
translateServer("en", key, params));
const status = typeof (err as { status?: unknown })?.status === "number"
? (err as { status: number }).status
: typeof (err as { statusCode?: unknown })?.statusCode === "number"
? (err as { statusCode: number }).statusCode
: null;
const type = typeof (err as { type?: unknown })?.type === "string"
? (err as { type: string }).type
: null;
if (status === 400 && type === "entity.parse.failed") {
res.status(400).json({ error: translate("errors.validation") });
return;
}
if (err instanceof HttpError) {
const translatedMessage = translate(err.message);
if (err.status >= 500) {
attachErrorContext(
req,
res,
{ message: err.message, stack: err.stack, name: err.name, details: err.details },
err,
);
const tc = getTelemetryClient();
if (tc) trackErrorHandlerCrash(tc, { errorCode: err.name });
}
res.status(err.status).json({
error: translatedMessage,
...(err.details ? { details: err.details } : {}),
});
return;
}
if (err instanceof ZodError) {
res.status(400).json({ error: translate("errors.validation"), details: err.errors });
return;
}
const rootError = err instanceof Error ? err : new Error(String(err));
attachErrorContext(
req,
res,
err instanceof Error
? { message: err.message, stack: err.stack, name: err.name }
: { message: String(err), raw: err, stack: rootError.stack, name: rootError.name },
rootError,
);
res.status(500).json({ error: translate("errors.internalServer") });
const tc = getTelemetryClient();
if (tc) trackErrorHandlerCrash(tc, { errorCode: rootError.name });
}
|