Files
gameno-api/middlewares/globalErrorHandler.js
T
kavehhn 1ef54ad414 fix: stream FilePond uploads to S3 and proxy private file previews
Disk-based multer plus an authenticated content endpoint match the working Node.js uploader and avoid browser signed-URL failures on SeaweedFS.
2026-08-15 18:46:26 +03:30

115 lines
3.6 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 Multer upload errors
if (err.name === 'MulterError') {
statusCode = 400;
errorCode = err.code === 'LIMIT_FILE_SIZE' ? 'VALIDATION_FAILED' : 'FILE_REQUIRED';
details = { reason: err.code, field: err.field };
}
// 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;