'use strict'; const { MulterError } = require("multer"), { ValidationError } = require("joi"), { Errors, LIMIT_FILE_SIZE_ERR } = require('../helpers/constants'), { AppError, AppAuthError } = require("../helpers/app_error"), env = require('../helpers/env'), utils = require('../helpers/utils'), debug = require('debug')('agm:errHandler'); function createAppErrObj(tag = Errors.UNKNOWN_APP_ERROR, err = null) { const errObj = { error: { '.tag': tag } }; // Check if the tag (passed message) contains underscore (indicating it's a constant from Errors) if (tag && tag.indexOf('_') === -1) { // Tag doesn't contain underscore - treat as custom message // Use defaultMessage for .tag value and original tag as message errObj.error['.tag'] = err?.defaultMessage || Errors.UNKNOWN_APP_ERROR; if (!env.PRODUCTION) { errObj.error.message = tag; } } else { // Tag contains underscore - treat as constant from Errors or fallback to defaultMessage or UNKNOWN_APP_ERROR errObj.error['.tag'] = tag || err?.defaultMessage || Errors.UNKNOWN_APP_ERROR; } if (!env.PRODUCTION && err) { // Include messageLong or the error info dev mode for debugging const errMessage = typeof err === 'string' ? err : err?.messageLong || err?.toString(); errObj.error.message = errMessage; } return errObj; } function createAppValidationErrObj(valError) { let errObj = createAppErrObj(Errors.INVALID_PARAM); if (!utils.isEmptyObj(valError) && valError.message) { errObj.error.message = valError.message; } return errObj; } /** * Handle Application error for a given request with the provided error object * @param {*} req The Request object of the request * @param {*} res The Response object of the request * @param {*} err The error object (runtime error object or App* error classes) * Notes: return HTTP statusCode for the custom App errors with 409 as default. * For Stripe errors the code will always be 402. The client should base on the .code value for detail error. * @returns */ function ErrorHandler(err, req, res, next) { const statusCode = err && err.statusCode || 409; // Application error default HTTP status code try { // Check if response has already been sent or is in invalid state if (res.headersSent || res.writableEnded || res.destroyed) { env.LOG_ALL_ERRORS && debug('Response already sent, cannot send error:', err); return; } if (err.type && err.type.startsWith('Stripe')) { /* For Stripe errors the code will always be 402. The client should base on the .code value for detailed error. TODO: In Production mode => consider filtering Stripe errors to send back less detail. */ return res.status(402).send(err); } // Handle Joi ValidationError if (err instanceof ValidationError) { return res.status(statusCode).send(createAppValidationErrObj(err)); } // Handle Mongoose ValidationError as AppParamError if (err.name === 'ValidationError') { // const messages = Object.values(err.errors).map((e) => e.message).join(', '); return res.status(409).send(createAppValidationErrObj(err)); } if (err instanceof AppError) { return res.status(err.statusCode).send(createAppErrObj(err.message, err)); } // Handle Mongoose duplicate key error (e.g., unique index violation) if (typeof err === "object" && err.code === 11000) { return res.status(statusCode).send(createAppErrObj(Errors.DUPLICATED_VALUE)); } // Handle Multer file upload errors if (err instanceof MulterError) { return res.status(statusCode).send(createAppErrObj(err.code == LIMIT_FILE_SIZE_ERR ? Errors.FILE_TOO_LARGE : Errors.INVALID_PARAM)); } res.status(500).send(createAppErrObj(Errors.UNKNOWN_APP_ERROR, err)); // Handle unknown errors } catch (error) { res.status(500).send(createAppErrObj(Errors.UNKNOWN_ERROR, error)); } finally { env.LOG_ALL_ERRORS && (debug(err instanceof AppAuthError ? err.message : err)); } } module.exports = { ErrorHandler }