NestJS 的 异常过滤器学习

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

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

json 复制代码
{
  "statusCode": 500,
  "message": "Internal server error"
}

使用 HttpException 抛出异常

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

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

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

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

json 复制代码
{
  "statusCode": 401,
  "message": "无权限操作"
}

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

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

具体实例如下:

typescript 复制代码
@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
    });
  }
}

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

json 复制代码
{
  "status": 401,
  "error": "系统异常"
}

自定义异常

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

typescript 复制代码
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 错误
HttpVersionNotSupportedException HTTP 的版本不对的时候可以抛出这个异常
PayloadTooLargeException 接口负载太高的时候可以抛出这个异常
UnsupportedMediaTypeException 当上传的文件类型不符合条件的时候就可以抛出这个异常
UnprocessableEntityException 比如我们添加某条数据时会提交对应的实体,假如请求的实体参数与定义的实体不匹配的时候就可以抛出这个异常
InternalServerErrorException 类似于 500 错误
NotImplementedException 当服务类调用了未实现的方法,我们可以抛出这个异常
ImATeapotException 类似于 226 错误
MethodNotAllowedException 比如请求接口是 Get 方式的,但是前端调用是 Post 的时候,我们可以抛出这个异常
BadGatewayException 网关出现错误的时候可以抛出这个异常
ServiceUnavailableException 当服务类不存在或者服务内部出现错误的时候就可以抛出这个异常
GatewayTimeoutException 网关超时的时候就可以抛出这个异常
PreconditionFailedException 类似于 412 错误

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

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

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

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

异常过滤器

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

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

typescript 复制代码
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,
    });
  }
}

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

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

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

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

typescript 复制代码
@Catch(HttpException, BadRequestException)

参数主机

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

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

绑定过滤器

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

typescript 复制代码
@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()装饰器可以接收单个过滤器实例,也可以接收多个实例,多个实例之间可以用逗号分隔。或者我们可以直接传递过滤器类,让框架帮我们实例化过滤器并帮我们依赖注入。

typescript 复制代码
@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 可以轻松地在整个模块中重用同一类的实例。

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

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

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

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

typescript 复制代码
import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";

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

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

捕获所有类型的异常

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

typescript 复制代码
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);
  }
}
相关推荐
神之王楠8 分钟前
学习风格的类型
学习
知识分享小能手21 分钟前
mysql学习教程,从入门到精通,SQL 删除数据(DELETE 语句)(19)
大数据·开发语言·数据库·sql·学习·mysql·数据开发
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑1 小时前
苍穹外卖学习笔记(五)
java·笔记·学习
张望远-长风万里1 小时前
运维监控专项学习笔记-id:0-需求场景、监控作用、监控能力
运维·笔记·学习
jun7788951 小时前
机器学习-监督学习:朴素贝叶斯分类器
人工智能·学习·机器学习
程序猿 董班长2 小时前
socket学习
学习
@月落2 小时前
alibaba获得店铺的所有商品 API接口
java·大数据·数据库·人工智能·学习
6230_3 小时前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
李小星同志4 小时前
高级算法设计与分析 学习笔记6 B树
笔记·学习
霜晨月c4 小时前
MFC 使用细节
笔记·学习·mfc