Nest —— Exception filters

2,168 阅读10分钟

前言

       之前匆匆写了一篇关于Nest官文的翻译,然而写的时候比较着急,所以很多地方的翻译比较马虎,甚至直接丢进翻译器在丢出来....(有的时候我会被丢进翻译器之后被丢出来的译文吓到,原因是...译文比自己翻译的好/(ㄒoㄒ)/~~)但还是有很多部分是翻译器解决不了的,语句通顺固然优雅锦上添花,但一点小的错误却是致命的。
       现在手头比较悠闲,也打算重新修改一份比较优雅的中文文档。有人会问花这么多时间写这个东西,是不是真的有用。百度上面也有一些关于Nest的文档,完全也不会有人来看你写的翻译。我的感受是,可能这就是我的学习方式吧。其实平时阅读文档,大多数情况下都是脑子说会了,手说不会。一边翻译英文文档,一遍理解框架的含义,还有助于提高阅读英文文档的能力。手敲过一遍和眼睛看过一遍真的不太一样,而且这种方式会增加使用时的自信。之后打算将每一章节分开书写,最后再通过链接汇总到一篇Nest妲己大记中去。就算没有人看,自己想要翻阅文档的时候,也可以拿出来看看,还能改改。这是一种乐趣,就好像养成游戏一样。



正文 Exception filters

Nest内置了一个异常层来负责处理未经处理得异常。当一个异常没有被你得应用代码处理时,就会被这一层捕获。然后自动地发送一个用户友好型响应。

这个动作由内置的全局异常过滤器来完成,处理了 HttpException(或其子类) 类型的异常。当一个异常无法被识别(既不是 HttpException 类型的异常,又不是继承自他的类),内置的异常过滤器会默认生成下面的json格式的响应。

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



Throwing standard exceptions 抛出标准的异常

Nest提供了一个内置的 HttpException 类。对于典型的基于HTTP REST/GraphQL的引用程序,当一个错误情况发生时,抛出标准的Http响应对象是非常好的。
例如,在 CatsController 中,我们有一个 findAll() 方法,设想这个路由由于一些原因抛出了一个异常。

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

Hints
这里我们使用了HttpStatus。这是一个辅助枚举类型。由@nestjs/common包引入。

当客户端访问这个节点时,响应如下。

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

HttpException 的构造器有两个必传入参决定了响应的形式:

  • response定义了JSON格式的响应体,可以是一个字符串,一个对象。
  • status参数绝对了Http状态码

JSON格式的响应体默认包含两个属性

  • statusCodestatus参数中提供的Http状态码的默认值
  • message:基于status状态码的对于Http错误的简短的描述

想要仅仅重写JSON响应体中message信息的部分,可以提供一个字符类型的response入参。
要重写整个JSON响应体,在response参数中传入一个对象。Nest会序列化这个对象并作为JSON响应体返回。
第二个构造器的参数——status,应当是一个合法的Http状态码。最好是使用由@nestjs/common导出的枚举类型 HttpStatus
重写整个响应体示例:

// cats.controller.ts
@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, 403)
}

然后响应如下:

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



Custom exceptions 自定义的异常

大多数情况下你不需要写自定义的异常,可以使用Nest内置的Http异常。如果你需要创建一个自定义的异常,最好是创建你自己的异常层,使用继承自 HttpException 的类。这样的话Nest就能识别你的异常,自动处理错误响应。

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

因为 ForbiddenException 继承自 HttpException ,他将与内置的异常处理无缝衔接,然后可以在 findAll() 方法中使用。

// cats.controller.ts
@Get()
async findAll() {
  throw new ForbiddenException()
}



Built-in Http exceptions 内置的Http异常

Nest提供了一系列继承自 HttpException 的标准的异常。由@nestjs/common导出,代表了许多常见的异常。

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • InternalServerErrorException
  • NotImplementedException
  • ServiceUnavailableException
  • GatewayTimeoutException



Exception filters 异常过滤器

虽然内置的异常过滤器可以自动地为你处理绝大多数地情况,然而你可能还是想要对异常层完全地控制。例如你可能想要添加日志,或者基于一些动态的因素来使用不同的JSON结构。异常过滤器就是为此目的而生。 他们允许你控制发送回客户端的对流的准确控制和响应内容。
让我们创建一个用来捕获异常的异常过滤器,异常是 HttpException 类的实例,并为他们实现自定义的响应逻辑。为此我们需要访问底层的请求和响应对象。我们访问请求对象来获取原始url并包括日志信息,访问响应对象通过response.json()来直接控制发送的响应。

// http-exception.filter.ts
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
      })
  }
}

Hints
所有的异常过滤器都应该实现ExcptionFilter<T>接口。这需要你提供catch(exception: T, host: ArgumentsHost)方法及其函数签名。T是异常的类型。 装饰器

@Catch(HttpException) 为异常过滤器绑定了所需的元数据,告诉Nest这个特殊的过滤器正在寻找 HttpException 类型的异常。装饰器 @Catch() 接受一个参数,或者逗号隔开的列表。可以让你立刻为多种类型的异常设置过滤器。



