Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -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();
}