Skip to content

Commit

Permalink
Merge pull request #158 from conceptadev/feature/improve-exception-ha…
Browse files Browse the repository at this point in the history
…ndling

feat: exceptions handling improvements
  • Loading branch information
MrMaz authored May 23, 2024
2 parents e500327 + c6cfad8 commit 0bc2b44
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { format } from 'util';
import { NotFoundException } from '@nestjs/common';
import { ExceptionInterface } from '@concepta/ts-core';

export class CustomNotFoundExceptionFixture
extends NotFoundException
implements ExceptionInterface
{
errorCode = 'CUSTOM_NOT_FOUND';
import { HttpStatus } from '@nestjs/common';
import { RuntimeException } from '../../exceptions/runtime.exception';

export class CustomNotFoundExceptionFixture extends RuntimeException {
constructor(itemId: number) {
super(
NotFoundException.createBody(
format('Item with id %d was not found.', itemId),
),
);
super({
message: 'Item with id %d was not found.',
messageParams: [itemId],
httpStatus: HttpStatus.NOT_FOUND,
});

this.errorCode = 'CUSTOM_NOT_FOUND';
}
}
5 changes: 5 additions & 0 deletions packages/nestjs-exception/src/exception.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ExceptionContext } from '@concepta/ts-core';

export type RuntimeExceptionContext = ExceptionContext & {
originalError?: Error;
};
57 changes: 57 additions & 0 deletions packages/nestjs-exception/src/exceptions/runtime.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { format } from 'util';
import { HttpStatus } from '@nestjs/common';
import { mapNonErrorToException } from '@concepta/ts-core';
import { RuntimeExceptionInterface } from '../interfaces/runtime-exception.interface';
import { RuntimeExceptionOptions } from '../interfaces/runtime-exception-options.interface';
import { RuntimeExceptionContext } from '../exception.types';