Arugements host 参数主机

让我们看一下 catch() 方法的参数。入参exception是当前生成的异常对象。host入参是一个 ArgumentsHost 类型的对象。 ArgumentsHost 是一个强大的实用对象,将在别的章节(ing)进一步研究。在上下文中,它主要的目的就为我们提供传入原始请求处理函数的请求和响应对象(在发生异常的控制器中)。上下文中,我们使用一些在 ArgumentsHost 上的辅助方法来获取想要的请求和响应对象。
host.switchToHttp() 辅助方法返回一个 HttpArguementsHost 对象。 HttpArguementsHost 对象依次具有两个有用的方法。我们使用这些方法获取想要的对象,也可以使用Express类型断言来返回原生的Express类型对象。

const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()

ArgumentsHost 函数在所有上下文都起作用。稍后我们会看见如何在通过 ArgumentsHost 以及他的辅助函数在任何执行环境中访问适当的底层参数。这将允许我们书写更加通用的异常过滤器来处理所有环境中的异常。

Binding filters 绑定过滤器

HttpExceptionFilter 和控制 CatsControllercreate() 方法联系起来。

// cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CrateCatDto) {
  throw new ForbiddenException()
}

Hints
import { UseFilters } from '@nestjs/common'

我们在这里使用了 @UseFilters() 装饰器。就像是 @Catch() 一样,接受一个过滤器的实例,或者逗号分隔的实例列表。这里,我们创建 HttpExceptionFilter 的实例。或者你也可以传入类,将实例化的工作交给框架来完成,并且启用依赖注入。

// cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException()
}

Hints
尽可能地去使用类而不是实例,减少了内存使用。因为Nest可以在整个模块中重用类生成的同一个实例。

在上面的例子中, HttpExceptionFilter 只应用到了一个 create() 路由处理函数。异常过滤器也可以应用在不同的范围中:方法、控制器或者全局。例如,设置一个控制器范围的过滤器:

// cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

这里为每个在 CatsController 中定义的路由处理函数设置了 HttpExceptionFilter 异常过滤器。
创建一个全局过滤器:

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

Warning
useGlobalFilters()方法不会为网关或混合应用应用设置过滤器。

全局范围的过滤器在整个应用中被使用,覆盖所有的控制器和路由处理函数。按照依赖注入,在任何模块之外注册的全局过滤器(就如上面使用 useGlobalFilters() 注册的过滤器)不能注入依赖,因为这是在任何模块的环境之外完成的。为了解决这个问题,你可以使用下面的形式注册一个全局的过滤器。

// app.module.ts
import { Module } from '@nestjs/common'
import { APP_FILTER } from '@nestjs/core'

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

Hints
当使用这种方法来为过滤器执行依赖注入,注意无论模块的构造器在哪里使用,过滤器是什么,实际上都是全局范围的。所以在哪里去写这个代码呢?选择过滤器定义的模块(就像上面 HttpExceptionFilter 的例子)。 useClass() 方法也不是处理自定义的provider注册的唯一的方式。

你可以使用这种技术按需添加许多过滤器,只要简单的放入providers数组中。



Catch everything 捕获所有

想要捕获所有未处理的异常(无论异常的类型是什么),不要在 @Catch() 方法中传参就好。

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

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArguementsHost) {
    const ctx = host.switchToHtttp()
    const response = ctx.getResponse()
    const request = ctx.getRequest()
    
    const status =
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR
      
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString()
      path: requets.url
    })
  }
}

上例中的过滤器会捕获所有类型的异常。



Inheritance 继承

你会创建完全自定义的过滤器来满足你应用的需求,然而,有时你可能只是想简单地扩展内置的全局过滤器,基于某些因素重写习惯。
为了将异常处理委托给基础的过滤器,你需要扩展 BaseExceptionFilter 并且调用继承的 catch() 方法。

// all-exceptions.filter.ts
import { Catch, ArguementsHost } from '@nestjs/common'
import { BaseExceptionFilter } from '@nestjs/core'

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArugementsHost) {
    super.catch(exception, host)
  }
}

Warning
扩展了 BaseExceptionFilter 方法范围和控制器返回的过滤器不可以使用new类实例化。而是要交给框架来完成。

上面的例子只是演示方法的概括。你的经扩展的异常过滤器会包含你特定的业务逻辑。

全局过滤器有两种方法扩展基础的过滤器。
第一个方法是当实例化自定义过滤器的时候注入 HttpServer 参数。

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  
  const { httpAdapter } = app.get(HttpAdapterHost)
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter))
  
  await app.listen(3000)
}
bootsrap()

第二个方法是使用 APP_FILTER 标识像这里(ing)展示那样。





后记

原文地址: docs.nestjs.com/exception-f…


关于本文

  • 文章非复制黏贴,经浏览文档,以自己的理解进行,代码测试,手打书写。本篇为翻译+意译。
  • 用作记录自己曾经学习、思考过的问题的一种笔记。
  • 用作前端技术交流分享。
  • 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。

关于我

  • 是一只有梦想的肥柴。
  • 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
  • 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
  • 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
  • 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz