diff --git a/apps/policy-engine/src/app/app.controller.ts b/apps/policy-engine/src/app/app.controller.ts index d755e39fd..765a2ed86 100644 --- a/apps/policy-engine/src/app/app.controller.ts +++ b/apps/policy-engine/src/app/app.controller.ts @@ -1,7 +1,6 @@ import { EvaluationRequest } from '@narval/policy-engine-shared' import { Body, Controller, Get, Logger, Post } from '@nestjs/common' import { generateInboundRequest } from '../app/persistence/repository/mock_data' -import { ApplicationException } from '../shared/exception/application.exception' import { AppService } from './app.service' import { EvaluationRequestDto } from './evaluation-request.dto' @@ -21,15 +20,6 @@ export class AppController { this.logger.log({ message: 'Received ping' }) - - throw new ApplicationException({ - message: 'Test error message', - context: { - foo: 'bar' - }, - suggestedHttpStatusCode: 400 - }) - // throw new HttpException('THIS SHOULD SHOW SOMEHWERE', 500) return 'pong' } diff --git a/apps/policy-engine/src/main.ts b/apps/policy-engine/src/main.ts index b17a3c481..cf9e2479d 100644 --- a/apps/policy-engine/src/main.ts +++ b/apps/policy-engine/src/main.ts @@ -4,6 +4,9 @@ import { NestFactory } from '@nestjs/core' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import { lastValueFrom, map, of, switchMap } from 'rxjs' import { AppModule } from './app/app.module' +import { Config } from './policy-engine.config' +import { ApplicationExceptionFilter } from './shared/filter/application-exception.filter' +import { HttpExceptionFilter } from './shared/filter/http-exception.filter' /** * Adds Swagger documentation to the application. @@ -37,6 +40,21 @@ const withGlobalPipes = (app: INestApplication): INestApplication => { return app } +/** + * Adds a global exception filter to the application. + * + * @param app - The Nest application instance. + * @param configService - The configuration service instance. + * @returns The modified Nest application instance. + */ +const withGlobalFilters = + (configService: ConfigService) => + (app: INestApplication): INestApplication => { + app.useGlobalFilters(new HttpExceptionFilter(configService), new ApplicationExceptionFilter(configService)) + + return app + } + async function bootstrap() { const logger = new Logger('AuthorizationNodeBootstrap') const application = await NestFactory.create(AppModule, { bodyParser: true }) @@ -51,6 +69,7 @@ async function bootstrap() { of(application).pipe( map(withSwagger), map(withGlobalPipes), + map(withGlobalFilters(configService)), switchMap((app) => app.listen(port)) ) ) diff --git a/apps/policy-engine/src/shared/filter/application-exception.filter.ts b/apps/policy-engine/src/shared/filter/application-exception.filter.ts new file mode 100644 index 000000000..f94f56b2d --- /dev/null +++ b/apps/policy-engine/src/shared/filter/application-exception.filter.ts @@ -0,0 +1,52 @@ +import { ArgumentsHost, Catch, ExceptionFilter, LogLevel, Logger } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { Response } from 'express' +import { Config, Env } from '../../policy-engine.config' +import { ApplicationException } from '../../shared/exception/application.exception' + +@Catch(ApplicationException) +export class ApplicationExceptionFilter implements ExceptionFilter { + private logger = new Logger(ApplicationExceptionFilter.name) + + constructor(private configService: ConfigService) {} + + catch(exception: ApplicationException, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const status = exception.getStatus() + const isProduction = this.configService.get('env') === Env.PRODUCTION + + this.log(exception) + + response.status(status).json( + isProduction + ? { + statusCode: status, + message: exception.message, + context: exception.context + } + : { + statusCode: status, + message: exception.message, + context: exception.context, + stack: exception.stack, + ...(exception.origin && { origin: exception.origin }) + } + ) + } + + // TODO (@wcalderipe, 16/01/24): Unit test the logging logic. For that, we + // must inject the logger in the constructor via dependency injection. + private log(exception: ApplicationException) { + const level: LogLevel = exception.getStatus() >= 500 ? 'error' : 'warn' + + if (this.logger[level]) { + this.logger[level](exception.message, { + status: exception.getStatus(), + context: exception.context, + stacktrace: exception.stack, + origin: exception.origin + }) + } + } +} diff --git a/apps/policy-engine/src/shared/filter/http-exception.filter.ts b/apps/policy-engine/src/shared/filter/http-exception.filter.ts new file mode 100644 index 000000000..6fe3ee39a --- /dev/null +++ b/apps/policy-engine/src/shared/filter/http-exception.filter.ts @@ -0,0 +1,41 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { Response } from 'express' +import { Config, Env } from '../../policy-engine.config' + +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + private logger = new Logger(HttpExceptionFilter.name) + + constructor(private configService: ConfigService) {} + + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const status = exception.getStatus() + + const isProduction = this.configService.get('env') === Env.PRODUCTION + + this.logger.error({ + message: exception.message, + stack: exception.stack, + response: exception.getResponse(), + statusCode: status + }) + + response.status(status).json( + isProduction + ? { + statusCode: status, + message: exception.message, + response: exception.getResponse() + } + : { + statusCode: status, + message: exception.message, + response: exception.getResponse(), + stack: exception.stack + } + ) + } +}