처음부터 차근차근

[NestJS] Exception filter 본문

FrameWork/NestJS

[NestJS] Exception filter

HangJu_95 2023. 11. 28. 14:14
728x90

Exception filter

Exception layer https://docs.nestjs.com/exception-filters

Nest는 예외 처리를 진행하기 위해 exception layer를 내장하고 있습니다. 

이 레이어는 app code에서 처리되지 않은 예외를 catch해서 정제된 응답을 제공합니다. 이러한 기능을 통해 유저 친화적인 응답을 제공하고 Server가 동작하지 않는 문제를 해결합니다.

기본적으로 Nest에 내장된 global exception filter가 HttpException 타입과 그 하위 타입의 예외들을 처리하고, 알 수 없는 예외(ORM Query 혹은 연결 문제 등)는 500 Internal server error를 디폴트로 응답합니다.

기본 exception filter는 statusCode와 message가 존재하는 exception이면 정상적으로 catch하고 응답합니다.(http-errors의 라이브러리를 일부 지원합니다.)

Throwing standard exceptions

간단한 예시를 통해 예외처리를 만들어보겠습니다.

// cats.controller.ts
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

이 간단한 예시는 HTTP REST/GraphQL API Server에 적합합니다.

throw new httpException을 통해 Response로 에러를 보낼 수 있습니다.

해당 메서드를 동작시켜보면

{
  "statusCode": 403,
  "message": "Forbidden"
}

해당하는 response object가 나옵니다.

HttpException의 생성자는 response와 status 두 가지 파라미터를 받습니다. response에 string을 넣으면 응답의 메세지로 전달되고, object을 넣으면 Response 전체가 해당 object가 됩니다.

status는 유효한 HTTP 상태코드여야 하며, HTTP 상태 코드 enum인 HTTPStatus를 활용하는 것이 가장 좋습니다.

간단한 예시를 통해 어떻게 Response가 출력되는지 확인해보겠습니다.

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) { 
    // 객체를 통해 response를 전달한다.
    // object일 경우 Response 전체로 전달한다.
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}
{
  "status": 403,
  "error": "This is a custom message"
}

Custom exception

NestJS에서 제공하는 다양한 Built-in Httpexception 덕분에 Custom을 할 필요는 없지만, 만들어야 할 경우에는 자체 위계 구조를 만들고, 모두 최상위 부모로 HttpException을 상속받는 구조를 추천합니다.

이렇게 만든다면 exception filter가 custom exception을 인식하고 처리할 수 있습니다.

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

Built-in HTTP exceptions

Nest에서는 다양한 HTTP exception을 제공합니다.

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

Exception filters

만약 exception layer의 동작 방식을 모두 직접 조작하고 싶으면 exception filter를 직접 구현할 수 있습니다. (Ex: 로깅 구현)

예시로, response를 조작하고 싶다면 이렇게 작성할 수 있습니다.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

모든 예외 필터는 ExceptionFilter 인터페이스를 구현해야 합니다. 이를 위해서는 catch() 메소드를 제공해야 합니다.

@Catch(HttpException) 데코레이터는 필수 메타데이터를 예외 필터에 바인딩하여 이 특정 필터가 HttpException 유형의 예외만 찾고 있음을 Nest에게 알립니다.

@Catch() 데코레이터를 단일 매개 변수 혹은 Array로도 사용할 수 있으며, 이를 통해 여러 유형의 예외에 대한 필터를 한 번에 설정할 수 있습니다.

binding filters

이제 새로 작성한 HttpExceptionFilter 메소드를 연결해보겠습니다.

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

여기에서는 @UseFilters() 데코레이터를 사용했습니다. @Catch() 데코레이터와 유사하게 단일 필터 인스턴스 또는 쉼표로 구분된 필터 인스턴스 목록을 사용할 수 있습니다.

또한 클래스를 전달할 수 있습니다.

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

컨트롤러 전역에 적용하기 위해서는 Controller에 적용하면 됩니다.

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

전역으로 사용하고 싶다면 Guard, Interceptor와 마찬가지로 적용하면 됩니다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

Catch everything

HTTP 예외처리가 아닌 모든 예외처리를 하고 싶을떄는, Catch() 데코레이터에 아무 인자도 넣지 않으면 됩니다.

아래 예시는 HTTPAdapterHost를 사용하여 응답을 전달하기 때문에 플랫폼에 구애받지 않는 코드가 되었습니다.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

inheritance

위 예시는 Application 요구 사항을 충족하도록 제작된 맞춤형 커스텀 예외필터입니다. 그러나 내장된 기본 적역 예외 필터를 단순히 확장하고 특정 요인에 따라 동작을 재정의하려는 경우의 사례가 있습니다.

예외 처리를 기본 필터에 위임하려면 BaseExceptionFilter를 확장하고 catch() 메서드를 호출해야 합니다.

전역 필터는 기본 필터를 확장할 수 있습니다. 이는 두 가지 방법이 있으며

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

해당 방법을 통해 전역 필터로 등록하거나, APP_FILTER 토큰을 사용하여 적용시킬 수 있습니다.

참조

https://docs.nestjs.com/exception-filters

https://gongmeda.tistory.com/54?category=1043173

 

 

 

 

'FrameWork > NestJS' 카테고리의 다른 글

[NestJS] 제어 역전과 의존성 주입  (1) 2023.11.29
[NestJS] Custom decorator  (0) 2023.11.28
[NestJS] Pipe  (0) 2023.11.28
[NestJS] interceptor  (0) 2023.11.27
[NestJS] Guard  (0) 2023.11.27