errorHandler.ts•12.2 kB
import { BaseErrorCode, McpError } from '../types-global/errors.js';
import { logger } from './logger.js';
import { sanitizeInputForLogging } from './security.js';
/**
* Generic error context interface
*/
export interface ErrorContext {
/** Unique request or operation identifier */
requestId?: string;
/** Any additional context information */
[key: string]: unknown;
}
/**
* Error handler options
*/
export interface ErrorHandlerOptions {
/** The context of the operation that caused the error */
context?: ErrorContext;
/** The name of the operation being performed */
operation: string;
/** The input that caused the error */
input?: unknown;
/** Whether to rethrow the error after handling */
rethrow?: boolean;
/** Custom error code to use when creating an McpError */
errorCode?: BaseErrorCode;
/** Custom error mapper function */
errorMapper?: (error: unknown) => Error;
/** Whether to include stack traces in logs */
includeStack?: boolean;
/** Whether this is a critical error that should abort operations */
critical?: boolean;
}
/**
* Base error mapping rule
*/
export interface BaseErrorMapping {
/** Pattern to match in the error message */
pattern: string | RegExp;
/** Error code for mapped errors */
errorCode: BaseErrorCode;
/** Custom error message template */
messageTemplate?: string;
}
/**
* Error mapping configuration
*/
export interface ErrorMapping<T extends Error = Error> extends BaseErrorMapping {
/** Factory function to create the mapped error */
factory: (error: unknown, context?: Record<string, unknown>) => T;
/** Additional context to merge with error context */
additionalContext?: Record<string, unknown>;
}
/**
* Simple mapper that maps error types to error codes
*/
const ERROR_TYPE_MAPPINGS: Record<string, BaseErrorCode> = {
'SyntaxError': BaseErrorCode.VALIDATION_ERROR,
'TypeError': BaseErrorCode.VALIDATION_ERROR,
'ReferenceError': BaseErrorCode.INTERNAL_ERROR,
'RangeError': BaseErrorCode.VALIDATION_ERROR,
'URIError': BaseErrorCode.VALIDATION_ERROR,
'EvalError': BaseErrorCode.INTERNAL_ERROR
};
/**
* Common error patterns for automatic classification
*/
const COMMON_ERROR_PATTERNS: BaseErrorMapping[] = [
// Authentication related errors
{ pattern: /auth|unauthorized|unauthenticated|not.*logged.*in|invalid.*token|expired.*token/i, errorCode: BaseErrorCode.UNAUTHORIZED },
// Permission related errors
{ pattern: /permission|forbidden|access.*denied|not.*allowed/i, errorCode: BaseErrorCode.FORBIDDEN },
// Not found errors
{ pattern: /not.*found|missing|no.*such|doesn't.*exist|couldn't.*find/i, errorCode: BaseErrorCode.NOT_FOUND },
// Validation errors
{ pattern: /invalid|validation|malformed|bad request|wrong format/i, errorCode: BaseErrorCode.VALIDATION_ERROR },
// Conflict errors
{ pattern: /conflict|already.*exists|duplicate|unique.*constraint/i, errorCode: BaseErrorCode.CONFLICT },
// Rate limiting
{ pattern: /rate.*limit|too.*many.*requests|throttled/i, errorCode: BaseErrorCode.RATE_LIMITED },
// Timeout errors
{ pattern: /timeout|timed.*out|deadline.*exceeded/i, errorCode: BaseErrorCode.TIMEOUT },
// External service errors
{ pattern: /service.*unavailable|bad.*gateway|gateway.*timeout/i, errorCode: BaseErrorCode.SERVICE_UNAVAILABLE }
];
/**
* Get a readable name for an error
* @param error Error to get name for
* @returns User-friendly error name
*/
function getErrorName(error: unknown): string {
if (error instanceof Error) {
return error.name || 'Error';
}
if (error === null) {
return 'NullError';
}
if (error === undefined) {
return 'UndefinedError';
}
return typeof error === 'object'
? 'ObjectError'
: 'UnknownError';
}
/**
* Get a message from an error
* @param error Error to get message from
* @returns Error message
*/
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (error === null) {
return 'Null error occurred';
}
if (error === undefined) {
return 'Undefined error occurred';
}
return typeof error === 'string'
? error
: String(error);
}
/**
* Error handler utility class with various error handling methods
*/
export class ErrorHandler {
/**
* Determine the appropriate error code for an error based on patterns and type
* @param error The error to classify
* @returns The appropriate error code
*/
public static determineErrorCode(error: unknown): BaseErrorCode {
// If it's already an McpError, use its code
if (error instanceof McpError) {
return error.code;
}
const errorName = getErrorName(error);
const errorMessage = getErrorMessage(error);
// Check if the error type has a direct mapping
if (errorName in ERROR_TYPE_MAPPINGS) {
return ERROR_TYPE_MAPPINGS[errorName as keyof typeof ERROR_TYPE_MAPPINGS];
}
// Check for common error patterns
for (const pattern of COMMON_ERROR_PATTERNS) {
const regex = pattern.pattern instanceof RegExp
? pattern.pattern
: new RegExp(pattern.pattern, 'i');
if (regex.test(errorMessage) || regex.test(errorName)) {
return pattern.errorCode;
}
}
// Default to internal error if no pattern matches
return BaseErrorCode.INTERNAL_ERROR;
}
/**
* Handle operation errors with consistent logging and transformation
* @param error The error that occurred
* @param options Error handling options
* @returns The transformed error
*/
public static handleError(error: unknown, options: ErrorHandlerOptions): Error {
const {
context,
operation,
input,
rethrow = false,
errorCode: explicitErrorCode,
includeStack = true,
critical = false
} = options;
// If it's already an McpError, use it directly but apply additional context
if (error instanceof McpError) {
// Add any additional context
if (context && Object.keys(context).length > 0) {
error.details = { ...error.details, ...context };
}
// Log the error with sanitized input
logger.error(`Error ${operation}: ${error.message}`, {
errorCode: error.code,
requestId: context?.requestId,
input: input ? sanitizeInputForLogging(input) : undefined,
stack: includeStack ? error.stack : undefined,
critical,
...context
});
if (rethrow) {
throw error;
}
return error;
}
// Sanitize input for logging
const sanitizedInput = input ? sanitizeInputForLogging(input) : undefined;
// Log the error with consistent format
logger.error(`Error ${operation}`, {
error: error instanceof Error ? error.message : String(error),
errorType: getErrorName(error),
input: sanitizedInput,
requestId: context?.requestId,
stack: includeStack && error instanceof Error ? error.stack : undefined,
critical,
...context
});
// Choose the error code (explicit > determined > default)
const errorCode = explicitErrorCode ||
ErrorHandler.determineErrorCode(error) ||
BaseErrorCode.INTERNAL_ERROR;
// Transform to appropriate error type
const transformedError = options.errorMapper
? options.errorMapper(error)
: new McpError(
errorCode,
`Error ${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`,
{
originalError: getErrorName(error),
...context
}
);
// Rethrow if requested
if (rethrow) {
throw transformedError;
}
return transformedError;
}
/**
* Map an error to a specific error type based on error message patterns
* @param error The error to map
* @param mappings Array of pattern and factory mappings
* @param defaultFactory Default factory function if no pattern matches
* @returns The mapped error
*/
public static mapError<T extends Error>(
error: unknown,
mappings: ErrorMapping<T>[],
defaultFactory?: (error: unknown, context?: Record<string, unknown>) => T
): T | Error {
// If it's already the target type and we have a default factory to check against, return it
if (defaultFactory && error instanceof Error) {
const defaultInstance = defaultFactory(error);
if (error.constructor === defaultInstance.constructor) {
return error as T;
}
}
const errorMessage = getErrorMessage(error);
// Check each pattern and return the first match
for (const mapping of mappings) {
const matches = mapping.pattern instanceof RegExp
? mapping.pattern.test(errorMessage)
: errorMessage.includes(mapping.pattern);
if (matches) {
return mapping.factory(error, mapping.additionalContext);
}
}
// Return default or original error
if (defaultFactory) {
return defaultFactory(error);
}
return error instanceof Error
? error
: new Error(String(error));
}
/**
* Create a simplified error mapper based on error patterns and codes
* @param patterns Array of error patterns, codes, and messages
* @param defaultErrorCode Default error code if no pattern matches
* @returns Error mapper function
*/
public static createErrorMapper(
patterns: BaseErrorMapping[],
defaultErrorCode: BaseErrorCode = BaseErrorCode.INTERNAL_ERROR
): (error: unknown, context?: Record<string, unknown>) => McpError {
return (error: unknown, context?: Record<string, unknown>): McpError => {
// Already an McpError
if (error instanceof McpError) {
// Add any additional context
if (context && Object.keys(context).length > 0) {
error.details = { ...error.details, ...context };
}
return error;
}
const errorMessage = getErrorMessage(error);
// Check each pattern for a match
for (const { pattern, errorCode, messageTemplate } of patterns) {
const matches = pattern instanceof RegExp
? pattern.test(errorMessage)
: errorMessage.includes(pattern);
if (matches) {
// Use template if provided, otherwise use original error message
const message = messageTemplate
? messageTemplate.replace('{message}', errorMessage)
: errorMessage;
return new McpError(
errorCode,
message,
{ originalError: getErrorName(error), ...context }
);
}
}
// No matches found, use default
return new McpError(
defaultErrorCode,
errorMessage,
{ originalError: getErrorName(error), ...context }
);
};
}
/**
* Format an error for consistent response structure
* @param error The error to format
* @returns Formatted error object
*/
public static formatError(error: unknown): Record<string, unknown> {
if (error instanceof McpError) {
return {
code: error.code,
message: error.message,
details: error.details || {}
};
}
if (error instanceof Error) {
return {
code: ErrorHandler.determineErrorCode(error),
message: error.message,
details: { errorType: error.name }
};
}
return {
code: BaseErrorCode.UNKNOWN_ERROR,
message: String(error),
details: { errorType: typeof error }
};
}
/**
* Safely execute a function and handle any errors
* @param fn Function to execute
* @param options Error handling options
* @returns The result of the function or error
*/
public static async tryCatch<T>(
fn: () => Promise<T> | T,
options: ErrorHandlerOptions
): Promise<T> {
try {
return await fn();
} catch (error) {
throw ErrorHandler.handleError(error, { ...options, rethrow: true });
}
}
}
export default ErrorHandler;