NestJS 的 异常过滤器学习

NestJS 带有一个内置的异常层,负责处理应用程序中所有未处理的异常。当应用程序代码未处理异常时,该层会捕获异常,然后自动发送对应的异常响应给用户。

在 NestJs 中默认异常过滤器为HttpException及其子类,当异常无法不属于HttpException或者子类的时候,内置的异常过滤器会生成如下的 JSON 响应:

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

使用 HttpException 抛出异常

使用HttpException抛出异常是很简单的,我们主要创建HttpException异常就可以。

就比如我们有一个保存的请求地址,这个接口需要在请求头中得Accept参数带入 Token,假如没有传入此参数或者传入的是*/*的字符串的话,我们就抛出没有授权的异常。

@Post('/save')
saveCommodity(@Body() commodity: Commodity, @Headers('Accept') token: string) {
  if (token !== '*/*' || token) {
    this.commodityService.create(commodity)
  } else {
    throw new HttpException('无权限操作', HttpStatus.UNAUTHORIZED)
  }
}

我们没有携带Accept请求接口后,我们收到的响应数据如下:

{
  "statusCode": 401,
  "message": "无权限操作"
}

从上述的例子我们可以看到HttpException的构造函数必须接收两个参数:

  • 响应状态码
  • 返回的消息,消息可以是字符串、数字和对象
  • 提供错误的原因,它不会被记录到响应的数据中,但是我们可以用于错误日志的收集,这样有利于我们遇到问题的时候可以很好的对问题进行定位分析

具体实例如下:

@Post('/save')
saveCommodity(@Body() commodity: Commodity) {
  try {
    await this.commodityService.create(commodity)
  } catch (error) {
    throw new HttpException({
      status: HttpStatus.UNAUTHORIZED,
      error: '系统异常',
    }, HttpStatus.UNAUTHORIZED, {
      cause: error
    });
  }
}

上述的接口发生异常时,返回的结果如下:

{
  "status": 401,
  "error": "系统异常"
}

自定义异常

有些时候因业务的需求我们可能需要编写自定义异常,那么自定义异常必须继承HttpException类。具体的例子如下:

export class ForbiddenException extends HttpException {
  constructor(errorInfo: ForbiddenExceptionInterface) {
    // 可以自定义接受的参数,我们就可以对捕获到的异常进行处理
    console.log(errorInfo.message);
    super(errorInfo.type, errorInfo.start);
  }
}

内置 HTTP 异常

NestJs 提供的内置 Http 异常如下:

异常类异常说明
BadRequestException错误请求异常,比如请求的参数类型不对或者请求参数缺失的时候都可以返回这个错误
UnauthorizedException当用户没有登录就调用需要鉴权的接口的话,就可以抛出这个问题
NotFoundException类似于 404 错误的抛出
ForbiddenException类似于 403 错误
NotAcceptableException类似于 406 错误
RequestTimeoutException请求超时异常
ConflictException类似于 409 错误
GoneException类似于 410 错误
HttpVersionNotSupportedExceptionHTTP 的版本不对的时候可以抛出这个异常
PayloadTooLargeException接口负载太高的时候可以抛出这个异常
UnsupportedMediaTypeException当上传的文件类型不符合条件的时候就可以抛出这个异常
UnprocessableEntityException比如我们添加某条数据时会提交对应的实体,假如请求的实体参数与定义的实体不匹配的时候就可以抛出这个异常
InternalServerErrorException类似于 500 错误
NotImplementedException当服务类调用了未实现的方法,我们可以抛出这个异常
ImATeapotException类似于 226 错误
MethodNotAllowedException比如请求接口是 Get 方式的,但是前端调用是 Post 的时候,我们可以抛出这个异常
BadGatewayException网关出现错误的时候可以抛出这个异常
ServiceUnavailableException当服务类不存在或者服务内部出现错误的时候就可以抛出这个异常
GatewayTimeoutException网关超时的时候就可以抛出这个异常
PreconditionFailedException类似于 412 错误

所有内置异常还可以 cause 使用以下参数提供错误和错误描述 options:

