Files
fusion/packages/dashboard/src/api-error.ts
Fusion 9d7f58542b feat(FN-2562): merge fusion/fn-2562 (auto-resolved)
- docs(FN-2562): document extracted terminal and session-diff registrars
- fix(FN-2562): complete Step 5 — restore lint green
- feat(FN-2562): complete Step 3 — extract session diff registrar
- feat(FN-2562): complete Step 2 — create terminal route registrar
- feat(FN-2562): complete Step 1 — extract diff-base helper module
2026-04-26 11:54:54 -07:00

115 lines
3.0 KiB
TypeScript

import type { NextFunction, Request, RequestHandler, Response } from "express";
import { createRuntimeLogger, type RuntimeLogger } from "./runtime-logger.js";
export interface ApiErrorResponse {
error: string;
details?: Record<string, unknown>;
}
export interface SendErrorOptions {
details?: Record<string, unknown>;
logger?: RuntimeLogger;
}
export class ApiError extends Error {
public readonly statusCode: number;
public readonly details?: Record<string, unknown>;
public readonly isOperational: boolean;
constructor(statusCode: number, message: string, details?: Record<string, unknown>) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.details = details;
this.isOperational = true;
}
}
export function sendErrorResponse(
res: Response,
statusCode: number,
message: string,
options?: SendErrorOptions,
): Response<ApiErrorResponse> {
if (statusCode >= 500) {
const request = res.req;
const logger = options?.logger ?? createRuntimeLogger("api:error");
logger.error("Request failed", {
method: request?.method,
path: request?.originalUrl ?? request?.path,
statusCode,
message,
});
}
const payload: ApiErrorResponse = { error: message };
if (options?.details !== undefined) {
payload.details = options.details;
}
return res.status(statusCode).json(payload);
}
type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise<unknown> | unknown;
export function catchHandler(fn: AsyncHandler): RequestHandler {
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (error) {
if (res.headersSent) {
next(error);
return;
}
if (error instanceof ApiError) {
sendErrorResponse(res, error.statusCode, error.message, { details: error.details });
return;
}
if (error instanceof Error) {
sendErrorResponse(res, 500, error.message);
return;
}
sendErrorResponse(res, 500, "Internal server error");
}
};
}
export function badRequest(message: string, details?: Record<string, unknown>): ApiError {
return new ApiError(400, message, details);
}
export function unauthorized(message: string): ApiError {
return new ApiError(401, message);
}
export function notFound(message: string): ApiError {
return new ApiError(404, message);
}
export function conflict(message: string): ApiError {
return new ApiError(409, message);
}
export function rateLimited(message: string, retryAfter?: number): ApiError {
return new ApiError(429, message, { retryAfter });
}
export function internalError(message: string): ApiError {
return new ApiError(500, message);
}
export function rethrowAsApiError(error: unknown, fallbackMessage = "Internal server error"): never {
if (error instanceof ApiError) {
throw error;
}
if (error instanceof Error && error.message) {
throw internalError(error.message);
}
throw internalError(fallbackMessage);
}