Exception filters


Nest에는 exception layer이 내장되어 있다. 이는 application의 전반에 걸쳐 모든 unhandled exception의 처리에 책임이 있다.
만일 개발자의 application code에 의해 발생되는, not handled한 exception이 발생할 때, 이것을 이 layer가 처리한다. 그리고 바로 자동적으로 적절하게 user-friendly response를 보낸다.



별도의 설치나 설정이 필요없이, 이러한 action은 built-in된 global exception filter에 의해 수행된다. 이러한 filter는 HttpException(혹은 이것의 subclass) type의 exception(예외)를 다룬다. 만일 unrecognized한 exception이 발생한다면(HttpException, HttpException에 의한 class가 둘다 아님), built-in된 exception filter가 다음과 같은 default JSON response를 일으킨다.


{
  "statusCode": 500,
  "message": "Internal server error"
}

<팁!>
global exception filter는 부분적으로 http-errors library를 지원한다. 기본적으로, statusCode 그리고 message 속성을 포함하는 모든 exception은 적절하게 채워서 response로 반환된다. (기본적으로 인식되지 못한 예외에 대한 InternalSererErrorException 대신)



Throwing standard exceptons

Nest는 @nestJs/common에서의 build-in HttpException class를 제공한다. 전형적인 HTTP REST/GraphQL API 기반 applications에 대해서, 특정한 error가 발생했을 때, standard한 HTTP response object를 보내는게 best이다.


예를 들어, CatsController에서의 findAll() method가 있다고 하자. 그리고 어떠한 reason으로 route handler가 exception을 throw했다고 하자. 다음과 같다.


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

<팁!>
@nestjs/common의 HttpStatus는 상태 정보를 담는 enum helper 이다.

만일 client가 endpoint에서 호출한다면, response는 다음과 같다.


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

HttpException 생성자는 두 개의 argument를 담는데, 이는 response를 결정한다.

  • response argument는 JSON response body를 정의한다. 이는 string 혹은 object가 될 수 있다.
  • status argument는 HTTP status code를 결정한다.

기본적으로 JSON response body는 두 개의 속성을 지닌다.

  • statusCode: status argument에 제공된 HTTP status code로 기본 설정된다.
  • message: status에 기반한 HTTP error의 짧은 설명이다.

JSON response body의 message를 덮어 쓰기 위해서는, string을 response argument에 공급하면 된다.


JSON response body를 전부 덮기 위해서는, response argument에 object를 전달하면 된다. Nest는 객체를 serialize하고 JSON response body 형태로 반환할 것 이다.

두 번째 argument인 status는 반드시 유효한 HTTP status code를 넣어야 하다. 가장 최고의 방법은 @nestjs/common의 HttpStatus enum을 넣는 것이다.

optional한 세 번째 argument, optionerror의 cause을 전달하는데 사용된다.
cause object는 response object로 직렬화 되지 않지만, HttpException에 의해 발생한 내부 error에 대한 정보를 보여주는 logging의 목적으로 유용하다.

다음은 전체 response body와 에러의 원인을 제공하는 예제 코드이다.

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) {
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

그렇다면 response는 다음과 같다.

{
  "status": 403,
  "error": "This is a custom message"
}


Custom Exceptions

많은 경우에, custom exception을 작성할 필요가없다. 단지 built-in 되있는 Nest HTTP exception을 쓰면된다.
만일 customized exception을 만들 필요가 있다면 제일 좋은 방법은 HttpException class를 상속받는 자신만의 exception을 만드는 것이다. Nest는 exception을 인식하고, 자동으로 error responses를 관리할 것이다.

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

ForbiddenException은 기본 HttpException을 확장하므로 내장된 예외 처리기와 원활하게 작동합니다. 따라서 이를 findAll() 메서드 내에서 사용할 수 있습니다.

@Get()
async findAll() {
  throw new ForbiddenException();
}


Built-in HTTP exceptions

Nest는 HttpException을 상속받은 standard exception 집합을 제공한다. 이들은 @nestjs/common pacakge에 있다.
다음은 대표적인 HTTP Exception들이다.

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

모든 built-in exception들은 모두 options parameter를 이용하여 error causeerror description을 제공할 수 있다.

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400,
}


Exception filters

base(built-in) exception filter는 자동적으로 많은 경우들을 다루지만, exception layer을 모두 제어하기를 원할 수 있다. 예를 들어
동적인 요소들에 근거한 다른 JSON schema를 사용하거나 log를 붙이기를 원할 수 있다. Exception filters는 이러한 목적을 위해 디자인되었다. 이 filter들은 제어의 흐름과, client로 반환되는 response의 내용을 정확하게 제어할 수 있다.


