UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

716 lines (628 loc) 25.3 kB
/** * Comprehensive error handling for ZPT navigation API */ import { logger } from '../../Utils.js'; export default class ErrorHandler { constructor(options = {}) { this.config = { logErrors: options.logErrors !== false, includeStackTrace: options.includeStackTrace || false, enableErrorReporting: options.enableErrorReporting || false, sanitizeErrorMessages: options.sanitizeErrorMessages !== false, maxErrorMessageLength: options.maxErrorMessageLength || 500, enableRecovery: options.enableRecovery !== false, ...options }; this.initializeErrorTypes(); this.initializeErrorCodes(); this.initializeRecoveryStrategies(); // Error statistics this.errorStats = { totalErrors: 0, errorsByType: new Map(), errorsByCode: new Map(), recentErrors: [] }; } /** * Initialize error type definitions */ initializeErrorTypes() { this.errorTypes = { VALIDATION_ERROR: { statusCode: 400, category: 'client', severity: 'medium', recoverable: true, description: 'Request validation failed' }, AUTHENTICATION_ERROR: { statusCode: 401, category: 'client', severity: 'medium', recoverable: false, description: 'Authentication required or failed' }, AUTHORIZATION_ERROR: { statusCode: 403, category: 'client', severity: 'medium', recoverable: false, description: 'Insufficient permissions' }, NOT_FOUND_ERROR: { statusCode: 404, category: 'client', severity: 'low', recoverable: false, description: 'Resource not found' }, RATE_LIMIT_ERROR: { statusCode: 429, category: 'client', severity: 'medium', recoverable: true, description: 'Rate limit exceeded' }, PARAMETER_ERROR: { statusCode: 400, category: 'client', severity: 'medium', recoverable: true, description: 'Invalid navigation parameters' }, CORPUS_ERROR: { statusCode: 503, category: 'server', severity: 'high', recoverable: true, description: 'Corpus access error' }, PROCESSING_ERROR: { statusCode: 500, category: 'server', severity: 'high', recoverable: true, description: 'Navigation processing failed' }, TIMEOUT_ERROR: { statusCode: 408, category: 'server', severity: 'medium', recoverable: true, description: 'Request timeout' }, RESOURCE_ERROR: { statusCode: 507, category: 'server', severity: 'high', recoverable: false, description: 'Insufficient resources' }, CONFIGURATION_ERROR: { statusCode: 500, category: 'server', severity: 'critical', recoverable: false, description: 'System configuration error' }, EXTERNAL_SERVICE_ERROR: { statusCode: 502, category: 'server', severity: 'high', recoverable: true, description: 'External service unavailable' }, UNKNOWN_ERROR: { statusCode: 500, category: 'server', severity: 'high', recoverable: false, description: 'Unknown system error' } }; } /** * Initialize specific error codes */ initializeErrorCodes() { this.errorCodes = { // Validation errors (4000-4099) 'INVALID_ZOOM_LEVEL': { type: 'VALIDATION_ERROR', code: 4001 }, 'INVALID_TILT_REPRESENTATION': { type: 'VALIDATION_ERROR', code: 4002 }, 'INVALID_PAN_FILTER': { type: 'VALIDATION_ERROR', code: 4003 }, 'INVALID_TRANSFORM_PARAMS': { type: 'VALIDATION_ERROR', code: 4004 }, 'MISSING_REQUIRED_PARAM': { type: 'VALIDATION_ERROR', code: 4005 }, 'INVALID_TOKEN_LIMIT': { type: 'VALIDATION_ERROR', code: 4006 }, 'INVALID_FORMAT': { type: 'VALIDATION_ERROR', code: 4007 }, 'INVALID_DATE_RANGE': { type: 'VALIDATION_ERROR', code: 4008 }, 'INVALID_GEOGRAPHIC_BOUNDS': { type: 'VALIDATION_ERROR', code: 4009 }, // Parameter errors (4100-4199) 'PARAMETER_VALIDATION_FAILED': { type: 'PARAMETER_ERROR', code: 4101 }, 'PARAMETER_NORMALIZATION_FAILED': { type: 'PARAMETER_ERROR', code: 4102 }, 'INCOMPATIBLE_PARAMETERS': { type: 'PARAMETER_ERROR', code: 4103 }, 'PARAMETER_LIMIT_EXCEEDED': { type: 'PARAMETER_ERROR', code: 4104 }, // Corpus errors (5000-5099) 'CORPUS_UNAVAILABLE': { type: 'CORPUS_ERROR', code: 5001 }, 'CORPUS_QUERY_FAILED': { type: 'CORPUS_ERROR', code: 5002 }, 'CORPUS_TIMEOUT': { type: 'CORPUS_ERROR', code: 5003 }, 'SPARQL_ENDPOINT_ERROR': { type: 'CORPUS_ERROR', code: 5004 }, 'EMBEDDING_SERVICE_ERROR': { type: 'CORPUS_ERROR', code: 5005 }, // Processing errors (5100-5199) 'SELECTION_FAILED': { type: 'PROCESSING_ERROR', code: 5101 }, 'PROJECTION_FAILED': { type: 'PROCESSING_ERROR', code: 5102 }, 'TRANSFORMATION_FAILED': { type: 'PROCESSING_ERROR', code: 5103 }, 'CHUNKING_FAILED': { type: 'PROCESSING_ERROR', code: 5104 }, 'FORMATTING_FAILED': { type: 'PROCESSING_ERROR', code: 5105 }, 'ENCODING_FAILED': { type: 'PROCESSING_ERROR', code: 5106 }, 'TOKEN_COUNT_EXCEEDED': { type: 'PROCESSING_ERROR', code: 5107 }, // Resource errors (5200-5299) 'MEMORY_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5201 }, 'CPU_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5202 }, 'DISK_SPACE_ERROR': { type: 'RESOURCE_ERROR', code: 5203 }, 'CONNECTION_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5204 }, // Configuration errors (5300-5399) 'MISSING_DEPENDENCY': { type: 'CONFIGURATION_ERROR', code: 5301 }, 'INVALID_CONFIGURATION': { type: 'CONFIGURATION_ERROR', code: 5302 }, 'SERVICE_NOT_INITIALIZED': { type: 'CONFIGURATION_ERROR', code: 5303 }, // External service errors (5400-5499) 'LLM_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5401 }, 'EMBEDDING_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5402 }, 'SPARQL_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5403 } }; } /** * Initialize error recovery strategies */ initializeRecoveryStrategies() { this.recoveryStrategies = { 'VALIDATION_ERROR': this.recoverFromValidationError.bind(this), 'PARAMETER_ERROR': this.recoverFromParameterError.bind(this), 'CORPUS_ERROR': this.recoverFromCorpusError.bind(this), 'PROCESSING_ERROR': this.recoverFromProcessingError.bind(this), 'TIMEOUT_ERROR': this.recoverFromTimeoutError.bind(this), 'EXTERNAL_SERVICE_ERROR': this.recoverFromExternalServiceError.bind(this) }; } /** * Main error handling method * @param {Error} error - The error to handle * @param {Object} req - HTTP request object * @param {Object} res - HTTP response object * @param {Object} context - Additional context */ async handleError(error, req, res, context = {}) { const errorId = this.generateErrorId(); const timestamp = new Date().toISOString(); try { // Normalize error const normalizedError = this.normalizeError(error, context); // Log error if (this.config.logErrors) { this.logError(normalizedError, errorId, req, context); } // Update statistics this.updateErrorStats(normalizedError); // Attempt recovery if enabled let recoveredError = normalizedError; if (this.config.enableRecovery && normalizedError.recoverable) { recoveredError = await this.attemptRecovery(normalizedError, req, context); } // Format error response const errorResponse = this.formatErrorResponse(recoveredError, errorId, context); // Send response this.sendErrorResponse(res, errorResponse, recoveredError.statusCode); // Report error if enabled if (this.config.enableErrorReporting) { await this.reportError(recoveredError, errorId, req, context); } } catch (handlingError) { // Fallback error handling logger.error('Error handler failed', { originalError: error.message, handlingError: handlingError.message, errorId }); this.sendFallbackErrorResponse(res, errorId); } } /** * Normalize error into standard format */ normalizeError(error, context) { // Determine error type and code const { errorType, errorCode } = this.classifyError(error, context); const typeInfo = this.errorTypes[errorType]; return { id: this.generateErrorId(), type: errorType, code: errorCode, message: this.sanitizeErrorMessage(error.message), originalMessage: error.message, statusCode: context.statusCode || typeInfo.statusCode, category: typeInfo.category, severity: typeInfo.severity, recoverable: typeInfo.recoverable, timestamp: new Date().toISOString(), stackTrace: this.config.includeStackTrace ? error.stack : undefined, context: this.sanitizeContext(context), metadata: { field: error.field, details: error.details, cause: error.cause?.message } }; } /** * Classify error type based on error properties */ classifyError(error, context) { // Check for explicit error codes if (error.code && this.errorCodes[error.code]) { const codeInfo = this.errorCodes[error.code]; return { errorType: codeInfo.type, errorCode: error.code }; } // Check for status code override if (context.statusCode) { if (context.statusCode === 404) return { errorType: 'NOT_FOUND_ERROR', errorCode: 'NOT_FOUND' }; if (context.statusCode === 429) return { errorType: 'RATE_LIMIT_ERROR', errorCode: 'RATE_LIMIT_EXCEEDED' }; } // Pattern matching on error message const message = error.message.toLowerCase(); if (message.includes('validation') || message.includes('invalid')) { return { errorType: 'VALIDATION_ERROR', errorCode: 'VALIDATION_FAILED' }; } if (message.includes('parameter')) { return { errorType: 'PARAMETER_ERROR', errorCode: 'PARAMETER_ERROR' }; } if (message.includes('timeout') || message.includes('timed out')) { return { errorType: 'TIMEOUT_ERROR', errorCode: 'REQUEST_TIMEOUT' }; } if (message.includes('corpus') || message.includes('sparql')) { return { errorType: 'CORPUS_ERROR', errorCode: 'CORPUS_ACCESS_ERROR' }; } if (message.includes('processing') || message.includes('transformation')) { return { errorType: 'PROCESSING_ERROR', errorCode: 'PROCESSING_FAILED' }; } if (message.includes('memory') || message.includes('resource')) { return { errorType: 'RESOURCE_ERROR', errorCode: 'RESOURCE_EXHAUSTED' }; } if (message.includes('service') || message.includes('unavailable')) { return { errorType: 'EXTERNAL_SERVICE_ERROR', errorCode: 'SERVICE_UNAVAILABLE' }; } // Default classification return { errorType: 'UNKNOWN_ERROR', errorCode: 'UNKNOWN' }; } /** * Attempt error recovery */ async attemptRecovery(normalizedError, req, context) { const recoveryStrategy = this.recoveryStrategies[normalizedError.type]; if (!recoveryStrategy) { return normalizedError; } try { const recoveryResult = await recoveryStrategy(normalizedError, req, context); if (recoveryResult.recovered) { return { ...normalizedError, recovered: true, recoveryMessage: recoveryResult.message, fallbackData: recoveryResult.data }; } } catch (recoveryError) { logger.warn('Error recovery failed', { originalError: normalizedError.code, recoveryError: recoveryError.message }); } return normalizedError; } /** * Recovery strategy implementations */ async recoverFromValidationError(error, req, context) { // Try to provide corrected parameters if (error.code === 'INVALID_ZOOM_LEVEL') { return { recovered: true, message: 'Suggested valid zoom levels', data: { validZoomLevels: ['entity', 'unit', 'text', 'community', 'corpus'], suggestion: 'unit' } }; } if (error.code === 'INVALID_TILT_REPRESENTATION') { return { recovered: true, message: 'Suggested valid tilt representations', data: { validTilts: ['embedding', 'keywords', 'graph', 'temporal'], suggestion: 'keywords' } }; } return { recovered: false }; } async recoverFromParameterError(error, req, context) { // Try to provide default parameters return { recovered: true, message: 'Using default parameters', data: { defaultParams: { zoom: 'unit', tilt: 'keywords', transform: { maxTokens: 4000, format: 'structured' } } } }; } async recoverFromCorpusError(error, req, context) { // Try alternative data sources or cached results return { recovered: true, message: 'Using cached corpus data', data: { source: 'cache', freshness: 'stale', limitation: 'Limited results available from cache' } }; } async recoverFromProcessingError(error, req, context) { // Try simplified processing return { recovered: true, message: 'Using simplified processing mode', data: { mode: 'simplified', limitations: ['No chunking', 'Basic formatting', 'Reduced metadata'] } }; } async recoverFromTimeoutError(error, req, context) { // Suggest pagination or reduced scope return { recovered: true, message: 'Request timeout - suggest reducing scope', data: { suggestions: [ 'Reduce maxTokens parameter', 'Use more specific pan filters', 'Try pagination for large results' ] } }; } async recoverFromExternalServiceError(error, req, context) { // Try fallback services return { recovered: true, message: 'Using fallback processing mode', data: { mode: 'fallback', limitations: ['No embeddings', 'Basic text processing only'] } }; } /** * Format error response */ formatErrorResponse(normalizedError, errorId, context) { const response = { success: false, error: { id: errorId, code: normalizedError.code, type: normalizedError.type, message: normalizedError.message, timestamp: normalizedError.timestamp, statusCode: normalizedError.statusCode }, requestId: context.requestId }; // Add recovery information if (normalizedError.recovered) { response.recovery = { recovered: true, message: normalizedError.recoveryMessage, fallbackData: normalizedError.fallbackData }; } // Add field-specific information if (normalizedError.metadata.field) { response.error.field = normalizedError.metadata.field; } // Add details in debug mode if (this.config.includeStackTrace && normalizedError.stackTrace) { response.error.stackTrace = normalizedError.stackTrace; } // Add suggestions based on error type response.suggestions = this.generateErrorSuggestions(normalizedError); // Add rate limit information if (context.rateLimitInfo) { response.rateLimit = context.rateLimitInfo; } return response; } /** * Generate helpful suggestions based on error type */ generateErrorSuggestions(error) { const suggestions = []; switch (error.type) { case 'VALIDATION_ERROR': suggestions.push('Check parameter format against API schema'); suggestions.push('Use /api/navigate/schema endpoint for parameter documentation'); break; case 'PARAMETER_ERROR': suggestions.push('Verify zoom/pan/tilt parameter combinations'); suggestions.push('Use /api/navigate/options endpoint for valid values'); break; case 'RATE_LIMIT_ERROR': suggestions.push('Reduce request frequency'); suggestions.push('Implement exponential backoff'); break; case 'TIMEOUT_ERROR': suggestions.push('Reduce scope of request (smaller maxTokens)'); suggestions.push('Use more specific filters to narrow results'); break; case 'CORPUS_ERROR': suggestions.push('Check system status'); suggestions.push('Try again in a few moments'); break; case 'PROCESSING_ERROR': suggestions.push('Simplify navigation parameters'); suggestions.push('Try smaller token limits'); break; } return suggestions; } /** * Utility methods */ sanitizeErrorMessage(message) { if (!this.config.sanitizeErrorMessages) return message; // Remove potentially sensitive information let sanitized = message.replace(/password[=:]\s*\S+/gi, 'password=***'); sanitized = sanitized.replace(/token[=:]\s*\S+/gi, 'token=***'); sanitized = sanitized.replace(/key[=:]\s*\S+/gi, 'key=***'); // Truncate if too long if (sanitized.length > this.config.maxErrorMessageLength) { sanitized = sanitized.substring(0, this.config.maxErrorMessageLength) + '...'; } return sanitized; } sanitizeContext(context) { const sanitized = { ...context }; // Remove sensitive fields delete sanitized.password; delete sanitized.token; delete sanitized.key; delete sanitized.authorization; return sanitized; } logError(error, errorId, req, context) { const logData = { errorId, type: error.type, code: error.code, message: error.message, severity: error.severity, statusCode: error.statusCode, requestId: context.requestId, method: req?.method, path: req?.url, userAgent: req?.headers?.['user-agent'], timestamp: error.timestamp }; switch (error.severity) { case 'critical': logger.error('Critical error occurred', logData); break; case 'high': logger.error('High severity error', logData); break; case 'medium': logger.warn('Medium severity error', logData); break; case 'low': logger.info('Low severity error', logData); break; default: logger.error('Error occurred', logData); } } updateErrorStats(error) { this.errorStats.totalErrors++; // Update error type counts const typeCount = this.errorStats.errorsByType.get(error.type) || 0; this.errorStats.errorsByType.set(error.type, typeCount + 1); // Update error code counts const codeCount = this.errorStats.errorsByCode.get(error.code) || 0; this.errorStats.errorsByCode.set(error.code, codeCount + 1); // Keep recent errors (last 100) this.errorStats.recentErrors.push({ id: error.id, type: error.type, code: error.code, timestamp: error.timestamp }); if (this.errorStats.recentErrors.length > 100) { this.errorStats.recentErrors.shift(); } } sendErrorResponse(res, errorResponse, statusCode) { res.setHeader('Content-Type', 'application/json'); res.statusCode = statusCode || 500; res.end(JSON.stringify(errorResponse, null, 2)); } sendFallbackErrorResponse(res, errorId) { const fallbackResponse = { success: false, error: { id: errorId, code: 'SYSTEM_ERROR', message: 'An unexpected system error occurred', timestamp: new Date().toISOString() } }; res.setHeader('Content-Type', 'application/json'); res.statusCode = 500; res.end(JSON.stringify(fallbackResponse)); } generateErrorId() { return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async reportError(error, errorId, req, context) { // Placeholder for external error reporting // Could integrate with services like Sentry, Rollbar, etc. logger.info('Error reported', { errorId, type: error.type, code: error.code }); } /** * Statistics and monitoring */ getErrorStats() { return { total: this.errorStats.totalErrors, byType: Object.fromEntries(this.errorStats.errorsByType), byCode: Object.fromEntries(this.errorStats.errorsByCode), recent: this.errorStats.recentErrors.slice(-10) // Last 10 errors }; } getErrorTypeInfo(errorType) { return this.errorTypes[errorType] ? { ...this.errorTypes[errorType] } : null; } getErrorCodeInfo(errorCode) { return this.errorCodes[errorCode] ? { ...this.errorCodes[errorCode] } : null; } resetStats() { this.errorStats = { totalErrors: 0, errorsByType: new Map(), errorsByCode: new Map(), recentErrors: [] }; } /** * Configuration methods */ updateConfig(newConfig) { Object.assign(this.config, newConfig); } getErrorHandlerInfo() { return { config: this.config, errorTypes: Object.keys(this.errorTypes), errorCodes: Object.keys(this.errorCodes), recoveryStrategies: Object.keys(this.recoveryStrategies), statistics: this.getErrorStats() }; } }