Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: exceptions handling improvements #158

Merged
merged 2 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
import { HttpStatus } from '@nestjs/common';
import { RuntimeException } from '../../exceptions/runtime.exception';

export class CustomNotFoundExceptionFixture
extends NotFoundException
implements ExceptionInterface
{
export class CustomNotFoundExceptionFixture extends RuntimeException {
httpStatus = HttpStatus.NOT_FOUND;
errorCode = 'CUSTOM_NOT_FOUND';

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],
});
}
}
45 changes: 45 additions & 0 deletions packages/nestjs-exception/src/exceptions/runtime.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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';

export class RuntimeException
extends Error
implements RuntimeExceptionInterface
{
errorCode = 'RUNTIME_EXCEPTION';
httpStatus?: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
safeMessage?: string;

context: {
originalError: Error;
};

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),
};
}
}
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
7 changes: 7 additions & 0 deletions packages/nestjs-exception/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
// 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,23 @@
import { HttpStatus } from '@nestjs/common';
import { ExceptionInterface } from '@concepta/ts-core/src/exceptions/interfaces/exception.interface';

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?: Record<string, unknown> & { originalError?: Error };
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export interface ExceptionInterface extends Error {
/**
* Additional context
*/
context?: Record<string, unknown>;
context?: Record<string, unknown> & { originalError?: unknown };
}
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
Loading