Skip to content

异常过滤器

Nest 有个内置的异常处理层,异常过滤器(exception filters) 的定位是用于处理未手动捕获的异常。

image-20240121121245877

在服务端处理的过程中,很多时候都需要抛出异常,而这种异常是不能乱抛的,往往需要真实准确的异常错误信息反馈给前端。

内置 HTTP 异常

Nest 提供一组继承自基类 HttpException 的标准异常,代表许多最常见的 HTTP 异常。

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

内置异常还可以使用 options 参数提供错误信息和错误码:

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

响应将如下所示:

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

抛出异常

只要我们在程序中抛出错误时,就会被异常层捕获并响应给客户端:

ts
@Controller("cats")
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  getHello(): string {
    throw new HttpException(
      {
        message: "请求错误",
        error: true,
        statusCode: HttpStatus.BAD_REQUEST
      },
      HttpStatus.BAD_REQUEST
    );
  }
}

访问 / ,客户端会接收到抛出的异常:

image-20240121124247607

自定义异常

大部分情况下,Nest 内置的异常类已经覆盖 HTTP 请求大部分的场景。如果确实需要创建自定义异常,这个异常类需要继承基类 HttpException 类,Nest 将会识别我们抛出的异常。

ts
import { HttpException } from "@nestjs/common";
import { HttpExceptionOptions } from "@nestjs/common/exceptions/http.exception";

export class ParamException extends HttpException {
  constructor(response: string | Record<string, any>, status: number, options?: HttpExceptionOptions) {
    super(response, status, options);
  }
}

// 抛出自定义异常
@Get()
getHello(): string {
  throw new ParamException(
    {
      message: "参数错误",
      error: true,
      statusCode: HttpStatus.BAD_REQUEST
    },
    HttpStatus.BAD_REQUEST
  );
}

但是在实际开发处理中,服务端一般不会抛 HTTP 异常给客户端。

一般的做法是遵循 RESTFul 风格,返回给前端 HttpCode: 200,并约定业务使用的 errorcode 编码,抛出通用的业务错误。前端根据约定的编码,给予用户对应的错误提示。

基于这种场景,我们需要对错误信息进行过滤,在任何非预期的业务错误场景下,只需要抛出这个异常即可。

业务错误码

错误码设计如下:

  • 业务错误码由 10 位纯数字组成。
  • 业务错误码格式:4030000001
  • 错误码说明
1-3位4-5位6-8位9-10位
4030000001
HTTP状态码服务编号,单体服务为00模块编码业务错误编码,001表示未登录

以下是个人习惯封装的样子:

ts
interface HttpError{
  message: string; // 错误详细描述
  error_code: number; // 业务错误编码
  code: string; // 业务错误枚举key
  path: stirng;
}

我们还需要实现异常过滤器,负责捕获 HttpException 类实例的异常,并为它们实现自定义响应逻辑。

异常过滤器类需要实现 ExceptionFilter 接口,@Catch() 装饰器则是告诉 Nest 在发生异常时,需要过滤的类型为 ParamException (如果为空,默认过滤全部异常类型,但是多数情况下,默认需要过滤全部情况),我们可以一次传递多个异常类型。

ts
@Catch(ParamException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost): any {
    console.log(exception);
    console.log(host);
  }
}

参数ArgumentsHost

可以发现,请求工具(postman)处于一直挂起,这是因为我们在过滤器中未返回RequestObject 内容,!!这会导致客户端一直挂起。

ArgumentsHost 参数则可以获取适当的上下文对象(底层依赖的框架 Express 或其他适配器), RequestObject可以从中获取,接着我们只要返回内容,即可解除请求挂起的问题。

控制器抛出错误,由过滤器过滤之后,再返还给客户端:

展开查看异常枚举
ts
// 异常枚举
class ErrorCode {
  readonly #code: number;
  readonly #msg: string;
  readonly #errorCode: string;
  constructor(code: number, errorCode: string, msg: string) {
    this.#code = code;
    this.#msg = msg;
    this.#errorCode = errorCode;
  }

  get msg() {
    return this.#msg;
  }

  get code() {
    return this.#code;
  }
  get errorCode() {
    return this.#errorCode;
  }
}

export const AUTH_TOKEN_EXPIRED = new ErrorCode(4030000002, "AUTH_TOKEN_EXPIRED", "TOKEN 已经过期或失效");
ts
// cats.controller.ts
@Get()
getHello(@Query("id") id: string): string {
  if (!id) {
    throw new HttpException(
      {
        error_code: AUTH_TOKEN_EXPIRED.errorCode,
        code: AUTH_TOKEN_EXPIRED.code,
        message: AUTH_TOKEN_EXPIRED.msg
      },
      HttpStatus.FORBIDDEN
    );
  } else {
    return "Hello World! " + id;
  }
}

完善参数不全的异常过滤器并响应给客户端:

ts
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost): any {
    // 取得上下文
    const ctx = host.switchToHttp();
    // 响应对象
    const response = ctx.getResponse<Response>();
    // 请求对象
    const request = ctx.getRequest<Request>();
    // 错误对象
    const reason = exception.getResponse() as object;

    console.log("reason", reason);

    response.send({
      ...reason,
      path: request.url
    });
  }
}

image-20240121220425386

绑定位置

过滤器绑定的位置分为模块、全局。

模块注册:

  • 绑定在路由方法之上。
  • 绑定在控制器上。
ts
// 2.绑定整个控制器
@UseFilters(HttpExceptionFilter)
@Controller("cats")
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  // 1.绑定在路由方法上
  @UseFilters(HttpExceptionFilter)
  @Get()
  getHello(@Query("id") id: string): string {
    if (!id) {
      throw new HttpException(
        {
          error_code: AUTH_TOKEN_EXPIRED.errorCode,
          code: AUTH_TOKEN_EXPIRED.code,
          message: AUTH_TOKEN_EXPIRED.msg
        },
        HttpStatus.FORBIDDEN
      );
    } else {
      return "Hello World! " + id;
    }
  }
}

全局注册:

  • main 中通过 app.useGlobalFilter 注册全局过滤器。
  • 通过依赖注入的形式注册过滤器。
ts
app.useGlobalFilter(HttpExceptionFilter)

依赖注入

依赖注入

app.useGlobalFilter 的方式无法使用依赖注入的服务。我们可以在模块中使用依赖注入的方式,这样我们的自定义管道也能使用依赖服务做一些事情。比如在管道中查询数据库,判断是否存在该用户的id。学习更多依赖注入点击这里

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

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
    AppService
  ],
})

export class AppModule {}

这样注册后,在 HttpExceptionFilter 方法中就能使用 AppService 这个注入的服务。

案例一:封装异常错误对象

如有转载或 CV 的请标注本站原文地址