[NestJS 带你飞!] DAY08 - Exception & Exception filters

什麽是 Exception?

简单来说就是系统发生了错误,导致原本程序无法完成的情况,这种时候会尽可能把错误转化为有效资讯。通常一套系统都会针对错误做处理,提供有效的错误讯息,就如一间大餐厅收到客诉必须出面回应客人,并要让客人觉得这个回覆是有经过系统整理的,而不是草率回应。

在 JavaScript 中,最常见的抛出错误方法就是使用 Error,这个 Error 即为 Exception 的概念,把错误讯息包装起来变成统一格式:

throw new Error('我达达的马蹄是美丽的错误');

Nest 错误处理机制

在抛出错误後,需要有个机制去捕捉这些错误,并从中提取资讯来整理回应的格式,Nest 在底层已经帮我们做了一套错误处理机制 - Exception filter,它会去捕捉抛出的错误,并将错误讯息、HttpCode 进行友善地包装:
https://ithelp.ithome.com.tw/upload/images/20210323/20119338ITCS0KRsJ5.png

我们可以做个小实验,修改一下 app.controller.ts 的内容,在 getHello() 里直接抛出 Error

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new Error('出错罗!');
    return this.appService.getHello();
  }
}

这时候透过浏览器查看 http://localhost:3000 会发现收到的错误资讯跟我们定义的「出错罗!」不同:

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

原因是 Nest 内建的 Exception filter 会去侦测抛出的错误是什麽类型的,它只能够接受 Nest 内建的 HttpException 与继承该类别的 Exception,若不属於这类型的错误就会直接抛出 Internal server error

标准 Exception

Nest 内建的标准 Exception 即为 HttpException,它是一个标准的 class,提供非常弹性的使用体验,透过给定 constructor 两个必填参数来自订错误讯息与 HttpCode。这里先来进行简单的测试并修改 app.controller.ts

import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new HttpException('出错罗!', HttpStatus.BAD_REQUEST);
    return this.appService.getHello();
  }
}

透过浏览器查看 http://localhost:3000 会发现与我们期望的回应是一致的:

{
  "statusCode": 400,
  "message": "出错罗!"
}

那如果今天不想要用 Nest 的预设格式怎麽办?可以把第一个错误讯息的参数换成 Object,Nest 会自动 覆盖格式。这里一样做个简单的测试并修改 app.controller.ts

import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new HttpException(
      {
        code: HttpStatus.BAD_REQUEST,
        msg: '出错罗!'
      },
        HttpStatus.BAD_REQUEST
      );
    return this.appService.getHello();
  }
}

透过浏览器查看 http://localhost:3000 会发现格式已经变成我们预期的样子了:

{
  "code": 400,
  "msg": "出错罗!"
}

内建 Http Exception

Nest 有内建一套基於 HttpException 的 Exception 可以使用,让开发者根据不同的错误来选用不同的 Exception,这里将它们列出来给大家参考:

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

这边我们挑选 BadRequestException 进行测试并修改 app.controller.ts

import { BadRequestException, Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException('出错罗!');
    return this.appService.getHello();
  }
}

这时候透过浏览器查看 http://localhost:3000 会得到下方的回应内容:

{
  "statusCode":400,
  "message":"出错罗!",
  "error":"Bad Request"
}

如果不想用 Nest 的预设格式,同样可以把参数换成 Object 来覆盖:

import { BadRequestException, Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException({ msg: '出错罗!' });
    return this.appService.getHello();
  }
}

透过浏览器查看 http://localhost:3000 会发现格式为我们预期的样子:

{
  "msg": "出错罗!"
}

自订 Exception

前面有提到 HttpException 为标准的 class,这表示我们可以自行设计类别来继承 HttpException,达到自订 Exception 的效果。不过大多数情况下不太需要自订,因为 Nest 提供的 Exception 已经很够用了!这边我们先新增一个 custom.exception.tssrc/exceptions 底下:

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

export class CustomException extends HttpException {
  constructor () {
    super('未知的错误', HttpStatus.INTERNAL_SERVER_ERROR);
  }
}

修改 app.controller.ts 进行测试:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { CustomException } from './exceptions/custom.exception';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new CustomException();
    return this.appService.getHello();
  }
}

透过浏览器查看 http://localhost:3000 会发现与预期结果相同:

{
  "statusCode":500,
  "message":"未知的错误"
}

自订 Exception filter

如果希望完全掌握错误处理机制的话,Nest 是可以自订 Exception filter 的,透过这样的方式来添加 log,或是直接在这个层级定义回传的格式。Exception Filter 必须要使用 @Catch(...exceptions: Type<any>[]) 装饰器来捕捉错误,可以指定要捕捉特定类别的 Exception,也可以捕捉全部的错误,若要捕捉全部就不需要带任何参数到 @Catch 里,另外,还需要让该 class 去实作 ExceptionFilter<T>,它会限制一定要设计 catch(exception: T, host: ArgumentsHost) 这个方法。我们在 src/filters 下新增 http-exception.filter.ts,来建立一个捕捉 HttpException 的 Exception filter:

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const message = exception.message;
    const timestamp = new Date().toISOString();

    const responseObject = {
        code: status,
        message,
        timestamp
    };
    response.status(status).json(responseObject);
  }
}

这个范例主要是希望可以捕捉 HttpException,在捕捉时,会获得该 Exception 以及一个叫 ArgumentsHost 的东西,透过它来取得 Response 物件,进而回传下方的格式到客户端:

{
  "code": 400,
  "message": "出错罗!",
  "timestamp": "2021-09-23T06:45:55.216Z"
}

ArgumentsHost

是一个用来取得当前请求相关参数的 class,它是一个抽象概念,由於 Nest 能够实作 REST API、WebSocket 与 MicroService,每个架构的参数都会有些不同,这时候透过抽象的方式做统合是最合适的,以 Express 作为底层的 REST API 来说,它封装了 RequestResponseNextFunction,但如果是 MicroService 的话,封装的内容物又不同了,所以 ArgumentsHost 提供了一些共同介面来取得这些底层的资讯:

取得当前应用类型

透过 getType() 取得当前应用类型,以 REST API 来说,会得到字串 http

host.getType() === 'http'; // true

取得封装参数

透过 getArgs() 取得当前应用类型下封装的参数,以 Express 为底层的 REST API 来说,即 RequestResponseNextFunction

const [req, res, next] = host.getArgs();

从上面可以得出封装的参数为 阵列格式,Nest 提供了透过索引值取得参数的方法 - getArgByIndex(index: number)

const req = host.getArgByIndex(0);

以上的方法都是透过对阵列的操作来取得相关参数,但这样在面对不同架构的重用 会有困难,毕竟不同架构的封装参数都会不同,这时候可以使用下方的方式来取得相关内容:

const rpcCtx: RpcArgumentsHost = host.switchToRpc(); // MicroService 的封装内容
const httpCtx: HttpArgumentsHost = host.switchToHttp(); // REST 的封装内容
const wsCtx: WsArgumentsHost = host.switchToWs(); // WebSocket 的封装内容

使用 Exception filter

使用方法十分简单,粗略地分成两种:

  1. 单一资源:在 Controller 的方法中套用 @UseFilters 装饰器,只会针对该资源套用。
  2. Controller:直接在 Controller 上套用 @UseFilters 装饰器,会针对整个 Controller 中的资源套用。

@UseFilters 的参数则带入要使用的 Exception filter。下方为单一资源的范例,修改 app.controller.ts 进行测试:

import { BadRequestException, Controller, Get, UseFilters } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  @UseFilters(HttpExceptionFilter)
  getHello(): string {
    throw new BadRequestException('出错罗!');
    return this.appService.getHello();
  }
}

下方为 Controller 套用的范例:

import { BadRequestException, Controller, Get, UseFilters } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

@Controller()
@UseFilters(HttpExceptionFilter)
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException('出错罗!');
    return this.appService.getHello();
  }
}

上方的两个范例输出结果相同,透过浏览器查看 http://localhost:3000

{
  "code":400,
  "message":"出错罗!",
  "timestamp":"2021-09-23T07:05:11.102Z"
}

注意@UseFilters 带入的 Exception filter 可以是 class 本身,也可以是实例,他们的差别在於使用 class 会透过 Nest 依赖注入进行实例的管理,而带入实例的话则不会。若没有特别需要的话,还是以带入 class 为主。

全域 Exception filter

如果我的 Exception filter 是要套用到每一个资源上的话,不就要替每个 Controller 都添加 @UseFilters 吗?别担心,Nest 非常贴心提供了配置在全域的方法,只需要在 main.ts 进行修改即可:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

透过 useGlobalFilters 来指定全域的 Exception filter,实在是非常方便!

用依赖注入实作全域 Exception filter

上面的方法是透过模组外部完成全域配置的,如果希望透过依赖注入的方式来实作的话有没有什麽方式可以达到呢?Nest 确实有提供解决方案,只需要在 AppModule 进行配置,既然是用依赖注入的方式,那就跟 Provider 脱离不了关系,透过指定 tokenAPP_FILTER 来实现,这里是用 useClass 来指定要建立实例的类别:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

小结

要如何做好错误处理是一门学问,Nest 提供了制式化却不失弹性的解决方案,透过 Exception 定义错误格式,并经由 Exception filter 统一处理这些错误,省去了很多麻烦。这边附上今天的懒人包:

  1. Exception 即为错误物件。
  2. Nest 内建的标准 Exception 为 HttpException
  3. Nest 内建错误处理机制,名为 Exception filter,会自动处理错误并包装回应格式。
  4. 内建的 Exception filter 在收到非 HttpException 系列的 Exception 时,会统一回覆 Internal server error
  5. HttpException 可以透过给定 Object 来覆写格式。
  6. Nest 内建大量的 Http Exception。
  7. 可以自订 Exception filter,并可以套用至单一资源、Controller 或全域。
  8. 全域 Exception filter 可以透过依赖注入的方式实作。

<<:  ASP.NET MVC 从入门到放弃(Day18)-MVC检视(View)介绍

>>:  Day 0x16 UVa10235 Simply Emirp

[DAY18] 用 Azure Machine Learning SDK 建立 Workspace

DAY18 用 Azure Machine Learning SDK 建立 Workspace 大家...

[Day 20] Edge Impulse + BLE Sense实现唤醒词辨识(上)

在[Day 16]和[Day 17]「TFLM + BLE Sense + MP34DT05 就成了...

[Part 1 ] Vue.js 的精随-元件

前言 接下来多篇的元件介绍会以官方文件 Components In-Depth 章节为主: 未知pa...

D4: [漫画]工程师太师了-第2.5话

工程师太师了: 第2.5话 杂记: 报名的时候没注意到有最低300个字限制阿~~~~(抱头 本来只想...

Day39. 建造者模式

本文同步更新於blog Builder Pattern 将复杂对象的构建与其表示分离。 建造者模式...