UNPKG

@rhofkens/mcp-quotes-server-claude-code

Version:

Model Context Protocol (MCP) server for managing and serving quotes

378 lines 11.8 kB
/** * Comprehensive Error Handling Utilities * * Advanced error handling with retry logic, structured responses, and debugging capabilities */ import { BaseError, ErrorCode, APIError } from './errors.js'; import { logger } from './logger.js'; /** * Default retry configuration */ export const DEFAULT_RETRY_CONFIG = { maxRetries: 3, initialDelay: 1000, // 1 second maxDelay: 30000, // 30 seconds backoffMultiplier: 2, retryableErrors: [ ErrorCode.API_TIMEOUT, ErrorCode.API_RATE_LIMIT, ErrorCode.RESOURCE_UNAVAILABLE, ], }; /** * Error context builder */ export class ErrorContextBuilder { context = {}; setOperation(operation) { this.context.operation = operation; return this; } setInput(input) { this.context.input = input; return this; } setEnvironment() { this.context.environment = { nodeVersion: process.version, platform: process.platform, timestamp: new Date().toISOString(), }; return this; } setStackTrace(error) { if (error.stack) { this.context.stackTrace = error.stack .split('\n') .filter((line) => line.trim()) .slice(0, 10); // Limit stack trace depth } return this; } addRelatedError(code, message) { if (!this.context.relatedErrors) { this.context.relatedErrors = []; } this.context.relatedErrors.push({ code, message, timestamp: new Date().toISOString(), }); return this; } build() { return { operation: this.context.operation || 'unknown', ...this.context, }; } } /** * Generate recovery suggestions based on error type */ export function generateRecoverySuggestions(error) { const recovery = { suggestions: [], retryable: false, alternativeActions: [], }; switch (error.code) { case ErrorCode.API_RATE_LIMIT: recovery.suggestions = [ 'Wait for the rate limit to reset', 'Consider implementing request queuing', 'Upgrade your API plan for higher limits', ]; recovery.retryable = true; recovery.retryAfter = 60; // Default to 60 seconds break; case ErrorCode.API_TIMEOUT: recovery.suggestions = [ 'Check your internet connection', 'Try again with a smaller request', 'Increase the timeout configuration', ]; recovery.retryable = true; break; case ErrorCode.API_UNAUTHORIZED: recovery.suggestions = [ 'Verify your API key is correct', 'Check if your API key has expired', 'Ensure the API key has the required permissions', ]; recovery.retryable = false; recovery.documentation = 'https://serper.dev/docs/authentication'; break; case ErrorCode.VALIDATION_ERROR: recovery.suggestions = [ 'Check the input parameters match the expected format', 'Ensure all required fields are provided', 'Verify data types are correct', ]; recovery.retryable = false; break; case ErrorCode.MISSING_ENV_VAR: recovery.suggestions = [ 'Set the required environment variable', 'Check the .env file configuration', 'Verify environment variable names are correct', ]; recovery.retryable = false; recovery.documentation = 'https://docs.example.com/configuration'; break; default: recovery.suggestions = [ 'Try the operation again', 'Check the logs for more details', 'Contact support if the issue persists', ]; recovery.retryable = true; } return recovery; } /** * Create a structured error response */ export function createStructuredError(error, context, requestId) { const baseError = error instanceof BaseError ? error : new BaseError(error instanceof Error ? error.message : String(error), ErrorCode.UNKNOWN_ERROR); const recovery = generateRecoverySuggestions(baseError); return { error: { code: baseError.code, message: baseError.message, userMessage: baseError.getUserMessage(), timestamp: new Date().toISOString(), ...(requestId && { requestId }), ...(baseError.details && { details: baseError.details }), ...(context && { context }), recovery, }, }; } /** * Retry logic with exponential backoff */ export async function withRetry(operation, config = {}) { const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config }; let lastError; for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if error is retryable if (!isRetryableError(error, retryConfig.retryableErrors)) { throw error; } // Don't retry if we've exhausted attempts if (attempt === retryConfig.maxRetries) { break; } // Calculate delay with exponential backoff const delay = Math.min(retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attempt), retryConfig.maxDelay); // Log retry attempt logger.warn(`Retrying operation after ${delay}ms`, { attempt: attempt + 1, maxRetries: retryConfig.maxRetries, error: lastError.message, }); // Call retry callback if provided if (retryConfig.onRetry) { retryConfig.onRetry(lastError, attempt + 1); } // Wait before retrying await sleep(delay); } } // All retries exhausted throw new BaseError(`Operation failed after ${retryConfig.maxRetries} retries: ${lastError?.message}`, ErrorCode.INTERNAL_ERROR, 500, { lastError: lastError?.message, attempts: retryConfig.maxRetries + 1, }); } /** * Check if an error is retryable */ function isRetryableError(error, retryableErrors = []) { if (error instanceof BaseError) { return retryableErrors.includes(error.code); } // Network errors are usually retryable if (error instanceof Error) { const networkErrors = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED']; return networkErrors.some((code) => error.message.includes(code)); } return false; } /** * Sleep utility for delays */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Enhanced error logger with context */ export function logError(error, context, severity = 'error') { const baseError = error instanceof BaseError ? error : new BaseError(error instanceof Error ? error.message : String(error), ErrorCode.UNKNOWN_ERROR); const errorContext = new ErrorContextBuilder() .setOperation(context?.operation || 'unknown') .setInput(context?.input || {}) .setEnvironment() .setStackTrace(baseError) .build(); const logData = { code: baseError.code, message: baseError.message, statusCode: baseError.statusCode, details: baseError.details, context: errorContext, }; switch (severity) { case 'warn': logger.warn('Error occurred', logData); break; case 'fatal': logger.error('Fatal error occurred', logData); // In production, you might want to trigger alerts here break; default: logger.error('Error occurred', logData); } } /** * Circuit breaker pattern for API calls */ export class CircuitBreaker { threshold; resetTimeout; failures = 0; lastFailureTime = 0; state = 'closed'; constructor(threshold = 5, _breakerTimeout = 60000, // 1 minute (unused but kept for compatibility) resetTimeout = 30000 // 30 seconds ) { this.threshold = threshold; this.resetTimeout = resetTimeout; } async execute(operation) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime > this.resetTimeout) { this.state = 'half-open'; } else { throw new APIError('Circuit breaker is open - service temporarily unavailable', ErrorCode.RESOURCE_UNAVAILABLE); } } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failures = 0; this.state = 'closed'; } onFailure() { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.threshold) { this.state = 'open'; logger.warn('Circuit breaker opened', { failures: this.failures, threshold: this.threshold, }); } } getState() { return this.state; } reset() { this.failures = 0; this.state = 'closed'; this.lastFailureTime = 0; } } /** * Error aggregator for batch operations */ export class ErrorAggregator { errors = []; add(error, context) { this.errors.push({ error, context }); } hasErrors() { return this.errors.length > 0; } getErrors() { return [...this.errors]; } clear() { this.errors = []; } /** * Throw an aggregated error if any errors exist */ throwIfAny(message) { if (this.hasErrors()) { throw new BaseError(message, ErrorCode.INTERNAL_ERROR, 500, { errors: this.errors.map(({ error, context }) => ({ message: error.message, type: error.name, context, })), count: this.errors.length, }); } } } /** * Timeout wrapper for async operations */ export function withTimeout(operation, timeoutMs, errorMessage = 'Operation timed out') { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new APIError(errorMessage, ErrorCode.API_TIMEOUT)); }, timeoutMs); }); return Promise.race([operation, timeoutPromise]); } /** * Error boundary for async operations */ export async function errorBoundary(operation, fallback, onError) { try { return await operation(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); if (onError) { onError(err); } else { logError(err, { operation: 'errorBoundary' }, 'warn'); } return fallback; } } /** * Create a request ID for tracking */ export function generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Export all error types for convenience */ export * from './errors.js'; //# sourceMappingURL=errorHandling.js.map