Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
108 lines
3.3 KiB
JavaScript
108 lines
3.3 KiB
JavaScript
// /middlewares/globalErrorHandler.js
|
|
|
|
const logger = require('../utils/logger');
|
|
const { errorResponse } = require('../utils/apiResponse');
|
|
const { getLocalizedErrorMessage } = require('../utils/errorLocalization');
|
|
|
|
const formatDetailsMessage = (details) => {
|
|
if (!details) return null;
|
|
|
|
if (typeof details === 'string') return details;
|
|
|
|
if (Array.isArray(details)) {
|
|
const parts = details
|
|
.map((item) => {
|
|
if (!item) return null;
|
|
if (typeof item === 'string') return item;
|
|
return item.message || item.reason || null;
|
|
})
|
|
.filter(Boolean);
|
|
return parts.length ? parts.join(' — ') : null;
|
|
}
|
|
|
|
if (typeof details === 'object') {
|
|
const parts = Object.entries(details).map(([key, value]) => {
|
|
if (value == null) return null;
|
|
if (typeof value === 'string') return `${key}: ${value}`;
|
|
if (typeof value === 'object' && value.message) return `${key}: ${value.message}`;
|
|
if (key === 'keyValue') return Object.entries(value).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
return null;
|
|
}).filter(Boolean);
|
|
return parts.length ? parts.join(' — ') : null;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const globalErrorHandler = (err, req, res, next) => {
|
|
let statusCode = err.statusCode || 500;
|
|
let errorCode = err.errorCode || 'INTERNAL_SERVER_ERROR';
|
|
let details = err.details || null;
|
|
|
|
// Handle Mongoose CastError (e.g. invalid ObjectId)
|
|
if (err.name === 'CastError') {
|
|
statusCode = 400;
|
|
errorCode = 'VALIDATION_FAILED';
|
|
details = { field: err.path, value: err.value, reason: 'Invalid ObjectId format' };
|
|
}
|
|
|
|
// Handle Mongoose Duplicate Key Error (E11000)
|
|
if (err.code === 11000) {
|
|
statusCode = 409;
|
|
errorCode = 'DUPLICATE_KEY';
|
|
details = { keyPattern: err.keyPattern, keyValue: err.keyValue };
|
|
}
|
|
|
|
// Handle Mongoose ValidationError
|
|
if (err.name === 'ValidationError') {
|
|
statusCode = 400;
|
|
errorCode = 'VALIDATION_FAILED';
|
|
details = Object.keys(err.errors || {}).reduce((acc, key) => {
|
|
acc[key] = err.errors[key].message;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
// Handle Joi validation errors if forwarded as standard Error
|
|
if (err.isJoi) {
|
|
statusCode = 400;
|
|
errorCode = 'VALIDATION_FAILED';
|
|
details = err.details
|
|
? err.details.map((d) => ({ message: d.message, path: d.path }))
|
|
: null;
|
|
}
|
|
|
|
// Localized catalog message (Accept-Language / ?lang=)
|
|
let localizedMessage = err.overrideMessage
|
|
? err.overrideMessage
|
|
: getLocalizedErrorMessage(errorCode, req);
|
|
|
|
const detailsMessage = formatDetailsMessage(details);
|
|
if (detailsMessage && errorCode === 'VALIDATION_FAILED' && !err.overrideMessage) {
|
|
localizedMessage = `${localizedMessage} ${detailsMessage}`;
|
|
} else if (detailsMessage && errorCode === 'DUPLICATE_KEY') {
|
|
localizedMessage = `${localizedMessage} (${detailsMessage})`;
|
|
}
|
|
|
|
// Log error (stack trace for non-operational / 500 errors)
|
|
if (!err.isOperational || statusCode >= 500) {
|
|
logger.error(`[PROGRAMMING_ERROR] ${err.message}`, {
|
|
stack: err.stack,
|
|
path: req.originalUrl,
|
|
method: req.method
|
|
});
|
|
} else {
|
|
logger.warn(`[OPERATIONAL_ERROR] Code: ${errorCode} | Path: ${req.originalUrl} | Message: ${localizedMessage}`);
|
|
}
|
|
|
|
return errorResponse(
|
|
res,
|
|
statusCode,
|
|
errorCode,
|
|
localizedMessage,
|
|
details
|
|
);
|
|
};
|
|
|
|
module.exports = globalErrorHandler;
|