Skip to main content
Glama
ErrorHandler.ts7.84 kB
/** * Unified Error Handler for API requests * Provides consistent error handling across all platforms */ import { sanitizeRequest, maskSensitiveData } from './SecurityUtils.js'; import { logError as loggerError } from './Logger.js'; /** * API Error codes and their meanings */ export const HTTP_ERROR_CODES = { 400: 'Bad Request - Invalid parameters or syntax', 401: 'Unauthorized - Invalid or missing API key', 403: 'Forbidden - Access denied or rate limit exceeded', 404: 'Not Found - Resource does not exist', 405: 'Method Not Allowed - HTTP method not supported', 408: 'Request Timeout - Server took too long to respond', 429: 'Too Many Requests - Rate limit exceeded', 500: 'Internal Server Error - Server error', 502: 'Bad Gateway - Server communication error', 503: 'Service Unavailable - Server temporarily unavailable', 504: 'Gateway Timeout - Server timeout' } as const; /** * Custom API Error class with detailed information */ export class ApiError extends Error { public readonly status?: number; public readonly platform: string; public readonly operation: string; public readonly timestamp: string; public readonly retryable: boolean; public readonly details?: any; constructor(options: { message: string; status?: number; platform: string; operation: string; details?: any; }) { super(options.message); this.name = 'ApiError'; this.status = options.status; this.platform = options.platform; this.operation = options.operation; this.timestamp = new Date().toISOString(); this.details = options.details; // Determine if error is retryable this.retryable = this.isRetryable(options.status); } private isRetryable(status?: number): boolean { if (!status) return true; // Retryable: rate limits, timeouts, server errors return [408, 429, 500, 502, 503, 504].includes(status); } toJSON() { return { name: this.name, message: this.message, status: this.status, platform: this.platform, operation: this.operation, timestamp: this.timestamp, retryable: this.retryable }; } } /** * Error Handler class for unified error processing */ export class ErrorHandler { private platform: string; private verbose: boolean; constructor(platform: string, verbose: boolean = false) { this.platform = platform; this.verbose = verbose || process.env.NODE_ENV === 'development'; } /** * Handle HTTP errors from axios or similar libraries */ handleHttpError(error: any, operation: string): never { const status = error.response?.status; const responseMessage = this.extractErrorMessage(error); const url = error.config?.url; const method = error.config?.method?.toUpperCase() || 'GET'; // Sanitize sensitive data before logging const sanitizedConfig = sanitizeRequest(error.config); const sanitizedUrl = url ? this.sanitizeUrl(url) : 'unknown'; // Log error details (sanitized) this.logError({ status, message: responseMessage, url: sanitizedUrl, method, operation, config: this.verbose ? sanitizedConfig : undefined, responseData: this.verbose ? error.response?.data : undefined }); // Create user-friendly error message const userMessage = this.createUserMessage(status, responseMessage, operation); throw new ApiError({ message: userMessage, status, platform: this.platform, operation, details: this.verbose ? { url: sanitizedUrl, method } : undefined }); } /** * Handle generic errors */ handleError(error: any, operation: string): never { if (error.response) { // HTTP error this.handleHttpError(error, operation); } const message = error.message || 'Unknown error occurred'; this.logError({ message, operation, stack: this.verbose ? error.stack : undefined }); throw new ApiError({ message: `${this.platform} ${operation} failed: ${message}`, platform: this.platform, operation }); } /** * Extract error message from various error formats */ private extractErrorMessage(error: any): string { // Try different error message locations const candidates = [ error.response?.data?.message, error.response?.data?.error?.message, error.response?.data?.error, error.response?.data?.detail, error.response?.statusText, error.message ]; for (const candidate of candidates) { if (candidate && typeof candidate === 'string') { return candidate; } } return 'Unknown error'; } /** * Create user-friendly error message */ private createUserMessage(status: number | undefined, message: string, operation: string): string { const statusDesc = status ? HTTP_ERROR_CODES[status as keyof typeof HTTP_ERROR_CODES] : ''; // Platform-specific messages if (status === 401) { return `${this.platform}: Invalid or missing API key. Please check your credentials.`; } if (status === 403) { return `${this.platform}: Access forbidden. This may be due to rate limiting or insufficient permissions.`; } if (status === 404) { return `${this.platform}: Resource not found. The requested item may not exist.`; } if (status === 429) { return `${this.platform}: Rate limit exceeded. Please wait before making more requests.`; } if (status && status >= 500) { return `${this.platform}: Server error (${status}). The service may be temporarily unavailable.`; } // Generic message const prefix = `${this.platform} ${operation} failed`; const statusInfo = status ? ` (${status}${statusDesc ? ': ' + statusDesc : ''})` : ''; return `${prefix}${statusInfo}: ${maskSensitiveData(message)}`; } /** * Sanitize URL for logging */ private sanitizeUrl(url: string): string { try { const urlObj = new URL(url); // Remove sensitive query parameters const sensitiveParams = ['api_key', 'apikey', 'key', 'token', 'secret', 'auth']; sensitiveParams.forEach(param => { if (urlObj.searchParams.has(param)) { urlObj.searchParams.set(param, '***'); } }); return urlObj.toString(); } catch { // If URL parsing fails, mask the entire thing return '***sanitized-url***'; } } /** * Log error with consistent format */ private logError(details: Record<string, any>): void { loggerError(`[${this.platform}] Error:`, { timestamp: new Date().toISOString(), ...details }); } /** * Check if an error is retryable */ static isRetryable(error: any): boolean { if (error instanceof ApiError) { return error.retryable; } const status = error.response?.status; if (!status) return true; return [408, 429, 500, 502, 503, 504].includes(status); } /** * Get suggested retry delay based on error */ static getRetryDelay(error: any, attempt: number = 1): number { const status = error.response?.status || error.status; // For rate limiting, check Retry-After header if (status === 429) { const retryAfter = error.response?.headers?.['retry-after']; if (retryAfter) { const seconds = parseInt(retryAfter, 10); if (!isNaN(seconds)) { return seconds * 1000; } } // Default: exponential backoff for rate limits return Math.min(60000, 1000 * Math.pow(2, attempt)); } // For server errors, use exponential backoff if (status && status >= 500) { return Math.min(30000, 1000 * Math.pow(2, attempt)); } // Default delay return 1000 * attempt; } } export default ErrorHandler;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Dianel555/paper-search-mcp-nodejs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server