diff --git a/src/api.ts b/src/api.ts index d1bf57a6c..8bf40c0b0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -32,6 +32,7 @@ import { networkMiddleware, requestNetwork } from "./middleware/network"; import { renderMetrics, metricsContentType, httpRequestsTotal, httpRequestDurationSeconds } from "./metrics"; import { getAllCachedTokens, getCachedTokenDecimals } from "./tokenCache"; import { toDisplayAmount } from "./amount"; +import { HttpError, newCorrelationId } from "./utils/errors"; // ─── RPC Health Check Cache ─────────────────────────────────────────────── // Keyed by network (#163): one cache entry would let a healthy testnet RPC @@ -1106,9 +1107,24 @@ export function createApp(): express.Application { }); // ─── Global error handler ──────────────────────────────────────────────── - app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - console.error("[api] Unhandled error:", err); - res.status(500).json({ error: err.message ?? "Internal server error" }); + app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { + // A deliberately-thrown HttpError carries a message written for the caller, + // so it keeps its own status and text. + if (err instanceof HttpError) { + return res.status(err.status).json({ error: err.message }); + } + + // Anything else is unhandled, and its message is not ours to show. This + // handler used to answer `err.message` verbatim, which is how a Postgres + // connection string reached callers through a raw driver error. The caller + // gets a correlation id; the detail stays in the log. + const correlationId = newCorrelationId(); + console.error(`[api] Unhandled error (${correlationId}):`, err); + + return res.status(500).json({ + error: "Internal server error", + correlationId, + }); }); return app; diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 000000000..797340e46 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,13 @@ +import { randomUUID } from 'crypto'; + +// Errors that are safe to show the caller verbatim (4xx, deliberate) +export class HttpError extends Error { + constructor(public status: number, message: string) { + super(message); + this.name = 'HttpError'; + } +} + +export function newCorrelationId(): string { + return randomUUID(); +} \ No newline at end of file