From e374ded3824e88d2505bba40f7bb2d8d181b7dd8 Mon Sep 17 00:00:00 2001 From: John Rassa Date: Fri, 22 Mar 2024 14:32:39 -0400 Subject: [PATCH] feat: Error handler updates * Ensure error details included in response for JSON Schema Validation errors * Improve typings --- src/app/common/errors.ts | 23 +++++ src/app/common/express/error-handlers.ts | 115 +++++++++++------------ 2 files changed, 77 insertions(+), 61 deletions(-) diff --git a/src/app/common/errors.ts b/src/app/common/errors.ts index 8a927721..de83cca4 100644 --- a/src/app/common/errors.ts +++ b/src/app/common/errors.ts @@ -14,6 +14,15 @@ export class HttpError extends BaseError { ) { super(name, message); } + + toJSON(exposeServerErrors = false): Record { + return { + status: this.status, + message: this.message, + type: this.name, + stack: exposeServerErrors ? this.stack : undefined + }; + } } export class BadRequestError extends HttpError { @@ -23,6 +32,12 @@ export class BadRequestError extends HttpError { ) { super(StatusCodes.BAD_REQUEST, 'BadRequestError', message); } + + override toJSON(exposeServerErrors = false) { + const json = super.toJSON(exposeServerErrors); + json['errors'] = this.errors; + return json; + } } export class UnauthorizedError extends HttpError { constructor(message: string) { @@ -46,4 +61,12 @@ export class InternalServerError extends HttpError { constructor(message: string) { super(StatusCodes.INTERNAL_SERVER_ERROR, 'InternalServerError', message); } + + override toJSON(exposeServerErrors = false) { + const json = super.toJSON(exposeServerErrors); + if (exposeServerErrors) { + json['message'] = 'A server error has occurred.'; + } + return json; + } } diff --git a/src/app/common/express/error-handlers.ts b/src/app/common/express/error-handlers.ts index 27524715..da1fa76e 100644 --- a/src/app/common/express/error-handlers.ts +++ b/src/app/common/express/error-handlers.ts @@ -1,95 +1,69 @@ import config from 'config'; +import { ErrorRequestHandler } from 'express'; import { ValidationError } from 'express-json-validator-middleware'; import _ from 'lodash'; -import { Error } from 'mongoose'; +import { Error as MongooseError } from 'mongoose'; import { logger } from '../../../lib/logger'; -import { BadRequestError, HttpError, InternalServerError } from '../errors'; +import { BadRequestError, HttpError } from '../errors'; -const getStatus = (err) => { - logger.error(err.status < 400); - if (!err.status || err.status < 400 || err.status >= 600) { - return 500; - } - return err.status; -}; - -const getMessage = ( - err: string | Error | { name: string; message?: unknown } +export const mongooseValidationErrorHandler: ErrorRequestHandler = ( + err, + req, + res, + next ) => { - if (_.isString(err)) { - return err; - } - - if (err?.message) { - return `${err.name ?? 'Error'}: ${err.message}`; - } - return 'Error: Unknown error'; -}; - -const getMongooseValidationErrors = (err) => { - const errors = []; - - for (const field of Object.keys(err.errors ?? {})) { - if (err.errors[field].path) { - const message = - err.errors[field].type === 'required' - ? `${field} is required` - : err.errors[field].message; - errors.push({ field: field, message: message }); - } - } - - return errors; -}; - -export const mongooseValidationErrorHandler = (err, req, res, next) => { // Skip if not mongoose validation error - if (err.name !== 'ValidationError') { + if (!(err instanceof MongooseError.ValidationError)) { return next(err); } // Map to format expected by default error handler and pass on - const errors = getMongooseValidationErrors(err); + const errors = Object.entries(err.errors ?? {}) + .filter( + ([, innerError]) => innerError instanceof MongooseError.ValidatorError + ) + .map(([field, innerError]) => ({ field, message: innerError.message })); + return next( new BadRequestError(errors.map((e) => e.message).join(', '), errors) ); }; -export const jsonSchemaValidationErrorHandler = (err, req, res, next) => { +export const jsonSchemaValidationErrorHandler: ErrorRequestHandler = ( + err: Error, + req, + res, + next +) => { + // Skip if not json schema validation error if (!(err instanceof ValidationError)) { return next(err); } - return next(new BadRequestError('Invalid submission', err.validationErrors)); + return next( + new BadRequestError('Schema validation error', err.validationErrors) + ); }; -export const defaultErrorHandler = (err, req, res, next) => { +export const defaultErrorHandler: ErrorRequestHandler = ( + err, + req, + res, + next +) => { if (res.headersSent) { return next(err); } const exposeServerErrors = config.get('exposeServerErrors'); - if (err instanceof InternalServerError) { - return res.status(err.status).json({ - status: err.status, - message: exposeServerErrors - ? err.message - : 'A server error has occurred.', - type: err.name, - stack: exposeServerErrors ? err.stack : undefined - }); - } else if (err instanceof HttpError) { + if (err instanceof HttpError) { logger.error(req.url, err); - return res.status(err.status).json({ - status: err.status, - message: err.message, - type: err.name, - stack: config.get('exposeServerErrors') ? err.stack : undefined - }); + return res.status(err.status).json(err.toJSON(exposeServerErrors)); } + const errorResponse = { status: getStatus(err), type: err.type ?? 'server-error', @@ -102,7 +76,7 @@ export const defaultErrorHandler = (err, req, res, next) => { if (errorResponse.status >= 500 && errorResponse.status < 600) { // Swap the error message if `exposeServerErrors` is disabled - if (!config.get('exposeServerErrors')) { + if (!exposeServerErrors) { errorResponse.message = 'A server error has occurred.'; delete errorResponse.stack; } @@ -111,3 +85,22 @@ export const defaultErrorHandler = (err, req, res, next) => { // Send the response res.status(errorResponse.status).json(errorResponse); }; + +const getStatus = (err: Parameters[0]) => { + if (!err.status || err.status < 400 || err.status >= 600) { + return 500; + } + return err.status; +}; + +const getMessage = (err: Parameters[0]) => { + if (_.isString(err)) { + return err; + } + + if (err?.message) { + return `${err.name ?? 'Error'}: ${err.message}`; + } + + return 'Error: Unknown error'; +};