/** * Global Error Handling Middleware * Comprehensive error handling with proper logging, sanitization, and response formatting */ const logger = require('../utils/logger'); /** * Custom error classes for better error handling */ class ValidationError extends Error { constructor(message, details = null) { super(message); this.name = 'ValidationError'; this.statusCode = 400; this.details = details; } } class AuthenticationError extends Error { constructor(message = 'Authentication failed') { super(message); this.name = 'AuthenticationError'; this.statusCode = 401; } } class AuthorizationError extends Error { constructor(message = 'Access denied') { super(message); this.name = 'AuthorizationError'; this.statusCode = 403; } } class NotFoundError extends Error { constructor(message = 'Resource not found') { super(message); this.name = 'NotFoundError'; this.statusCode = 404; } } class ConflictError extends Error { constructor(message = 'Resource conflict') { super(message); this.name = 'ConflictError'; this.statusCode = 409; } } class RateLimitError extends Error { constructor(message = 'Rate limit exceeded') { super(message); this.name = 'RateLimitError'; this.statusCode = 429; } } class ServiceError extends Error { constructor(message = 'Internal service error', originalError = null) { super(message); this.name = 'ServiceError'; this.statusCode = 500; this.originalError = originalError; } } class DatabaseError extends Error { constructor(message = 'Database operation failed', originalError = null) { super(message); this.name = 'DatabaseError'; this.statusCode = 500; this.originalError = originalError; } } /** * Main error handling middleware * @param {Error} error - Error object * @param {Object} req - Express request object * @param {Object} res - Express response object * @param {Function} next - Express next function */ function errorHandler(error, req, res, next) { const correlationId = req.correlationId || 'unknown'; const startTime = Date.now(); // Don't handle if response already sent if (res.headersSent) { logger.error('Error occurred after response sent', { correlationId, error: error.message, stack: error.stack, }); return next(error); } // Log the error logError(error, req, correlationId); // Determine error details const errorResponse = createErrorResponse(error, req, correlationId); // Set appropriate headers res.set({ 'Content-Type': 'application/json', 'X-Correlation-ID': correlationId, }); // Send error response res.status(errorResponse.statusCode).json(errorResponse.body); // Log response time for error handling const duration = Date.now() - startTime; logger.info('Error response sent', { correlationId, statusCode: errorResponse.statusCode, duration: `${duration}ms`, }); } /** * Log error with appropriate detail level * @param {Error} error - Error object * @param {Object} req - Express request object * @param {string} correlationId - Request correlation ID */ function logError(error, req, correlationId) { const errorInfo = { correlationId, name: error.name, message: error.message, statusCode: error.statusCode || 500, method: req.method, url: req.originalUrl, path: req.path, ip: req.ip, userAgent: req.get('User-Agent'), userId: req.user?.playerId || req.user?.adminId, userType: req.user?.type, timestamp: new Date().toISOString(), }; // Add stack trace for server errors if (!error.statusCode || error.statusCode >= 500) { errorInfo.stack = error.stack; // Add original error if available if (error.originalError) { errorInfo.originalError = { name: error.originalError.name, message: error.originalError.message, stack: error.originalError.stack, }; } } // Add request body for debugging (sanitized) if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) { errorInfo.requestBody = sanitizeForLogging(req.body); } // Add query parameters if (Object.keys(req.query).length > 0) { errorInfo.queryParams = req.query; } // Determine log level const statusCode = error.statusCode || 500; if (statusCode >= 500) { logger.error('Server error occurred', errorInfo); } else if (statusCode >= 400) { logger.warn('Client error occurred', errorInfo); } else { logger.info('Request completed with error', errorInfo); } // Audit sensitive errors if (shouldAuditError(error, req)) { logger.audit('Error occurred', { ...errorInfo, audit: true, }); } } /** * Create standardized error response * @param {Error} error - Error object * @param {Object} req - Express request object * @param {string} correlationId - Request correlation ID * @returns {Object} Error response object */ function createErrorResponse(error, req, correlationId) { const statusCode = determineStatusCode(error); const isDevelopment = process.env.NODE_ENV === 'development'; const isProduction = process.env.NODE_ENV === 'production'; const baseResponse = { error: true, correlationId, timestamp: new Date().toISOString(), }; // Handle different error types switch (error.name) { case 'ValidationError': return { statusCode: 400, body: { ...baseResponse, type: 'ValidationError', message: 'Request validation failed', details: error.details || error.message, }, }; case 'AuthenticationError': return { statusCode: 401, body: { ...baseResponse, type: 'AuthenticationError', message: isProduction ? 'Authentication required' : error.message, }, }; case 'AuthorizationError': return { statusCode: 403, body: { ...baseResponse, type: 'AuthorizationError', message: isProduction ? 'Access denied' : error.message, }, }; case 'NotFoundError': return { statusCode: 404, body: { ...baseResponse, type: 'NotFoundError', message: error.message || 'Resource not found', }, }; case 'ConflictError': return { statusCode: 409, body: { ...baseResponse, type: 'ConflictError', message: error.message || 'Resource conflict', }, }; case 'RateLimitError': return { statusCode: 429, body: { ...baseResponse, type: 'RateLimitError', message: error.message || 'Rate limit exceeded', retryAfter: error.retryAfter, }, }; // Database errors case 'DatabaseError': case 'SequelizeError': case 'QueryFailedError': return { statusCode: 500, body: { ...baseResponse, type: 'DatabaseError', message: isProduction ? 'Database operation failed' : error.message, ...(isDevelopment && { stack: error.stack }), }, }; // JWT errors case 'JsonWebTokenError': case 'TokenExpiredError': case 'NotBeforeError': return { statusCode: 401, body: { ...baseResponse, type: 'TokenError', message: 'Invalid or expired token', }, }; // Multer errors (file upload) case 'MulterError': return { statusCode: 400, body: { ...baseResponse, type: 'FileUploadError', message: getMulterErrorMessage(error), }, }; // Default server error default: return { statusCode: statusCode >= 400 ? statusCode : 500, body: { ...baseResponse, type: 'ServerError', message: isProduction ? 'Internal server error' : error.message, ...(isDevelopment && { stack: error.stack, originalError: error.originalError, }), }, }; } } /** * Determine HTTP status code from error * @param {Error} error - Error object * @returns {number} HTTP status code */ function determineStatusCode(error) { // Use explicit status code if available if (error.statusCode && typeof error.statusCode === 'number') { return error.statusCode; } // Use status property if available if (error.status && typeof error.status === 'number') { return error.status; } // Default mappings by error name const statusMappings = { ValidationError: 400, CastError: 400, JsonWebTokenError: 401, TokenExpiredError: 401, UnauthorizedError: 401, AuthenticationError: 401, ForbiddenError: 403, AuthorizationError: 403, NotFoundError: 404, ConflictError: 409, MulterError: 400, RateLimitError: 429, }; return statusMappings[error.name] || 500; } /** * Get user-friendly message for Multer errors * @param {Error} error - Multer error * @returns {string} User-friendly error message */ function getMulterErrorMessage(error) { switch (error.code) { case 'LIMIT_FILE_SIZE': return 'File size too large'; case 'LIMIT_FILE_COUNT': return 'Too many files uploaded'; case 'LIMIT_FIELD_KEY': return 'Field name too long'; case 'LIMIT_FIELD_VALUE': return 'Field value too long'; case 'LIMIT_FIELD_COUNT': return 'Too many fields'; case 'LIMIT_UNEXPECTED_FILE': return 'Unexpected file field'; default: return 'File upload error'; } } /** * Sanitize data for logging (remove sensitive information) * @param {Object} data - Data to sanitize * @returns {Object} Sanitized data */ function sanitizeForLogging(data) { if (!data || typeof data !== 'object') return data; try { const sanitized = JSON.parse(JSON.stringify(data)); const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization']; function recursiveSanitize(obj) { if (typeof obj !== 'object' || obj === null) return obj; Object.keys(obj).forEach(key => { if (sensitiveFields.some(field => key.toLowerCase().includes(field))) { obj[key] = '[REDACTED]'; } else if (typeof obj[key] === 'object') { recursiveSanitize(obj[key]); } }); return obj; } return recursiveSanitize(sanitized); } catch { return '[SANITIZATION_ERROR]'; } } /** * Determine if error should be audited * @param {Error} error - Error object * @param {Object} req - Express request object * @returns {boolean} True if should audit */ function shouldAuditError(error, req) { const statusCode = error.statusCode || 500; // Audit all server errors if (statusCode >= 500) return true; // Audit authentication/authorization errors if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) { return true; } // Audit admin-related errors if (req.user?.type === 'admin') return true; // Audit security-related endpoints if (req.path.includes('/auth/') || req.path.includes('/admin/')) { return true; } return false; } /** * Handle async errors in route handlers * @param {Function} fn - Async route handler function * @returns {Function} Wrapped route handler */ function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } /** * 404 Not Found handler * @param {Object} req - Express request object * @param {Object} res - Express response object * @param {Function} next - Express next function */ function notFoundHandler(req, res, next) { const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`); next(error); } module.exports = { errorHandler, notFoundHandler, asyncHandler, // Export error classes ValidationError, AuthenticationError, AuthorizationError, NotFoundError, ConflictError, RateLimitError, ServiceError, DatabaseError, };