Nest文档翻译-Exception filters-异常过滤器

272 阅读10分钟

Exception filters-异常过滤器

Nest 提供了一个内置的异常层,该层负责处理应用程序中所有未处理的异常。当应用程序代码没有处理异常时,这个异常将被该层捕获,然后自动发送一个对用户友好的响应。 image.png 作为开箱即用的功能,此操作由内置的全局异常过滤器执行,该过滤器处理 HttpException 类型的异常及其子类。当一个异常无法识别时(既不是 HttpException 也不是从 HttpException 继承的类) ,内置的异常过滤器会生成以下默认的 JSON 响应:

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

提示: 全局异常过滤器仅支持部分 http-error 库。基本上,包含 statusCodemessage属性的异常都将被正确填充并作为响应发送,而不是未识别异常 InternalServerErrorException

Throwing standard exceptions-抛出标准异常

Nest 提供了一个内置的从 @nestjs/common 包中导入的 HttpException 类。对于典型的基于 HTTP REST/GraphQL API 的应用程序,最佳实践是在出现某些错误条件时发送标准 HTTP 响应对象。

例如,在 CatsController 中,我们有一个 findAll()方法(一个 GET 路由处理程序)。让我们假设这个路由处理程序由于某种原因引发异常。为了证明这一点,我们将对其进行硬编码如下:

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

提示: 我们在这里使用了 HttpStatus。这是从 @nestjs/common 包导入的辅助枚举类。

当客户端调用此方法时,响应如下:

{
  "statusCode": 403,
  "message": "Forbidden"
}

HttpException 构造函数接受两个的必需参数来确定响应内容:

  • response 参数定义 JSON 响应主体。它可以是字符串或对象。
  • status 参数定义了 HTTP状态码。

默认情况下,JSON 响应主体包含两个属性:

  • statusCode :默认为 status 参数中提供的 HTTP状态码。
  • message : 基于 HTTP状态码 错误的简短描述

要覆盖 JSON 响应主体的消息部分,请在第一个参数 response 中提供一个字符串。要覆盖整个 JSON 响应体,在 response 参数中传递一个对象。Nest 将序列化该对象并将其作为 JSON 响应体返回。

第二个参数 status 应该是一个有效的 HTTP状态码。最佳实践是使用从 @nestjs/common 导入的 HttpStatus 枚举类。

有第三个构造函数参数(可选)- options -可用于提供错误原因。这个 cause 对象没有序列化到 response 对象中,但是它可以用于记录日志,提供有关导致引发 HttpException 的内部错误的有价值的信息。

下面是覆盖整个响应主体并提供错误原因的示例:

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) { 
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

如果使用上面的方法,响应内容看起来应该是这样的:

{
  "status": 403,
  "error": "This is a custom message"
}

Custom exceptions-自定义异常

在许多情况下,您不需要编写自定义异常,可以使用内置的 Nest HTTP 异常。如果确实需要创建自定义异常,最好创建自己的异常架构,其中自定义异常从基类 HttpException 继承。使用这种方法,Nest 将识别您的异常,并自动处理错误响应。让我们实现这样一个自定义异常:

export class BusinessException extends HttpException {

  constructor(err: BusinessError | string) {
    if (typeof err === 'string') {
      err = {
        code: BUSINESS_ERROR_CODES.COMMON,
        message: err
      }
    }
    super(err, HttpStatus.OK)
  }
}

因为 BusinessException 扩展了基本的 HttpException,所以它可以与内置的异常处理程序无缝地工作,因此我们可以在 findBusinessError() 方法中使用它。

@Get('findBusinessError')
@Version([VERSION_NEUTRAL, '1'])
findBusinessError() {
  const a: any = {}
  try {
    console.log(a.b.c)
  } catch (error) {
    throw new BusinessException('你这个参数错误')
  }
  return this.userService.findAll()
}

Built-in HTTP exceptions-内置的HTTP异常

Nest 提供了一组从基类 HttpException 继承的标准异常。它们是从 @nestjs/common 包中公开的,代表许多最常见的 HTTP 异常:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

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

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })

如果使用上面的方法,响应内容看起来应该是这样的:

{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400,
}

Exception filters-异常过滤器

虽然基本的内置异常过滤器可以为您自动处理许多情况,但你可能希望完全控制异常层。例如,你可能希望添加日志记录,或者基于某些动态因素使用不同的 JSON 格式。异常过滤器就是为此而设计的。它们可以让您控制精确的控制流和发送回客户端的响应的内容。

让我们创建一个异常过滤器,它负责捕获作为 HttpException 类实例的异常,并为它们实现自定义响应逻辑。为此,我们需要访问底层平台 RequestResponse 对象。我们将访问 Request 对象,以便提取原始 url 并将其包含在日志信息中。我们将使用 Response 对象直接控制发送的响应,使用 Response.json()方法( fastify 平台为 Response.send()

import { FastifyReply, FastifyRequest } from 'fastify'
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
import { BusinessException } from './business.exception';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<FastifyReply>()
    const request = ctx.getRequest<FastifyRequest>()
    const status = exception.getStatus()

    // 处理业务异常
    if (exception instanceof BusinessException) {
      const error = exception.getResponse()
      response.status(HttpStatus.OK).send({
        data: null,
        status: error['code'],
        extra: {},
        message: error['message'],
        success: false
      })
      return
    }

    response.status(status).send({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.getResponse()
    })
  }
}

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

