249 lines
5.5 KiB
JavaScript
249 lines
5.5 KiB
JavaScript
const logger = require('../config/logger');
|
|
const { AppError } = require('../utils/AppError');
|
|
|
|
/**
|
|
* Handle Sequelize validation errors
|
|
*/
|
|
const handleSequelizeValidationError = (error) => {
|
|
const errors = error.errors.map(err => ({
|
|
field: err.path,
|
|
message: err.message,
|
|
value: err.value
|
|
}));
|
|
|
|
return {
|
|
statusCode: 400,
|
|
message: 'Validation error',
|
|
errors
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle Sequelize unique constraint errors
|
|
*/
|
|
const handleSequelizeUniqueConstraintError = (error) => {
|
|
const field = error.errors[0]?.path;
|
|
const value = error.errors[0]?.value;
|
|
|
|
return {
|
|
statusCode: 409,
|
|
message: `${field} '${value}' already exists`
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle Sequelize foreign key constraint errors
|
|
*/
|
|
const handleSequelizeForeignKeyConstraintError = (error) => {
|
|
return {
|
|
statusCode: 400,
|
|
message: 'Invalid reference to related resource'
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle Sequelize database connection errors
|
|
*/
|
|
const handleSequelizeConnectionError = (error) => {
|
|
return {
|
|
statusCode: 503,
|
|
message: 'Database connection error. Please try again later.'
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle JWT errors
|
|
*/
|
|
const handleJWTError = () => {
|
|
return {
|
|
statusCode: 401,
|
|
message: 'Invalid token. Please log in again.'
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle JWT expired errors
|
|
*/
|
|
const handleJWTExpiredError = () => {
|
|
return {
|
|
statusCode: 401,
|
|
message: 'Your token has expired. Please log in again.'
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle Sequelize errors
|
|
*/
|
|
const handleSequelizeError = (error) => {
|
|
// Validation error
|
|
if (error.name === 'SequelizeValidationError') {
|
|
return handleSequelizeValidationError(error);
|
|
}
|
|
|
|
// Unique constraint violation
|
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
|
return handleSequelizeUniqueConstraintError(error);
|
|
}
|
|
|
|
// Foreign key constraint violation
|
|
if (error.name === 'SequelizeForeignKeyConstraintError') {
|
|
return handleSequelizeForeignKeyConstraintError(error);
|
|
}
|
|
|
|
// Database connection error
|
|
if (error.name === 'SequelizeConnectionError' ||
|
|
error.name === 'SequelizeConnectionRefusedError' ||
|
|
error.name === 'SequelizeHostNotFoundError' ||
|
|
error.name === 'SequelizeAccessDeniedError') {
|
|
return handleSequelizeConnectionError(error);
|
|
}
|
|
|
|
// Generic database error
|
|
return {
|
|
statusCode: 500,
|
|
message: 'Database error occurred'
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Send error response in development
|
|
*/
|
|
const sendErrorDev = (err, res) => {
|
|
res.status(err.statusCode).json({
|
|
status: err.status,
|
|
message: err.message,
|
|
error: err,
|
|
stack: err.stack,
|
|
...(err.errors && { errors: err.errors })
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Send error response in production
|
|
*/
|
|
const sendErrorProd = (err, res) => {
|
|
// Operational, trusted error: send message to client
|
|
if (err.isOperational) {
|
|
res.status(err.statusCode).json({
|
|
status: err.status,
|
|
message: err.message,
|
|
...(err.errors && { errors: err.errors })
|
|
});
|
|
}
|
|
// Programming or unknown error: don't leak error details
|
|
else {
|
|
// Log error for debugging
|
|
logger.error('ERROR 💥', err);
|
|
|
|
// Send generic message
|
|
res.status(500).json({
|
|
status: 'error',
|
|
message: 'Something went wrong. Please try again later.'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Centralized error handling middleware
|
|
*/
|
|
const errorHandler = (err, req, res, next) => {
|
|
err.statusCode = err.statusCode || 500;
|
|
err.status = err.status || 'error';
|
|
|
|
// Log the error
|
|
logger.logError(err, req);
|
|
|
|
// Handle specific error types
|
|
let error = { ...err };
|
|
error.message = err.message;
|
|
error.stack = err.stack;
|
|
|
|
// Sequelize errors
|
|
if (err.name && err.name.startsWith('Sequelize')) {
|
|
const handled = handleSequelizeError(err);
|
|
error.statusCode = handled.statusCode;
|
|
error.message = handled.message;
|
|
error.isOperational = true;
|
|
if (handled.errors) error.errors = handled.errors;
|
|
}
|
|
|
|
// JWT errors
|
|
if (err.name === 'JsonWebTokenError') {
|
|
const handled = handleJWTError();
|
|
error.statusCode = handled.statusCode;
|
|
error.message = handled.message;
|
|
error.isOperational = true;
|
|
}
|
|
|
|
if (err.name === 'TokenExpiredError') {
|
|
const handled = handleJWTExpiredError();
|
|
error.statusCode = handled.statusCode;
|
|
error.message = handled.message;
|
|
error.isOperational = true;
|
|
}
|
|
|
|
// Multer errors (file upload)
|
|
if (err.name === 'MulterError') {
|
|
error.statusCode = 400;
|
|
error.message = `File upload error: ${err.message}`;
|
|
error.isOperational = true;
|
|
}
|
|
|
|
// Send error response
|
|
if (process.env.NODE_ENV === 'development') {
|
|
sendErrorDev(error, res);
|
|
} else {
|
|
sendErrorProd(error, res);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle async errors (wrap async route handlers)
|
|
*/
|
|
const catchAsync = (fn) => {
|
|
return (req, res, next) => {
|
|
fn(req, res, next).catch(next);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle 404 Not Found errors
|
|
*/
|
|
const notFoundHandler = (req, res, next) => {
|
|
const error = new AppError(
|
|
`Cannot find ${req.originalUrl} on this server`,
|
|
404
|
|
);
|
|
next(error);
|
|
};
|
|
|
|
/**
|
|
* Log unhandled rejections
|
|
*/
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
logger.error('Unhandled Rejection at:', {
|
|
promise,
|
|
reason: reason.stack || reason
|
|
});
|
|
// Optional: Exit process in production
|
|
// process.exit(1);
|
|
});
|
|
|
|
/**
|
|
* Log uncaught exceptions
|
|
*/
|
|
process.on('uncaughtException', (error) => {
|
|
logger.error('Uncaught Exception:', {
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
// Exit process on uncaught exception
|
|
process.exit(1);
|
|
});
|
|
|
|
module.exports = {
|
|
errorHandler,
|
|
catchAsync,
|
|
notFoundHandler
|
|
};
|