https://docs.nestjs.com/exception-filters
Nest에는 어플리케이션에서 처리되지 않은 모든 예외를 처리하는 빌트인 예외 계층이 있다. 예외가 프로그램 코드에서 처리되지 않으면 예외 계층에서 잡은 후, 자동으로 적절한 응답을 보내준다.
이 작업은 HttpException 유형(및 하위 클래스)의 예외를 처리하는 global exception filter에서 수행된다. 예외가 처리되지 않은 경우(HttpException 또는 HttpException에서 상속되는 클래스가 아닌 경우), 아래와 같은 디폴트 JSON 응답을 생성한다.
{
"statusCode": 500,
"message": "Internal server error"
}
Nest는 HttpException
클래스를 제공한다.
전형적인 HTTP REST/GraphQL API 기반 어플리케이션에서는, 스탠다드 HTTP 응답 객체를 보내는 것이 최선이다.
예를 들어, findAll() 메소드가 있을 때 어떤 이유로 예외를 던진다고 가정해보자. 아래와 같이 하드 코딩 할 수 있다.
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
/*
{
"statusCode": 403,
"message": "Forbidden"
}
*/
여기서 사용한
HttpStatus
는@nestjs/common
패키지에서 가져온 helper enum이다.
HttpException
생성자는 두 매개변수를 가진다.
response
: JSON 응답 바디를 결정, 문자열 또는 객체가 올 수 있음status
: HTTP 상태 코드 결정기본적으로, JSON 응답 바디는 두 프로퍼티를 포함한다.
statusCode
: status
매개변수에서 받은 HTTP 상태 코드message
: status
에 기반한 HTTP 에러 설명메세지 부분을 오버라이드 하고 싶으면 response
인수에 문자열을 넘기면 됨
JSON 응답 바디 전체를 오버라이드 하고 싶으면, 객체를 response
인수에 넘기면 됨
그럼 Nest가 객체를 시리얼라이즈해서 JSON 응답 바디로 반환해준다.
아래는 객체 사용 예시
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}
/*
{
"status": 403,
"error": "This is a custom message"
}
*/
예외를 커스텀하고 싶으면, HttpException 클래스를 이용해서 자신만의 exceptions hierarchy를 만드는 것이 좋다.
이렇게 하면 Nest가 너의 예외를 알아차리고 자동적으로 에러 응답 처리
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
ForbiddenException이 HttpException을 extends 했기 때문에, 빌트인 예외 핸들러와 원활하게 작동한다.
@Get()
async findAll() {
// throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
throw new ForbiddenException();
}
HttpException
. These are exposed from the @nestjs/common
package, and represent many of the most common HTTP exceptions:BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException
예외 처리 흐름을 제어할 수 있게 도와주고 클라이언트로 보내는 응답의 내용을 바꿀 수 있음
일단 HttpException을 처리하고 응답을 커스텀하는 인스턴스를 만들어보자.
이걸 위해서 Request
와 Response
객체에 접근해야 함
Request
객체는 로그를 남길 때 오리지널 url을 가져오기 위해 사용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,
});
}
}
모든 exception 필터는 제너릭
ExceptionFilter<T>
인터페이스를 implement 해야 한다.
catch(exception: T, host: ArgumentsHost)
메소드 정의 필요
@Catch(HttpException) 데코레이터는 Nest에게 해당 필터가 다른 게 아닌 HttpException을 찾고 있다고 말해준다. (comma-separated list도 받을 수 있음)
host 파라미터는 ArgumentsHost
객체이다.
ArgumentsHost
is a powerful utility object that we'll examine further in the execution context chapter*.
지금은 Request
와 Response
를 획득하기 위해 사용한다. (Learn more about ArgumentsHost)
(*The reason for this level of abstraction is that ArgumentsHost
functions in all contexts (e.g., the HTTP server context we're working with now, but also Microservices and WebSockets). In the execution context chapter we'll see how we can access the appropriate underlying arguments for any execution context with the power of ArgumentsHost
and its helper functions. This will allow us to write generic exception filters that operate across all contexts.)
우리의 HttpExceptionFilter
를 CatsController
의 create()
메소드와 묶어보자.
@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();
}
되도록이면 클래스를 넘겨주자. (Nest는 한 클래스의 인스턴스를 모듈 전체에서 재사용하기 쉽기 때문에, 메모리 사용량을 줄임)
exception 필터는 다양한 레벨에서 적용할 수 있다.
아래는 컨트롤러 레벨에서 적용
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
아래는 글로벌 레벨에서 적용
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
useGlobalFilters()
메소드는 does not set up filters for gateways or hybrid applications.
모듈 밖에서 등록된 글로벌 필터(위의 main.ts 예시)는 의존성 주입이 불가능하다.
이 문제를 해결하기 위해서, 글로벌 필터를 다음과 같이 등록할 수 있다.
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
이 방법으로 필터에 의존성 주입을 수행하는 경우, 필터는 사실상 전역적이게 된다는 점 유의!
어디서 이걸 해야하나? 필터가 정의된 모듈에서 하세용
useClass
가 이걸 해내는 유일한 방법은 아님. 자세한 내용은 here
unhandled 예외를 잡기 위해서, @Catch() 데코레이터의 파라미터를 비워두면 된다.
아래 예시에서는 응답을 전달하기 위해 HTTP adapter를 사용했다.
아래 코드는는 HTTP adapter를 사용하여 응답을 전송하고 플랫폼별 객체(Request, Response)를 직접 사용하지 않기 때문에 플랫폼에 구애받지 않는다.
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);
}
}
위 코드는 접근 방법을 보여주기 위함임. 확장 예외 필터를 구현하려면 맞춤형 로직(다양한 컨디션 처리)이 포함되어야..
글로벌 필터는 base filter를 확장할 수 있다. 이건 두가지 방법으로 가능
HttpAdapter
reference 주입(아래 코드 참고)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
토큰 사용 (참고)보통은 프로그램의 요구사항을 충족시키기 위해 예외 필터를 완전히 커스텀한다.
하지만, 너가 원한다면 간단하게 global exception filter를 확장해서 사용할 수도 있다.
예외 처리를 base filter에 위임하려면, BaseExceptionFilter
를 extends 해서 catch() 메소드를 구현해야 한다.
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를 확장하는 메소드 레벨과 컨트롤러 레벨 필터는 new 연산자로 인스턴스화가 불가능하다. 프레임워크가 알아서 하도록 냅두자.