Skip to content

Commit

Permalink
feat: Error handler updates
Browse files Browse the repository at this point in the history
* Ensure error details included in response for JSON Schema Validation errors
* Improve typings
  • Loading branch information
jrassa committed Mar 22, 2024
1 parent 9d9feef commit e374ded
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 61 deletions.
23 changes: 23 additions & 0 deletions src/app/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ export class HttpError extends BaseError {
) {
super(name, message);
}

toJSON(exposeServerErrors = false): Record<string, unknown> {
return {
status: this.status,
message: this.message,
type: this.name,
stack: exposeServerErrors ? this.stack : undefined
};
}
}

export class BadRequestError extends HttpError {
Expand All @@ -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) {
Expand All @@ -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;
}
}
115 changes: 54 additions & 61 deletions src/app/common/express/error-handlers.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>('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<boolean>('exposeServerErrors') ? err.stack : undefined
});
return res.status(err.status).json(err.toJSON(exposeServerErrors));
}

const errorResponse = {
status: getStatus(err),
type: err.type ?? 'server-error',
Expand All @@ -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<boolean>('exposeServerErrors')) {
if (!exposeServerErrors) {
errorResponse.message = 'A server error has occurred.';
delete errorResponse.stack;
}
Expand All @@ -111,3 +85,22 @@ export const defaultErrorHandler = (err, req, res, next) => {
// Send the response
res.status(errorResponse.status).json(errorResponse);
};

const getStatus = (err: Parameters<ErrorRequestHandler>[0]) => {
if (!err.status || err.status < 400 || err.status >= 600) {
return 500;
}
return err.status;
};

const getMessage = (err: Parameters<ErrorRequestHandler>[0]) => {
if (_.isString(err)) {
return err;
}

if (err?.message) {
return `${err.name ?? 'Error'}: ${err.message}`;
}

return 'Error: Unknown error';
};

0 comments on commit e374ded

Please sign in to comment.