export class RuntimeException
extends Error
implements RuntimeExceptionInterface
{
private _errorCode = 'RUNTIME_EXCEPTION';
private _httpStatus: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
private _safeMessage?: string;
public context: RuntimeExceptionContext = {};

constructor(
options: RuntimeExceptionOptions = { message: 'Runtime Exception' },
) {
const {
message = '',
messageParams = [],
safeMessage,
safeMessageParams = [],
originalError,
httpStatus,
} = options;

super(format(message, ...messageParams));

if (httpStatus) {
this._httpStatus = httpStatus;
}

if (safeMessage) {
this._safeMessage = format(safeMessage, ...safeMessageParams);
}

this.context.originalError = mapNonErrorToException(originalError);
}

public get errorCode() {
return this._errorCode;
}

protected set errorCode(v: string) {
this._errorCode = v;
}

public get httpStatus() {
return this._httpStatus;
}

public get safeMessage() {
return this._safeMessage;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import supertest from 'supertest';
import { HttpAdapterHost } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { AppModuleFixture } from '../__fixtures__/app.module.fixture';
import { ExceptionsFilter } from './exceptions.filter';

Expand All @@ -27,19 +27,24 @@ describe('Exception (e2e)', () => {

it('Should return unknown', async () => {
const response = await supertest(app.getHttpServer()).get('/test/unknown');
expect(response.body.statusCode).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(response.body.errorCode).toEqual('UNKNOWN');
expect(response.body.message).toEqual('Internal Server Error');
});

it('Should return bad request', async () => {
const response = await supertest(app.getHttpServer()).get(
'/test/bad-request',
);
expect(response.body.statusCode).toEqual(HttpStatus.BAD_REQUEST);
expect(response.body.errorCode).toEqual('HTTP_BAD_REQUEST');
expect(response.body.message).toEqual('Bad Request');
});

it('Should return not found', async () => {
const response = await supertest(app.getHttpServer()).get('/test/123');
expect(response.body.errorCode).toEqual('CUSTOM_NOT_FOUND');
expect(response.body.statusCode).toEqual(HttpStatus.NOT_FOUND);
expect(response.body.message).toEqual('Item with id 123 was not found.');
});
});
36 changes: 30 additions & 6 deletions packages/nestjs-exception/src/filters/exceptions.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@nestjs/common';
import { ExceptionInterface } from '@concepta/ts-core';
import { ERROR_CODE_UNKNOWN } from '../constants/error-codes.constants';
import { RuntimeException } from '../exceptions/runtime.exception';
import { mapHttpStatus } from '../utils/map-http-status.util';

@Catch()
Expand All @@ -20,24 +21,47 @@ export class ExceptionsFilter implements ExceptionFilter {

const ctx = host.switchToHttp();

// error code is UNKNOWN unless it gets overridden
let errorCode = ERROR_CODE_UNKNOWN;

// error is 500 unless it gets overridden
let statusCode = 500;

// what will this message be?
let message: string = 'Internal Server Error';

// is this an http exception?
if (exception instanceof HttpException) {
// set the status code
statusCode = exception.getStatus();
// are we missing the error code?
if (!exception.errorCode) {
// its missing, try to set it
exception.errorCode = mapHttpStatus(statusCode);
// map the error code
errorCode = mapHttpStatus(statusCode);
// set the message
message = exception.message;
} else if (exception instanceof RuntimeException) {
// its a runtime exception, set error code
errorCode = exception.errorCode;
// did they provide a status hint?
if (exception?.httpStatus) {
statusCode = exception.httpStatus;
}
// set the message
if (statusCode >= 500) {
// use safe message or internal sever error
message = exception?.safeMessage ?? 'Internal Server Error';
} else if (exception?.safeMessage) {
// use the safe message
message = exception.safeMessage;
} else {
// use the error message
message = exception.message;
}
}

const responseBody = {
statusCode,
errorCode: exception?.errorCode ?? ERROR_CODE_UNKNOWN,
message: exception.message,
errorCode,
message,
timestamp: new Date().toISOString(),
};

Expand Down
10 changes: 10 additions & 0 deletions packages/nestjs-exception/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
// types
export { RuntimeExceptionContext } from './exception.types';

// filters
export { ExceptionsFilter } from './filters/exceptions.filter';

// interfaces
export { RuntimeExceptionOptions } from './interfaces/runtime-exception-options.interface';
export { RuntimeExceptionInterface } from './interfaces/runtime-exception.interface';

// exceptions
export { RuntimeException } from './exceptions/runtime.exception';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { HttpStatus } from '@nestjs/common';

export interface RuntimeExceptionOptions {
httpStatus?: HttpStatus;
message?: string;
messageParams?: (string | number)[];
safeMessage?: string;
safeMessageParams?: (string | number)[];
originalError?: unknown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HttpStatus } from '@nestjs/common';
import { ExceptionInterface } from '@concepta/ts-core/src/exceptions/interfaces/exception.interface';
import { RuntimeExceptionContext } from '../exception.types';

export interface RuntimeExceptionInterface extends ExceptionInterface {
/**
* Optional HTTP status code to use only when this exception is sent over an HTTP service.
*
* Please consider this to be a hint for API error responses.
*/
httpStatus?: HttpStatus;

/**
* If set, this message will be used on responses instead of `message`.
*
* Use this when the main message might expose
*/
safeMessage?: string;

/**
* Additional context
*/
context: RuntimeExceptionContext;
}
3 changes: 3 additions & 0 deletions packages/ts-core/src/core.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ExceptionContext = Record<string, unknown> & {
originalError?: unknown;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExceptionContext } from '../../core.types';

export interface ExceptionInterface extends Error {
/**
* The error code.
Expand All @@ -7,5 +9,5 @@ export interface ExceptionInterface extends Error {
/**
* Additional context
*/
context?: Record<string, unknown>;
context?: ExceptionContext;
}
3 changes: 1 addition & 2 deletions packages/ts-core/src/exceptions/not-an-error.exception.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { format } from 'util';
import { ExceptionInterface } from './interfaces/exception.interface';

export class NotAnErrorException extends Error implements ExceptionInterface {
Expand All @@ -12,7 +11,7 @@ export class NotAnErrorException extends Error implements ExceptionInterface {
originalError: unknown,
message = 'An error was caught that is not an Error object',
) {
super(format(message));
super(message);
this.context = {
originalError,
};
Expand Down
2 changes: 2 additions & 0 deletions packages/ts-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { ExceptionContext } from './core.types';

export { ExceptionInterface } from './exceptions/interfaces/exception.interface';

export { Type } from './utils/interfaces/type.interface';
Expand Down

0 comments on commit 0bc2b44

Please sign in to comment.