HttpException class의 instance인 exception을 잡고, 이를 위한 사용자 정의 응답 로직을 구현하는 exception filter를 만들 것이다. 이것을 위해, 기본 플랫폼의 RequestResponse 객체에 접근할 필요가 있다. Request 객체에 접근하고, 원래의 url을 빼내고, 이를 log 정보에 포함시킬 것이다. Response 객체를 이용하여 응답을 직접 제어하고 response.json() method를 사용하여 응답을 전송할 것이다.

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

// @Catch(HttpExeception):  HttpException 타입의 예외를 잡아 처리하는 필터임을 지정한다.
@Catch(HttpException)
// HttpExceptionFilter는 ExceptionFilter interface를 구현한다. 이는 'catch' method를 포함한다.
export class HttpExceptionFilter implements ExceptionFilter {
  // catch() : ExceptionFilter의 일부로, 예외가 발생했을 때 이를 처리하는 메서드. 예외를 잡아내고, 적절하게 처리하여 클라이언트에게 응답을 반환하는 역할
  // exception: 발생한 HttpException 객체이다.
  // host: ArgumentsHost 객체로, 여러 유형의 요청 컨텍스트를 캡슐화한다.
  // 요청 컨텍스트: 특정 요청에 대한 정보를 포함하는 객체로, 요청의 전체 생명 주기 동안 관련 데이터를 캡슐화한다.
  //     다음과 같은 정보를 갖는다.
  //         1. HTTP 요청/응답 객체: Express나 Fastify의 요청 및 응답 객체
  //         2. WebSocket 데이터: WebSocket 연결 및 메시지 데이터
  //         3. RPC 요청: 원격 프로시저 호출에 대한 요청 데이터
  catch(exception: HttpException, host: ArgumentsHost) {
    // HTTP 컨텍스트로 전환하여 express의 request, response 객체를 가져온다.
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    // 예외 객체에서 HTTP 상태 코드를 가져온다.
    const status = exception.getStatus();

    // 상태 코드와 JSON 형식의 응답을 클라이언트에게 전송한다.
    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

<팁!>
모든 exception filter들은 모두 generic ExceptionFilter interface를 사용해야 한다. 이는 catch(exception: T, host: ArgumentHost) method에게 나타난 <T>를 제공해야한다. 여기서 T는 예외의 유형을 나타낸다.


<주의사항!>
만일 @nestjs/platform-fastify를 사용한다면, response.json() 대신 response.send()를 사용해야 한다.


@Catch(HttpException) decorator는 요구된 metadata와 exception filter를 묶어준다. exception filter는 Nest에게 이 특수한 filter는 HttpException 타입의 예외를 원한다고 말해줘야한다. @Catch() decorator는 한 개의 파라미터나, comma로 구분된 리스트를 받는다. 이는 한 번에 filter의 타입을 설정하도록 해준다.



Arguments Host

catch() 메서드의 parameter를 보자. exception parameter는 현재 처리되고 있는 exception object이다. host parameter는 ArgumentsHost 객체이다. ArgumentsHost는 강력한 utility object로, 나중에 execution context chapter*에서 더 자세히 설명할 것 이다. 이번 code sample에서는 원래의 요청 핸들러(예외가 발생한 controller)로 전달되는 Request 및 Response 객체에 대한 참조를 얻기 위해 이를 사용한다. 이 code sample에서는 _ArgumentsHost*_의 몇 가지 helper method를 사용하여 원하는 ReqeustResponse 객체를 가져왔다. ArgumentsHost

이 정도의 추상화(abstraction)를 사용하는 이유는 ArgumentsHost가 모든 context에서 작동하기 때문이다.(e.g. 지금 작업 중인 HTTP 서버 context뿐만 아니라, 마이크로서비스와 WebSockets도 포함된다.) execution context chapter에서는 ArgumentsHost와 그 helper 함수의 기능을 통해 모든 실행 컨텍스트에 대해 적절한 인자를 접근할 수 있는 방법을 살펴볼 것 이다. 이를 통해 모든 context에서 작동하는 일반적인 exception filter를 작성할 수 있다.



Binding filters

이제 우리의 HttpExceptionFilter를 CatsController의 create() method에 연결하자.

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

<팁!>
@UseFilters()는 @nestjs/common에서 가져올 수 있다.

@UseFilter()는 하나의 filter instance를 받거나, comma로 구분된 filter instance를 받는다. 코드에는 위에 작성된 HttpExceptionFilter instance를 넣었다. 대신에 instance 대신 class를 전달하여 instance화 책임을 프레임워크에 맡기고, dependency injection을 가능케 할 수 있다.


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

<팁!>
가능하다면, instance 대신 class를 사용함으로써 filter를 적용하는 것을 선호하자. 이는 Nest가 전체 module에서 같은 class의 instance를 쉽게 재사용할 수 있기 때문이다. 곧, memory 사용에 있어 이점을 취한다.


위의 코드에서 method-scoped에서 HttpExceptionFilter는 오직 하나의 create() route handler가 적용됬다. exception filter들은 다른 수준에서 적용될 수 있다. controller, resolver, gateway, controller-scoped, global-scope 등등 예를 들어 controller에 적용하는 것은 아래와 같다.

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

이 구성은 CatsController내에 정의된 모든 route handler에 대해 HttpExceptionFilter가 적용된다.

이를 global-scope filter로 만들자고하면, 다음과 같다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

<주의사항!>
useGlobalFilters() method는 gateway나 hybrid application에 대해 filter를 적용하지 못한다.


global-scopde filter는 모든 controller, route handler에 대해 application 전반적으로 사용된다. DI 측면에서 위의 useGlobalFilters() 와 같이, 어떤 모듈의 바깥에서 등록된 global filter은 dependency를 inject할 수 없다. 이는 any module의 context 바깥에서 완료됬기 때문이다. 이러한 문제를 해결하기 위해서는, any module에서 부터 바로 global-scope filter를 등록할 수 있다.

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

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

이러한 접근 방식을 사용하여 filter에 대해 dependency inject을 수행할 때, 이 구성이 사용되는 module에 상관없이 filter는 실제로 global이라는 점에 유의해야 한다. 이 작업은 어디에서 수행되는가? filter(위의 경우 HttpExceptionFilter)가 정의된 모듈을 선택해야 한다. 또한 useClass는 custom provider registration을 처리하는 유일한 방법은 아니다. 자세히

많은 filter들을 이 기술로 추가할 수 있다. 간다히 각 provider array에 추가하기만 하면 된다.



Catch everything

모든 unhandled exception을 catch하기 위해, @Catch() decorator의 parameter를 비워두면 된다.


아래의 코드는 플랫폼과 무관하다. 왜냐하면 response를 처리하기 위한 HTTP adapter를 사용하고 Request, Response 따위의 플랫폼 종속 객체를 쓰지 않기 때문이다.

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

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  // HttpAdapterHost를 주입받는다.
  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.
    // httpAdapter는 현재 애플리케이션에서 사용 중인 HTTP 서버 어댑터이다. 
    //    1. 요청 URL 가져오기: httpAdapter.getReqeustUrl(req)
    //    2. 응답 보내기: httpAdapter.replay(res, body, statusCode)
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

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

    // 응답으로 반환될 JSON 객체를 구성
    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      // 현재 요청의 URL 경로를 얻기 위해 사용된다. 
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

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

<주의사항!>
다양한 예외 필터를 조합하여 사용할 때, 모든 예외를 잡는 필터와 특정 타입에 바인딩된 필터를 함께 사용하는 경우, "모든 예외를 잡는" 필터를 먼저 선언해야 한다. 이렇게 해야 특정 필터가 바인됭 타입의 예외를 올바르게 처리할 수 있다.

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';
import { SpecificExceptionFilter } from './filters/specific-exception.filter';

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


Inheritance

일반적으로, application 요구사항을 충족시키기 위해 완전하 사용자 정의된 exception filter를 만든다. 하지만, 간단히 built-in default global exception fileter를 확장하고, 특정 요소에 따라 동작을 재정의하고자 할 때가 있을 수 있다.


예외 처리를 기본 필터에 위임하려면, BaseExceptionFilter를 확장하고, 상속받은 catch() method를 호출해야 한다.
아래와 같은 방식으로 기본 전역 예외 필터를 확장하고 커스터마이징하여 애플리케이션의 요구 사항에 맞게 사용할 수 있다.

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

<주의사항!>
BaseExceptionFilter를 확장한 Method-scoped와 Controller-scoped filter는 new를 이용해 instance화 되면 안된다. 대신에 framework가 이들을 자동으로 instance화하도록 해야 한다.


Global filter는 base filter를 확장할 수 있다. 이는 두 가지 방법으로 된다.

첫 번째 방법은 custom global filter를 인스턴스화할 때 HttpAdapter를 주입하는 것이다.

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를 사용하는 것이다.

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { CustomGlobalExceptionFilter } from './custom-global-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: CustomGlobalExceptionFilter, // 글로벌 필터로 설정
    },
  ],
})
export class AppModule {}

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

Guards  (0) 2024.06.07
Pipes  (0) 2024.06.06
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Providers  (0) 2024.06.04

+ Recent posts