@catch (HttpException) 装饰器将所需的元数据绑定到异常筛选器,告诉 Nest 这个特定的过滤器正在寻找 HttpException 类型的异常,而不是其他异常。 @ catch() 装饰符可以采用单个参数或逗号分隔的列表。这使你可以同时为多种类型的异常设置过滤器。

Arguments host

让我们看看 catch()方法的参数。 exception 参数是当前正在处理的异常对象。 Host 参数是 ArgumentsHost 对象。 ArgumentsHost 是一个强大的实用工具对象,我们将在 Execution context 执行上下文 章节 中进一步研究它。在这个代码示例中,我们使用它来获取对正在传递给原始的路由处理程序(在异常发生的控制器中)的 RequestResponse 对象的引用。

在这个代码示例中,我们在 ArgumentsHost 上使用了一些辅助方法来获取所需的 RequestResponse 对象。

  • 这种抽象级别的原因是 ArgumentsHost 在所有上下文(例如,我们现在正在处理的 HTTP 服务器上下文,还有 Microservices 和 WebSocket)中都起作用。在 执行上下文 一章中,我们将看到如何使用 ArgumentsHost 及其辅助函数的能力来访问任何执行上下文的基本底层参数。这将允许我们编写跨所有上下文操作的通用异常过滤器。

Binding filters-绑定过滤器

让我们将新的 HttpExceptionFilterCatsControllercreate()方法联系起来。

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

提示: @UseFilters()装饰符是从 @nestjs/common 包导入的。

我们在这里使用了 @UseFilters()装饰器。与 @Catch()装饰符类似,它可以接受单个过滤器实例,或者以逗号分隔的过滤器实例列表。在这里,我们就地创建了 HttpExceptionFilter 的实例。或者,您可以传递类(而不是实例),将实例化的责任留给框架,并启用依赖注入

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

提示: 在可能的情况下,使用类而不是实例来应用过滤器是更好的选择。它减少了内存使用,因为 Nest 可以轻松地在整个模块中重用同一类的实例。

在上面的示例中, HttpExceptionFilter 仅应用于单个 create()路由处理程序,这是方法范围的应用。异常过滤器的作用域可以是不同的级别:方法作用域、控制器作用域或全局作用域。例如,要将筛选器设置为控制器作用域,可以执行以下操作:

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

此构造为 CatsController 中定义的每个路由处理程序设置 HttpExceptionFilter

要创建全局范围的筛选器,您需要执行以下操作:

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

警告: UseGlobalFilters()方法不为网关或混合应用程序设置筛选器。

全局范围的过滤器对于整个应用程序中的每个控制器和每个路由处理器都生效。就依赖注入而言,从任何模块外部注册的全局过滤器(使用 useGlobalFilters() ,如上例所示)不能注入依赖项,因为这是在模块的上下文之外完成的。为了解决这个问题,您可以使用以下结构直接从任何模块注册全局范围的过滤器:

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

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

提示: 当使用这种方法为过滤器执行依赖注入时,请注意,不管在哪个模块中使用这种构造,过滤器实际上是全局的。在哪里完成定义?选择定义过滤器(上面示例中的 HttpExceptionFilter)的模块。此外, useClass 不是处理自定义提供程序注册的唯一方法。

您可以根据需要使用此技术添加任意多个过滤器; 只需将每个过滤器添加到 providers 数组。

Catch everything

为了捕获每个未处理的异常(不管异常类型是什么) ,保持 @Catch()装饰器的参数列表为空,例如 @Catch()

在下面的例子中,我们有一个与平台无关的代码,因为它使用 HTTP 适配器传递响应,并且不直接使用任何特定于平台的对象( RequestResponse):

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

警告: 当组合一个异常过滤器来捕获特定类型与一个过滤器捕获所有内容时,应该首先声明”捕获任何内容”过滤器,以允许特定过滤器正确处理绑定类型。

Inheritance-继承

通常,您将创建完全定制的异常过滤器,以满足应用程序的需求。但是,在某些用例中,您可能希望简单地扩展内置的默认全局异常过滤器,并基于某些因素覆盖行为。

为了将异常处理委托给基筛选器,需要扩展 BaseExceptionFilter 并调用继承的 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 的方法范围和控制器范围的过滤器不应使用新建实例。相反,让框架自动实例化它们。

上面的实现只是演示该方法的一个基本框架。扩展异常过滤器的实现将包括量身定制的业务逻辑(例如,处理各种条件)。

全局筛选器可以扩展基础筛选器通过两种方式。

第一种方法是在实例化自定义全局筛选器时注入 HttpAdapter 引用:

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_ ILTER 令牌。

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

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