NestJS07-Exception filters

572 阅读8分钟

Nest有个内置的异常处理层,它的责任是处理访问应用程序没有处理的那些异常。当您的处理没有处理异常的时候,它会被这个层捕获,并且自动的返回一个对用户友好的返回。

Exception filter.png

开箱即用,此操作由内置的全局Exception filters异常过滤器执行,该过滤器处理HttpException类型的异常(及其子类)。当有异常被忽略(既不是HttpException也不是继承自HttpException的类),这个内置的异常过滤器会返回默认的JSON

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

抛出标准异常

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

例如,在CatsController里面,有findAll()方法。让我们假设这个路由处理程序出于某种原因抛出了一个异常。为了证明这一点,我们将硬编码如下:

  // cats.controller.ts
  @Get()
  async findAll() {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
  }

当客户端调用这个接口,会返回

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

HttpException构造函数必须传入2个参数来特定返回结果

  • response参数用来定义JSON返回的内容。可以是字符串也可以是Object对象/
  • status参数用来定义HTTP status code

默认的情况下,JSON返回内容里面有2个属性

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

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

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

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

  @Get('throw')
  async findAll_throw() {
    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"
}

自定义异常

在大多数情况下,您不需要使用自定义异常,并且使用内置Nest HTTP 异常,内置 HTTP 异常会描述。如果不得不创建自定义异常,比较好的做法是继承HttpException,Nest 会组织你的异常,并且自动生成异常返回内容。让我们来试一下

import {HttpException, HttpStatus} from '@nestjs/common'

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

因为ForbiddenException继承了HttpException,他的工作方式就和内置异常比较类似了。

  @Get('custom')
  async findAll_custom() {
    throw new ForbiddenException();
  }

内置 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

所有的内置异常都有cause(error)和options(error描述)参数

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

掉用上面Exception,我们可以看到下面的返回结果

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

异常过滤

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

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

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,
      });
  }
}
提示:
所有异常筛选器都应实现通用ExceptionFilter<T>接口。这需要您提供catch(exception:T,host:ArgumentHost)方法及其指定的签名。T表示异常的类型。
警告:
如果您使用的是@nestjs/platform-fastify,则可以使用response.send()代替response.json()。不要忘记从fastify导入正确的类型。

@Catch(HttpException)装饰器告诉Nest在发生HttpException的情况下,处理过滤器的逻辑。@Catch()可以使用单个参数或逗号分隔的列表。这允许您一次为几种类型的异常设置筛选器。

参数ArgumentsHost

让我们看看catch()方法的参数。exception参数是当前正在处理的异常对象。host参数是ArgumentHost对象。ArgumentsHost是一个强大的实用程序对象,具体可以参照execution context chapter。在这个代码示例中,我们使用它获取对传递给原始请求处理程序(在异常发生的控制器中)的请求Request和响应Response对象的引用。在这个代码示例中,我们使用了ArgumentHost上的一些助手方法来获取所需的请求和响应对象。了解更多关于ArgumentsHost

这种抽象级别的原因是ArgumentHost在所有上下文中都起作用(例如,我们现在使用的HTTP服务器上下文,以及微服务和WebSocket)。

绑定过滤器

CatsControllercreate()方法上new一个HttpExceptionFilter

  /**
   * 绑定过滤器
   */
  @Post('bind')
  @UseFilters(new HttpExceptionFilter())
  async create(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
  }
说明:`@UserFilters`通过`@nestjs/common`包导入

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

  @Post('bindNoNew')
  @UseFilters(HttpExceptionFilter)
  async create_no_new(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
  }
说明:推荐尽可能使用类来绑定过滤器,这会减少内存使用率。Nest可以重复利用这些类

例子里面我们的使用方法是加在方法上的。属于方法的作用范围。我们也是使用类过滤器或者全局过滤器。

类过滤器:

@Controller('cats')
// 类过滤器
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

全局过滤器:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
警告:useGlobalFilters()方法不为gateways或混合应用程序设置筛选器。

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

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

捕获所有异常

为了可以捕获所有的异常,我们可以不给@catch()传参。

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

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 {
    // 在某些情况下,“httpAdapter”可能在
    // 构造函数方法里,因此我们应该在这里解决它。
    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);
  }
}
警告:当将捕获所有内容的异常筛选器与绑定到特定类型的筛选器组合时,应首先声明“捕获任何内容”筛选器,以允许特定筛选器正确处理绑定类型。

继承

通常,您将创建完全定制的异常过滤器,以满足您的应用程序要求。但是,在某些情况下,您可能希望简单地扩展内置的默认全局异常筛选器,并基于某些因素重写行为。

为了将异常处理委托给基本筛选器,需要扩展BaseExceptionFilter并调用继承的catch()方法。

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}
警告:扩展BaseExceptionFilter的方法范围和控制器范围的筛选器不应使用new实例化。相反,让框架自动实例化它们。

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

全局过滤器可以扩展基本过滤器。这可以通过两种方式中的任一种来实现。

第一种方法是在实例化自定义全局筛选器时注入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_FILTER令牌:binding-filters

本章代码

代码