NestJS에서 Filter는, 애플리케이션에서 발생하는 각종 예외를 Filtering 하기 위한 목적으로 사용합니다. 이 글은 Filter를 적용하는 과정에서 발생한 문제(AxiosError 관련 문제)에 대한 해결을 논의합니다.
만약 Filter라는 개념 자체를 처음 접하시는 분이라면, 이전 글을 참고하시길 바랍니다.
이전 글: https://velog.io/@minkwan/TILNest-20250421
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { filterResponseParser } from './dto/filter-response-dto';
@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 instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const res: any = exception.getResponse();
const url: string = request.url;
const error: string = res.error;
const message: string = res?.message || 'http error 발생';
const timestamp: string = new Date().toISOString();
response.status(status).json(
filterResponseParser({
statusCode: status,
message,
data: null,
requestPath: url,
error,
timestamp,
}),
);
}
}
가장 먼저 예외를 처리하는 catch 메서드를 정의합니다. 매개변수로 exception과 host를 받는 모습을 확인할 수 있습니다.
해당 메서드에서 exception의 타입은 NestJS에서 제공하는 HttpException입니다. 즉, catch 메서드에 들어오는 exception은 HttpException입니다. host의 타입은 ArgumentHost인데요, ArgumentHost 역시 NestJS에서 제공하는, 실행 컨텍스트에 접근할 수 있게 해주는 유틸리티입니다.
실행 컨텍스트에 접근한다는 말은, 다양한 실행 환경(HTTP, WebSockets, gRPC 등)에서 요청과 응답 객체에 접근한다는 말과 같습니다.
지금 상황의 경우 HTTP 요청임이 분명하지만, 규모가 커질수록 매번 어떠한 요청인지 확인하기 어렵기에, host 컨텍스트를 switchToHttp() 메서드로 분명하게 변환한 뒤, 해당 값을 ctx 변수에 할당합니다. 이후 해당 ctx로부터 response와 request를 추출합니다. 추가적으로 url이나 error, timestamp 등 필요로 하는 속성 역시 추출합니다.
마지막으로 해당 응답에 대한 status를 json 형식으로 포맷팅하여 반환하게 됩니다.
요컨대, http-exceptions.filter.ts는 HttpException이 발생한 경우, 그에 상응하는 에러를 미리 정해놓은 형식에 맞게 반환하는 Filter라고 할 수 있습니다.
다음은 catch-everything.filter.ts에 대한 로직입니다.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { filterResponseParser } from './dto/filter-response-dto';
@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = filterResponseParser({
statusCode: httpStatus,
message: '서버 에러 발생',
data: null,
requestPath: httpAdapter.getRequestUrl(ctx.getRequest()),
});
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
http-exceptions.filter.ts와 크게 다를 바 없습니다.
다만 파일 이름에 맞게 http exception이 아니라면, 모두 위 Filter에서 에러가 잡히게 됩니다.
이제 의도적으로 에러를 발생시켜볼까 합니다. 예외 필터를 동작하게 하려면 예외를 발생시켜야겠죠.

토큰을 요청하는 url에 'ㄱ'을 추가했습니다. 잘못된 url 경로로 요청을 했다는 것은 HttpException이 발생했다는 것을 의미합니다. 따라서 사전에 작성해놓은 http-exceptions에서 에러가 잡혀야 합니다.

네트워크에서 에러 응답값을 확인해 보니, http-exceptions가 아니라 catch-everything 필터에서 에러가 잡혔다는 사실을 확인할 수 있습니다. HttpException을 의도적으로 발생시켰는데 catch-everything 필터에서 에러가 잡혔다는 것은, 제가 의도적으로 발생시킨 에러가 HttpException이 아니라는 사실을 의미합니다.
axios 통신 과정에서 발생한 에러는 HttpException이 아니라 AxiosError로 떨어집니다. 그렇다면 Service layer에서 모든 AxiosError를 HttpException으로 변환해 줘야 공용 필터인 HttpException으로 에러가 전달된다고 이해할 수 있습니다.
그런데 Http 관련 예외를 전역적으로 처리하기 위해서 Filter 로직을 따로 만든 것인데, 서비스 레이어에서 하나하나 AxiosError를 HttpException으로 변환해야 한다면 공용으로서의 의미를 상실하는 상황이 되겠죠.
그래서 "AxiosError를 HttpException으로 변환하는 공용 유틸 함수를 개발하는 게 좋지 않을까?"라는 생각을 하게 되었습니다.
import { HttpException, HttpStatus } from '@nestjs/common';
import { AxiosError } from 'axios';
export function convertAxiosErrorToHttpException(
error: unknown,
): HttpException {
// error가 AxiosError 타입인지 확인
if (
typeof error === 'object' &&
error !== null &&
'isAxiosError' in error &&
(error as any).isAxiosError
) {
const axiosError = error as AxiosError;
// Axios 응답 객체에서 HTTP 상태 코드를 가져오고, 없으면 502(Bad Gateway)로 설정
const status = axiosError.response?.status || HttpStatus.BAD_GATEWAY;
// Axios 응답 데이터에서 유의미한 필드들을 추출할 수 있도록 타입 단언
const responseData = axiosError.response?.data as
| { message?: string; error_description?: string; error?: string }
| undefined;
// 사용자에게 보여줄 메시지: message → error_description → 기본 Axios 메시지 순으로 우선순위 설정
const message =
responseData?.message ||
responseData?.error_description ||
axiosError.message ||
'Axios request failed';
// 에러 이름 또는 코드: error 필드가 없으면 기본값 'Bad Gateway' 사용
const errorMessage = responseData?.error || 'Bad Gateway';
// NestJS의 HttpException 인스턴스로 변환하여 반환 (일관된 포맷으로 필터와 호환)
return new HttpException(
{
statusCode: status,
message,
data: null,
error: errorMessage,
},
status,
);
}
// AxiosError가 아닌 일반 예외는 내부 서버 오류로 처리
return new HttpException(
{
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Unexpected error occurred',
data: null,
error: 'InternalServerError',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
에러가 AxiosError라면 일부 설정을 변경한 뒤, 최종적으로 HttpException 인스턴스로 변환하여 반환하는 코드입니다. 즉 서비스 레이어에서 위 함수를 실행했는데 AxiosError라면 해당 에러는 http-exceptions 필터로 넘어가게 됩니다.
만약 조건에 해당하지 않는다면(AxiosError가 아니라면), catch-everything filter로 예외가 넘어가게 될 것입니다.
Service Layer에 해당 함수를 적용했습니다.

아,,, 기가 막힌 모습을 확인할 수 있습니다.

Interceptors 개념을 적용할 수 있는 문제인지에 대한 고민이 남아있습니다. 관련해서 곧 Interceptors를 다룰 예정입니다.