| 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, |
| ) { |
| |
| |
| |
| 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 }); |
| } |
|
|