Tạo exception filter cho app
Mục tiêu
-
Trong bài này, ta sẽ tạo 3 loại exception filter:
- Http Exception Filter (Xử lý lỗi HTTP)
- Websocket Exception Filter (Xử lý lỗi Websocket)
- Catch all Exception Filter (Xử lý các unhandled errors)
-
Dữ liệu trả về lỗi của HTTP bao gồm các trường chính:
statusCode
: Mã lỗi HTTPmessage
: Thông báo lỗierrors
: Chi tiết lỗi (có thể có hoặc không)path
: Endpoint của API
-
Dữ liệu trả về lỗi của Websocket bao gồm các trường chính:
clientId
: ID của client kết nối tới server socketpattern
: Subscribe messagepayload
: Dữ liệu client gửi đếnmessage
: Thông báo lỗierrors
: Chi tiết lỗi (có thể có hoặc không)
- Đầu tiên, tạo BaseExceptionResponse.dto.ts:
BaseExceptionResponse.dto.ts
import { Expose } from "class-transformer";
export class BaseExceptionResponse {
@Expose()
statusCode: number;
@Expose()
message: string;
@Expose()
errors: any | null;
@Expose()
path: string;
}
- Tạo http-exception-filter.ts:
http-exception-filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { Request, Response } from "express";
import { BaseExceptionResponse } from "src/common/dto/BaseExceptionResponse.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>();
let statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string =
"An unexpected error occurred. Please contact admin to resolve this issue.";
let responseBody: BaseExceptionResponse = {
statusCode,
message,
path: request.url,
errors: null,
};
statusCode = exception.getStatus();
if (statusCode !== HttpStatus.INTERNAL_SERVER_ERROR) {
const exceptionResponse = exception.getResponse();
const exceptionMessage: string | undefined =
typeof exceptionResponse === "string"
? exceptionResponse
: (exceptionResponse as any).message;
message = exceptionMessage || "Unknown error message";
let extraErrorFields: Record<string, any> = {};
if (typeof exceptionResponse === "object" && exceptionResponse !== null) {
extraErrorFields = { ...exceptionResponse };
delete extraErrorFields.error;
}
responseBody = {
...responseBody,
...extraErrorFields,
message,
statusCode,
};
}
response.status(statusCode).json(responseBody);
}
}
- Tạo ws-exception-filter.ts:
ws-exception-filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from "@nestjs/common";
import { WsException } from "@nestjs/websockets";
import { Socket } from "socket.io";
@Catch(WsException)
export class WsExceptionFilter implements ExceptionFilter {
catch(exception: WsException, host: ArgumentsHost) {
const wsContext = host.switchToWs();
const socketClient = wsContext.getClient<Socket>();
const wsData = wsContext.getData();
const pattern = wsContext.getPattern();
const wsError = exception.getError();
const errorMessage =
(wsError instanceof Object ? (wsError as any)?.message : wsError) ||
"Unknown error message";
const errorResponse = {
...(wsError instanceof Object ? wsError : {}),
clientId: socketClient.id,
pattern,
payload: wsData,
message: errorMessage,
};
socketClient.emit("ws_exception", errorResponse);
}
}
- Tạo all-exception.filter.ts:
all-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
} from "@nestjs/common";
import { Request, Response } from "express";
import { Socket } from "socket.io";
import { BaseExceptionResponse } from "src/common/dto/BaseExceptionResponse.dto";
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost): void {
const contextType = host.getType();
if (contextType === "http") {
this.handleHttpException(exception, host);
} else if (contextType === "ws") {
this.handleWsException(exception, host);
}
}
private handleWsException(_exception: Error, host: ArgumentsHost) {
const socketClient = host.switchToWs().getClient<Socket>();
const wsData = host.switchToWs().getData();
const pattern = host.switchToWs().getPattern();
const responseBody = {
clientId: socketClient.id,
pattern,
payload: wsData,
message:
"An unexpected error occurred. Please contact admin to resolve this issue.",
errors: "Internal server error",
};
socketClient.emit("ws_exception", responseBody);
}
private handleHttpException(_exception: Error, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const responseBody: BaseExceptionResponse = {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message:
"An unexpected error occurred. Please contact admin to resolve this issue.",
path: request.url,
errors: "Internal server error",
};
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json(responseBody);
}
}
- Cuối cùng, ở file app.module.ts:
app.module.ts
import { APP_FILTER } from "@nestjs/core";
@Module({
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
],
})
export class AppModule {}
Chú ý
-
Lưu ý rằng phải đặt
HttpExceptionFilter
,WsExceptionFilter
sauAllExceptionsFilter
, điều này là rất quan trọng vì:- NestJS sẽ duyệt qua từng filter theo thứ tự đăng ký (từ cuối lên đầu), và nếu một filter xử lý được lỗi (
catch
xong không throw tiếp), thì NestJS không chuyển lỗi cho filter tiếp theo nữa. - Do đó, filter nào khai báo sau sẽ có cơ hội xử lý lỗi trước.
- NestJS sẽ duyệt qua từng filter theo thứ tự đăng ký (từ cuối lên đầu), và nếu một filter xử lý được lỗi (
-
Đối với WebSocket, ta phải chỉ định lại
@UseFilters(AllExceptionsFilter, WsExceptionFilter)
ở đầu gateway thì exception mới được xử lý (chỉ định vớiprovide: APP_FILTER
sẽ chỉ hoạt động đối với HTTP). Xem chi tiết tại đây
✅ Gợi ý sử dụng
- Đặt các filter chuyên biệt hơn ở sau cùng (được ưu tiên xử lý trước).
- Đặt các filter tổng quát (catch-all) như
AllExceptionsFilter
ở trước (vì nó sẽ nằm dưới cùng, được gọi cuối cùng như một "lưới an toàn").
caution
- Nếu truyền 1 object khi throw exception (hoặc custom exception). Hãy nhớ luôn thêm trường "message". Điều này là bắt buộc.
// ❌ Không hợp lệ
throw new BadRequestException({
errorType: ELoginExceptionErrorType.INVALID_2FA_OTP,
});
// ✅ Hợp lệ
throw new BadRequestException({
message: "Two factor authenticator code is invalid",
errorType: ELoginExceptionErrorType.INVALID_2FA_OTP,
});
// ✅ Hợp lệ bởi vì truyền 1 string thì NestJS tự gán nó là thuộc tính "message"
throw new BadRequestException("Two factor authenticator code is invalid");