API/Server/middleware/errorHandler.js
2026-01-24 16:47:19 -04:00

135 lines
2.9 KiB
JavaScript

const logger = require('../utils/logger');
// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class AuthenticationError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message = 'Access denied') {
super(message, 403);
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
}
}
class DatabaseError extends AppError {
constructor(message = 'Database operation failed') {
super(message, 500);
}
}
// Error handling middleware
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
logger.error({
error: err,
request: {
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.userId
}
});
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message).join(', ');
error = new ValidationError(message);
}
// Mongoose duplicate key error
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
const value = err.keyValue[field];
error = new ConflictError(`${field} '${value}' already exists`);
}
// Mongoose cast error
if (err.name === 'CastError') {
error = new ValidationError(`Invalid ${err.path}: ${err.value}`);
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
error = new AuthenticationError('Invalid token');
}
if (err.name === 'TokenExpiredError') {
error = new AuthenticationError('Token expired');
}
// Default error
if (!error.isOperational) {
error = new AppError('Something went wrong', 500);
}
res.status(error.statusCode || 500).json({
status: error.status || 'error',
message: error.message,
...(process.env.NODE_ENV === 'development' && {
stack: error.stack,
error: err
})
});
};
// Async error wrapper
const catchAsync = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// 404 handler
const notFound = (req, res, next) => {
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
next(error);
};
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
DatabaseError,
errorHandler,
catchAsync,
notFound
};