@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
378 lines • 11.8 kB
JavaScript
/**
* 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