From c5a2fab8f8d477b94cb997c31c2ff53f1c63c248 Mon Sep 17 00:00:00 2001 From: RikaTech2006 Date: Sat, 26 Sep 2026 23:49:05 +0000 Subject: [PATCH 1/2] fix(api): stop returning raw internal errors to unauthenticated callers --- src/api.ts | 17 ++++++++++++++--- src/utils/errors.ts | 13 +++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/utils/errors.ts diff --git a/src/api.ts b/src/api.ts index 96128f439..9c2ce2b8d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1002,10 +1002,21 @@ 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: any, req: Request, res: Response, next: NextFunction) => { + // Deliberate, already-classified errors keep their own status + message + if (err instanceof HttpError) { + return res.status(err.status).json({ error: err.message }); + } + + // Anything else is an unhandled internal error — never leak it + const correlationId = newCorrelationId(); + console.error(`[${correlationId}] Unhandled error:`, err); // swap for real logger if one exists + + 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 From e61b3dbd9ff295ee7506ed6044af1fec5042c611 Mon Sep 17 00:00:00 2001 From: Miracle656 Date: Mon, 19 Oct 2026 13:13:09 +0100 Subject: [PATCH 2/2] fix(api): wire the error handler's imports and keep the leak closed `HttpError` and `newCorrelationId` were used in src/api.ts but never imported, so the branch did not compile: error TS2304: Cannot find name 'HttpError'. error TS2552: Cannot find name 'newCorrelationId'. Added the import, restored the file's indentation, dropped the "swap for real logger if one exists" placeholder, and marked the unused handler parameters `_req`/`_next` as the file does elsewhere. Also wrote the reason into the comment rather than leaving it implicit: this handler used to answer `err.message` verbatim, which is how a Postgres connection string reached callers through a raw driver error. 50 suites, 753 tests passing. tsc clean. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB --- src/api.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/api.ts b/src/api.ts index bbfd21e71..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,21 +1107,25 @@ export function createApp(): express.Application { }); // ─── Global error handler ──────────────────────────────────────────────── - app.use((err: any, req: Request, res: Response, next: NextFunction) => { - // Deliberate, already-classified errors keep their own status + message - if (err instanceof HttpError) { - return res.status(err.status).json({ error: err.message }); - } + 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 an unhandled internal error — never leak it - const correlationId = newCorrelationId(); - console.error(`[${correlationId}] Unhandled error:`, err); // swap for real logger if one exists + // 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 res.status(500).json({ + error: "Internal server error", + correlationId, + }); }); -}); return app; }