import { Request, Response, NextFunction } from 'express';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { Logger } from '../utils/logger';
import { isDevelopment } from '../config';
// Custom error types
export class AppError extends Error {
public statusCode: number;
public isOperational: boolean;
public code?: string;
constructor(message: string, statusCode: number = 500, code?: string, isOperational: boolean = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.code = code;
Error.captureStackTrace?.(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(message: string, field?: string) {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
if (field) {
this.message = `${field}: ${message}`;
}
}
}
export class AuthenticationError extends AppError {
constructor(message: string = 'Authentication required') {
super(message, 401, 'AUTHENTICATION_ERROR');
this.name = 'AuthenticationError';
}
}
export class AuthorizationError extends AppError {
constructor(message: string = 'Insufficient permissions') {
super(message, 403, 'AUTHORIZATION_ERROR');
this.name = 'AuthorizationError';
}
}
export class NotFoundError extends AppError {
constructor(resource: string = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND_ERROR');
this.name = 'NotFoundError';
}
}
export class ConflictError extends AppError {
constructor(message: string = 'Resource conflict') {
super(message, 409, 'CONFLICT_ERROR');
this.name = 'ConflictError';
}
}
export class RateLimitError extends AppError {
public retryAfter?: number;
constructor(message: string = 'Rate limit exceeded', retryAfter?: number) {
super(message, 429, 'RATE_LIMIT_ERROR');
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
export class ExternalServiceError extends AppError {
public service?: string;
constructor(message: string, service?: string) {
super(message, 502, 'EXTERNAL_SERVICE_ERROR');
this.name = 'ExternalServiceError';
this.service = service;
}
}
// Error handler middleware
export const errorHandler = (
error: Error | AppError | McpError,
req: Request,
res: Response,
next: NextFunction
): void => {
// Don't handle if response already sent
if (res.headersSent) {
return next(error);
}
let statusCode = 500;
let message = 'Internal Server Error';
let code = 'INTERNAL_ERROR';
let details: any = {};
// Handle different error types
if (error instanceof AppError) {
statusCode = error.statusCode;
message = error.message;
code = error.code || 'APP_ERROR';
if (error instanceof RateLimitError && error.retryAfter) {
res.set('Retry-After', error.retryAfter.toString());
}
} else if (error instanceof McpError) {
// Handle MCP errors
switch (error.code) {
case ErrorCode.InvalidParams:
statusCode = 400;
code = 'INVALID_PARAMS';
break;
case ErrorCode.MethodNotFound:
statusCode = 404;
code = 'METHOD_NOT_FOUND';
break;
case ErrorCode.InvalidRequest:
statusCode = 400;
code = 'INVALID_REQUEST';
break;
default:
statusCode = 500;
code = 'MCP_ERROR';
}
message = error.message;
} else if (error.name === 'ValidationError') {
statusCode = 400;
message = error.message;
code = 'VALIDATION_ERROR';
} else if (error.name === 'CastError') {
statusCode = 400;
message = 'Invalid data format';
code = 'CAST_ERROR';
} else if (error.name === 'MongoError' || error.name === 'PostgresError') {
statusCode = 500;
message = 'Database error';
code = 'DATABASE_ERROR';
details.dbError = isDevelopment() ? error.message : 'Database operation failed';
} else if (error.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token';
code = 'INVALID_TOKEN';
} else if (error.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired';
code = 'TOKEN_EXPIRED';
} else {
// Unknown error
message = isDevelopment() ? error.message : 'Internal Server Error';
}
// Log error
Logger.error('Request error', error, {
statusCode,
code,
url: req.url,
method: req.method,
userId: (req as any).userId,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// Prepare error response
const errorResponse: any = {
success: false,
error: {
code,
message,
...(Object.keys(details).length > 0 && { details })
}
};
// Add stack trace in development
if (isDevelopment() && error.stack) {
errorResponse.error.stack = error.stack;
}
// Add request ID if available
if ((req as any).requestId) {
errorResponse.requestId = (req as any).requestId;
}
res.status(statusCode).json(errorResponse);
};
// 404 handler
export const notFoundHandler = (req: Request, res: Response): void => {
Logger.warn('Route not found', {
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
res.status(404).json({
success: false,
error: {
code: 'NOT_FOUND',
message: `Route ${req.method} ${req.url} not found`
}
});
};
// Async error wrapper
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Validation error handler
export const handleValidationError = (error: any): ValidationError => {
if (error.name === 'ValidationError') {
const messages = Object.values(error.errors).map((err: any) => err.message);
throw new ValidationError(messages.join(', '));
}
if (error.code === 11000) {
// Duplicate key error
const field = Object.keys(error.keyValue)[0];
throw new ValidationError(`${field} already exists`, field);
}
throw error;
};
// MCP error converter
export const convertToMcpError = (error: Error | AppError): McpError => {
if (error instanceof McpError) {
return error;
}
if (error instanceof ValidationError) {
return new McpError(ErrorCode.InvalidParams, error.message);
}
if (error instanceof AuthenticationError) {
return new McpError(ErrorCode.InvalidRequest, error.message);
}
if (error instanceof NotFoundError) {
return new McpError(ErrorCode.MethodNotFound, error.message);
}
return new McpError(ErrorCode.InternalError, error.message);
};
// Operational error checker
export const isOperationalError = (error: Error): boolean => {
if (error instanceof AppError) {
return error.isOperational;
}
return false;
};
// Process error handlers
export const handleUncaughtException = (error: Error): void => {
Logger.error('Uncaught Exception', error, { fatal: true });
// Give logger time to write before exiting
setTimeout(() => {
process.exit(1);
}, 1000);
};
export const handleUnhandledRejection = (reason: any, promise: Promise<any>): void => {
Logger.error('Unhandled Promise Rejection', new Error(reason), {
fatal: true,
promise: promise.toString()
});
// Give logger time to write before exiting
setTimeout(() => {
process.exit(1);
}, 1000);
};
// Request timeout handler
export const timeoutHandler = (timeoutMs: number = 30000) => {
return (req: Request, res: Response, next: NextFunction): void => {
const timeout = setTimeout(() => {
if (!res.headersSent) {
Logger.warn('Request timeout', {
url: req.url,
method: req.method,
timeout: timeoutMs,
userId: (req as any).userId
});
res.status(408).json({
success: false,
error: {
code: 'REQUEST_TIMEOUT',
message: 'Request timeout'
}
});
}
}, timeoutMs);
// Clear timeout when response finishes
res.on('finish', () => {
clearTimeout(timeout);
});
next();
};
};
// Health check error
export class HealthCheckError extends AppError {
public component: string;
constructor(component: string, message: string) {
super(`Health check failed for ${component}: ${message}`, 503, 'HEALTH_CHECK_ERROR');
this.name = 'HealthCheckError';
this.component = component;
}
}