campverse-backend / Middleware /errorHandler.js
CampVerse CI
Deploy backend from GitHub Actions
c904036
Raw History Blame Contribute Delete
5.43 kB
const winston = require('winston');
const {
isOperationalError,
isDatabaseError,
} = require('../Utils/errors');
// Configure logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'campverse-backend' },
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
// Add file transport for production
if (process.env.NODE_ENV === 'production') {
logger.add(new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}));
logger.add(new winston.transports.File({
filename: 'logs/combined.log'
}));
}
// Enhanced error handling middleware
function errorHandler(err, req, res, _next) {
// Generate correlation ID for request tracking
const correlationId = req.correlationId || generateCorrelationId();
// Log error with context
const errorContext = {
correlationId,
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
userId: req.user?.id,
timestamp: new Date().toISOString(),
error: {
name: err.name,
message: err.message,
stack: err.stack,
statusCode: err.statusCode
}
};
// Handle different error types
let statusCode = 500;
let message = 'Internal Server Error';
let details = null;
if (isOperationalError(err)) {
// Operational errors (expected)
statusCode = err.statusCode;
message = err.message;
details = err.details;
// Log as warning for client errors, error for server errors
if (statusCode >= 500) {
logger.error('Operational Error', errorContext);
} else {
logger.warn('Client Error', errorContext);
}
} else if (isDatabaseError(err)) {
// Database errors
statusCode = 500;
message = 'Database operation failed';
// Handle specific MongoDB errors
if (err.code === 11000) {
statusCode = 409;
message = 'Resource already exists';
const field = Object.keys(err.keyPattern || {})[0];
details = { field, message: `Duplicate value for ${field}` };
} else if (err.name === 'CastError') {
statusCode = 400;
message = 'Invalid ID format';
details = { field: err.path, value: err.value };
} else if (err.name === 'ValidationError') {
statusCode = 400;
message = 'Validation failed';
details = Object.values(err.errors).map(e => ({
field: e.path,
message: e.message,
value: e.value
}));
}
logger.error('Database Error', errorContext);
} else if (err.name === 'JsonWebTokenError') {
// JWT errors
statusCode = 401;
message = 'Invalid token';
logger.warn('JWT Error', errorContext);
} else if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired';
logger.warn('Token Expired', errorContext);
} else if (err.name === 'MulterError') {
// File upload errors
statusCode = 400;
message = 'File upload error';
if (err.code === 'LIMIT_FILE_SIZE') {
message = 'File too large';
} else if (err.code === 'LIMIT_FILE_COUNT') {
message = 'Too many files';
}
logger.warn('File Upload Error', errorContext);
} else {
// Unexpected errors
logger.error('Unexpected Error', errorContext);
// In production, don't expose internal error details
if (process.env.NODE_ENV === 'production') {
message = 'Something went wrong';
} else {
message = err.message;
details = { stack: err.stack };
}
}
// Send error response
const errorResponse = {
success: false,
error: message,
correlationId,
timestamp: new Date().toISOString()
};
// Add details in development or for operational errors
if (details && (process.env.NODE_ENV !== 'production' || isOperationalError(err))) {
errorResponse.details = details;
}
// Add stack trace in development
if (process.env.NODE_ENV === 'development' && err.stack) {
errorResponse.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
}
// Generate correlation ID for request tracking
function generateCorrelationId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Middleware to add correlation ID to requests
function addCorrelationId(req, res, next) {
req.correlationId = req.get('X-Correlation-ID') || generateCorrelationId();
res.set('X-Correlation-ID', req.correlationId);
next();
}
// Async error wrapper for route handlers
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection', {
reason: reason.toString(),
stack: reason.stack,
promise
});
// Graceful shutdown
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', {
error: error.message,
stack: error.stack
});
// Graceful shutdown
process.exit(1);
});
module.exports = {
errorHandler,
addCorrelationId,
asyncHandler,
logger
};