import { redactSensitiveInfoFromErrorMessage } from './secure-config.js'; import { getLogger } from './logging.js'; const logger = getLogger(); /** * Common AWS error types that might need special handling */ export enum AwsErrorType { ACCESS_DENIED = 'AccessDeniedException', RESOURCE_NOT_FOUND = 'ResourceNotFoundException', VALIDATION_ERROR = 'ValidationException', LIMIT_EXCEEDED = 'LimitExceededException', THROTTLING = 'ThrottlingException', SERVICE_UNAVAILABLE = 'ServiceUnavailableException', REQUEST_TIMEOUT = 'RequestTimeoutException', NETWORK_ERROR = 'NetworkingError', CREDENTIALS_ERROR = 'CredentialsError' } /** * HTTP Status codes for different error types */ export enum HttpStatusCode { BAD_REQUEST = 400, UNAUTHORIZED = 401, FORBIDDEN = 403, NOT_FOUND = 404, CONFLICT = 409, TOO_MANY_REQUESTS = 429, INTERNAL_SERVER_ERROR = 500, SERVICE_UNAVAILABLE = 503, } /** * Base error class for all application errors */ export class BaseError extends Error { constructor( message: string, public readonly statusCode: HttpStatusCode = HttpStatusCode.INTERNAL_SERVER_ERROR, public readonly isOperational: boolean = true, public readonly errorCode: string = 'INTERNAL_ERROR', public readonly originalError?: Error | unknown ) { super(message); this.name = this.constructor.name; // Capture stack trace Error.captureStackTrace(this, this.constructor); } /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message }; } } /** * Custom error class for AWS service errors */ export class AwsServiceError extends BaseError { constructor( message: string, public readonly serviceName: string, public readonly operation: string, originalError?: Error | unknown, statusCode: HttpStatusCode = HttpStatusCode.INTERNAL_SERVER_ERROR, ) { super( message, statusCode, true, `AWS_${serviceName.toUpperCase()}_ERROR`, originalError ); // Get AWS specific error details if available if (originalError && typeof originalError === 'object' && originalError !== null && '$metadata' in originalError) { const awsError = originalError as any; this.awsRequestId = awsError.$metadata?.requestId; this.awsErrorType = awsError.__type || awsError.name; this.awsStatusCode = awsError.$metadata?.httpStatusCode; } } // AWS-specific error properties public readonly awsRequestId?: string; public readonly awsErrorType?: string; public readonly awsStatusCode?: number; /** * Returns a detailed representation of the error */ toDetailedString(): string { return `AWS ${this.serviceName} Error (${this.operation}): ${this.message}${ this.awsRequestId ? `\nRequest ID: ${this.awsRequestId}` : '' }${ this.awsErrorType ? `\nError Type: ${this.awsErrorType}` : '' }${ this.awsStatusCode ? `\nStatus Code: ${this.awsStatusCode}` : '' }`; } /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message, service: this.serviceName, operation: this.operation, // Only include requestId in response as it's safe to share and helpful for debugging requestId: this.awsRequestId }; } } /** * Custom error class for configuration errors */ export class ConfigurationError extends BaseError { constructor( message: string, configKey?: string, originalError?: Error | unknown ) { super( message, HttpStatusCode.INTERNAL_SERVER_ERROR, true, 'CONFIGURATION_ERROR', originalError ); this.configKey = configKey; } public readonly configKey?: string; /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message }; } } /** * Custom error class for tool execution errors */ export class ToolExecutionError extends BaseError { constructor( message: string, public readonly toolName: string, public readonly toolParams?: Record, originalError?: Error | unknown, statusCode: HttpStatusCode = HttpStatusCode.INTERNAL_SERVER_ERROR ) { super( message, statusCode, true, `TOOL_${toolName.toUpperCase()}_ERROR`, originalError ); } /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message, tool: this.toolName }; } } /** * Custom error class for transport errors */ export class TransportError extends BaseError { constructor( message: string, public readonly sessionId: string, originalError?: Error | unknown, statusCode: HttpStatusCode = HttpStatusCode.INTERNAL_SERVER_ERROR ) { super( message, statusCode, true, 'TRANSPORT_ERROR', originalError ); } /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message, sessionId: this.sessionId }; } } /** * Custom error class for validation errors */ export class ValidationError extends BaseError { constructor( message: string, public readonly validationErrors: Record[], originalError?: Error | unknown ) { super( message, HttpStatusCode.BAD_REQUEST, true, 'VALIDATION_ERROR', originalError ); } /** * Returns a sanitized version of the error for client responses */ toClientResponse(): Record { return { error: this.errorCode, message: this.message, validationErrors: this.validationErrors }; } } /** * Map AWS error codes to HTTP status codes */ const awsErrorStatusCodeMap: Record = { // AWS CloudWatch Logs error codes ResourceNotFoundException: HttpStatusCode.NOT_FOUND, InvalidParameterException: HttpStatusCode.BAD_REQUEST, LimitExceededException: HttpStatusCode.BAD_REQUEST, ServiceUnavailableException: HttpStatusCode.SERVICE_UNAVAILABLE, ThrottlingException: HttpStatusCode.SERVICE_UNAVAILABLE, // AWS CloudTrail error codes InvalidTimeRangeException: HttpStatusCode.BAD_REQUEST, InvalidMaxResultsException: HttpStatusCode.BAD_REQUEST, InvalidLookupAttributesException: HttpStatusCode.BAD_REQUEST, OperationNotPermittedException: HttpStatusCode.FORBIDDEN, // AWS common error codes AccessDeniedException: HttpStatusCode.FORBIDDEN, UnauthorizedException: HttpStatusCode.UNAUTHORIZED, UnrecognizedClientException: HttpStatusCode.UNAUTHORIZED, ValidationException: HttpStatusCode.BAD_REQUEST, InvalidRequestException: HttpStatusCode.BAD_REQUEST, InternalServerException: HttpStatusCode.INTERNAL_SERVER_ERROR, }; /** * Determine HTTP status code based on AWS error type * @param error The AWS error * @returns An appropriate HTTP status code */ function getAwsErrorStatusCode(error: any): HttpStatusCode { // Try to extract the AWS error type const errorType = error.__type || error.name || ''; // Remove service prefix if present (e.g., "CloudWatch#ResourceNotFoundException" -> "ResourceNotFoundException") const errorName = errorType.includes('#') ? errorType.split('#')[1] : errorType; // Return mapped status code or default to 500 return awsErrorStatusCodeMap[errorName] || HttpStatusCode.INTERNAL_SERVER_ERROR; } /** * Wrap an AWS error with additional context * @param error The original error * @param serviceName The AWS service name * @param operation The operation being performed * @returns A wrapped AwsServiceError */ export function wrapAwsError(error: unknown, serviceName: string, operation: string): AwsServiceError { // Extract error message const errorMessage = error instanceof Error ? error.message : String(error); // Determine appropriate HTTP status code const statusCode = typeof error === 'object' && error !== null ? getAwsErrorStatusCode(error) : HttpStatusCode.INTERNAL_SERVER_ERROR; return new AwsServiceError( `Error in ${serviceName} during ${operation}: ${errorMessage}`, serviceName, operation, error, statusCode ); } /** * Format an error for client response * @param error The error to format * @returns A formatted error response object */ export function formatErrorForClient(error: unknown): Record { // Handle our custom error types if (error instanceof BaseError) { // Get the base response but redact any potentially sensitive info from message const response = error.toClientResponse(); if (typeof response.message === 'string') { response.message = redactSensitiveInfoFromErrorMessage(response.message); } return response; } // Handle standard Error objects if (error instanceof Error) { return { error: 'UNKNOWN_ERROR', message: redactSensitiveInfoFromErrorMessage(error.message) }; } // Handle other unknown error types let errorMessage = String(error); errorMessage = redactSensitiveInfoFromErrorMessage(errorMessage); return { error: 'UNKNOWN_ERROR', message: errorMessage }; } /** * Determine if an error is operational (expected) or programming (unexpected) * @param error The error to check * @returns Whether the error is operational */ export function isOperationalError(error: unknown): boolean { if (error instanceof BaseError) { return error.isOperational; } return false; } /** * Map of AWS error types to more user-friendly messages */ const AWS_ERROR_MESSAGES: Record = { [AwsErrorType.ACCESS_DENIED]: 'Access denied. Your AWS credentials do not have permission to perform this action.', [AwsErrorType.RESOURCE_NOT_FOUND]: 'The requested AWS resource was not found.', [AwsErrorType.VALIDATION_ERROR]: 'The request parameters failed AWS service validation.', [AwsErrorType.LIMIT_EXCEEDED]: 'AWS service limit exceeded. Please reduce request frequency or increase service limits.', [AwsErrorType.THROTTLING]: 'Request rate limited by AWS. Please reduce request frequency.', [AwsErrorType.SERVICE_UNAVAILABLE]: 'AWS service is currently unavailable. Please try again later.', [AwsErrorType.REQUEST_TIMEOUT]: 'AWS request timed out. Please check your network connection or try again later.', [AwsErrorType.NETWORK_ERROR]: 'Network error occurred while connecting to AWS. Please check your internet connection.', [AwsErrorType.CREDENTIALS_ERROR]: 'Invalid or expired AWS credentials. Please update your AWS credentials.' }; /** * Extracts error type from an AWS error response * @param error AWS error object * @returns Extracted error type or undefined */ function extractAwsErrorType(error: any): string | undefined { // Different AWS SDKs can structure errors differently if (!error) return undefined; // Try to extract the AWS error type from various error object structures return error.name || // V3 SDK error.code || // V2 SDK (error.$metadata && error.$metadata.code) || // V3 SDK metadata (error.__type ? error.__type.split('#').pop() : undefined); // Raw AWS error } /** * Common patterns to detect in AWS error messages */ const AWS_ERROR_PATTERNS = [ { pattern: /access denied|not authorized|forbidden|no permission/i, type: AwsErrorType.ACCESS_DENIED }, { pattern: /not found|doesn't exist|does not exist/i, type: AwsErrorType.RESOURCE_NOT_FOUND }, { pattern: /invalid|validation|malformed|incorrect/i, type: AwsErrorType.VALIDATION_ERROR }, { pattern: /limit exceeded|too many requests/i, type: AwsErrorType.LIMIT_EXCEEDED }, { pattern: /throttl|rate exceed|too many request/i, type: AwsErrorType.THROTTLING }, { pattern: /unavailable|down|maintenance/i, type: AwsErrorType.SERVICE_UNAVAILABLE }, { pattern: /timeout|timed out|slow/i, type: AwsErrorType.REQUEST_TIMEOUT }, { pattern: /network|connect|unreachable|DNS|route/i, type: AwsErrorType.NETWORK_ERROR }, { pattern: /credential|auth|login|sign|token|expired/i, type: AwsErrorType.CREDENTIALS_ERROR } ]; /** * Determines error type from error message if not explicitly provided * @param errorMessage Error message to analyze * @returns Inferred error type */ function inferErrorTypeFromMessage(errorMessage: string): string | undefined { if (!errorMessage) return undefined; for (const { pattern, type } of AWS_ERROR_PATTERNS) { if (pattern.test(errorMessage)) { return type; } } return undefined; } /** * Handles AWS errors and transforms them into appropriate application errors * @param error Original error from AWS SDK * @param serviceName AWS service name (e.g., 'CloudWatchLogs') * @param operation Operation being performed * @returns Properly wrapped and contextualized error */ export function handleAwsError(error: any, serviceName: string, operation: string): Error { try { // Safely extract error information const errorType = extractAwsErrorType(error); const errorMessage = error instanceof Error ? error.message : String(error); const safeErrorMessage = redactSensitiveInfoFromErrorMessage(errorMessage); // Try to infer error type from message if not explicitly provided const inferredType = errorType || inferErrorTypeFromMessage(safeErrorMessage); // Get user-friendly message const friendlyMessage = inferredType && AWS_ERROR_MESSAGES[inferredType] ? AWS_ERROR_MESSAGES[inferredType] : `Error in AWS ${serviceName} during ${operation}`; // Determine appropriate HTTP status code let statusCode = HttpStatusCode.INTERNAL_SERVER_ERROR; // Map common AWS errors to HTTP status codes if (inferredType) { switch (inferredType) { case AwsErrorType.ACCESS_DENIED: statusCode = HttpStatusCode.FORBIDDEN; break; case AwsErrorType.RESOURCE_NOT_FOUND: statusCode = HttpStatusCode.NOT_FOUND; break; case AwsErrorType.VALIDATION_ERROR: statusCode = HttpStatusCode.BAD_REQUEST; break; case AwsErrorType.LIMIT_EXCEEDED: case AwsErrorType.THROTTLING: statusCode = HttpStatusCode.TOO_MANY_REQUESTS; break; case AwsErrorType.SERVICE_UNAVAILABLE: statusCode = HttpStatusCode.SERVICE_UNAVAILABLE; break; case AwsErrorType.CREDENTIALS_ERROR: statusCode = HttpStatusCode.UNAUTHORIZED; break; } } // Create specialized error based on type if (inferredType === AwsErrorType.VALIDATION_ERROR) { return new ValidationError( `${friendlyMessage}: ${safeErrorMessage}`, [{ field: operation, message: safeErrorMessage }], error ); } else if (inferredType === AwsErrorType.CREDENTIALS_ERROR) { return new ConfigurationError( `${friendlyMessage}: ${safeErrorMessage}`, 'AWS_CREDENTIALS', error ); } else { // Default to AwsServiceError for other cases return new AwsServiceError( `${friendlyMessage}: ${safeErrorMessage}`, serviceName, operation, error, statusCode ); } } catch (handlerError) { // If error handling itself fails, log and return a generic error logger.error('Error in AWS error handler:', handlerError); return new AwsServiceError( `Unhandled error in ${serviceName} during ${operation}`, serviceName, operation, error ); } } /** * Safely gets error status code handling common edge cases * @param error Error object * @returns HTTP status code */ export function getErrorStatusCode(error: unknown): HttpStatusCode { if (!error) { return HttpStatusCode.INTERNAL_SERVER_ERROR; } // For our custom error types if (typeof error === 'object' && 'statusCode' in error && typeof error.statusCode === 'number') { return error.statusCode as HttpStatusCode; } // For AWS SDK errors with HTTP status codes if (typeof error === 'object' && error !== null && '$metadata' in error && typeof error.$metadata === 'object' && error.$metadata !== null && 'httpStatusCode' in error.$metadata && typeof error.$metadata.httpStatusCode === 'number') { const httpStatus = error.$metadata.httpStatusCode; // Only return if it's a valid HTTP status code if (httpStatus >= 100 && httpStatus < 600) { return httpStatus as HttpStatusCode; } } // Default status code return HttpStatusCode.INTERNAL_SERVER_ERROR; }