throw new BadRequestException("请求参数有误", {
  cause: new Error(),
  description: "姓名不能为数字",
});

上述的异常返回的数据如下:

{
  "message": "请求参数有误",
  "error": "姓名不能为数字",
  "statusCode": 400
}

异常过滤器

虽然基本(内置)异常过滤器可以自动为您处理许多情况,但您可能希望完全控制异常层。例如,您可能想要添加日志记录或根据某些动态因素使用不同的 JSON 架构。异常过滤器正是为此目的而设计的。它们使您可以控制确切的控制流程以及发送回客户端的响应内容。

我们创建一个异常过滤器类,负责捕获HttpException并且重组返回的异常数据。具体的代码如下:

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,
    });
  }
}

上述代码捕获到异常后,会把返回的结构体转为如下的形式,并非默认的数据结构:

{
  "statusCode": 401,
  "timestamp": "2023-07-29T04:21:08.189Z",
  "path": "/commodity/save"
}

注意:所有异常过滤器都应该实现通用 ExceptionFilter接口。这要求您提供 catch(exception: T, host: ArgumentsHost)带有指定签名的方法。T 表示异常的类型。

装饰@Catch(HttpException)器将所需的元数据绑定到异常过滤器,告诉 Nest 这个特定的过滤器正在寻找类型的异常 HttpException,而不是其他任何东西。装饰@Catch()器可以采用单个参数或逗号分隔的列表。这使您可以同时为多种类型的异常设置过滤器。例如如下的代码:

@Catch(HttpException, BadRequestException)

参数主机

通过上述的例子我们可以看到catch()方法主要是接收两个参数,一个是exception用于获取当前处理的异常对象,另外一个是host它是ArgumentsHost对象,在后续的笔记中说明这个对象。

上述的例子中我们使用host来获取对传递给原始请求处理程序(在引发异常的控制器中)的和对象的引用。在此代码示例中,我们使用了一些辅助方法来获取所需的对象。

绑定过滤器

异常过滤器的使用很简单,使用@UseFulter()装饰器就可以完成过滤器的绑定,具体的例子如下:

@Post('/save')
@UseFilters(new HttpExceptionFilter())
saveCommodity(@Body() commodity: Commodity, @Headers('Accept') token: string) {
  if (token !== '*/*' && token) {
    this.commodityService.create(commodity)
  } else {
    throw new ForbiddenException({ message: '这是自定义错误', type: 'UNAUTHORIZED', start: HttpStatus.UNAUTHORIZED })
  }
}

@UserFilters()装饰器可以接收单个过滤器实例,也可以接收多个实例,多个实例之间可以用逗号分隔。或者我们可以直接传递过滤器类,让框架帮我们实例化过滤器并帮我们依赖注入。

@Post('/save')
@UseFilters(HttpExceptionFilter)
saveCommodity(@Body() commodity: Commodity, @Headers('Accept') token: string) {
  if (token !== '*/*' && token) {
    this.commodityService.create(commodity)
  } else {
    throw new ForbiddenException({ message: '这是自定义错误', type: 'UNAUTHORIZED', start: HttpStatus.UNAUTHORIZED })
  }
}

注意:尽可能使用类而不是实例来应用过滤器。它减少了内存使用量,因为 Nest 可以轻松地在整个模块中重用同一类的实例。

异常过滤器不仅仅可以在控制器上进行绑定还可以在全局访问进行绑定。全局访问的过滤器我们可以这样进行声明:

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

注意:useGlobalFilters()方法不会为网关和混合应用程序设置过滤器。

全局访问的过滤器是应用于整个应用程序、每个控制器和每个路由处理程序的。当我们需要在某个模块注入全局过滤器的依赖项是不行的,因为全局的过滤器是在没有上下文环境下完成,所以没有上下文的支持,模块是不能找到对应实例。为了解决这个问题,我们可以用下面的方式来进行过滤器的注入:

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

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

采用上述的方式进行异常过滤器的注入,注入后的过滤器还是全局的。

捕获所有类型的异常

为了捕获每个未处理的异常(无论异常类型如何),请将@Catch()装饰器的参数列表留空,例如@Catch()。例如如下的代码:

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);
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值