NestJS 🧑‍🍳 厨子必修课(八):异常过滤器

710 阅读7分钟

1. 前言

通过接口 GET /users/4 可以查询到 id 为 4 的用户信息,通过 GET /users/123 可以查询到 id 为 123 的用户信息,问题是数据库中并没有后者的数据,此时应该告诉前端没有这项数据。本篇将会讲解错误处理相关的内容,这将涉及到 NestJS 中的异常过滤器部分。

欢迎加入技术交流群

image.png

  1. NestJS 🧑‍🍳  厨子必修课(一):后端的本质
  2. NestJS 🧑‍🍳 厨子必修课(二):项目创建
  3. NestJS 🧑‍🍳 厨子必修课(三):控制器
  4. NestJS 🧑‍🍳 厨子必修课(四):服务类
  5. NestJS 🧑‍🍳 厨子必修课(五):Prisma 集成(上)
  6. NestJS 🧑‍🍳 厨子必修课(六):Prisma 集成(下)
  7. NestJS 🧑‍🍳 厨子必修课(七):管道
  8. NestJS 🧑‍🍳 厨子必修课(八):异常过滤器

2. NotFoundException

通过 NotFoundException 实例直接抛出错误:

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}
  
  async findOne(id: number) {
	  const user = await this.prisma.user.findUnique({
	    where: { id },
	    // 关联
	    include: {
	      orders: true,
	    },
	  });
	  if (!user) {
	    throw new NotFoundException('用户不存在');
	  }
	  return user;
	}
}

这里做了防御性编程,如果没有查询到用户,使用 throw new NotFoundException('用户不存在'); 抛出错误。

访问 http://localhost:3000/users/123 后:

image.png

返回的 JSON 对象由以下字段组成:

  • message:描述具体错误信息,通常是一个字符串,解释错误的原因或背景。
  • error:标识发生错误的类型或类别。通常为错误的名称或类别(如 “Bad Request” 或 “Unauthorized”)。
  • statusCode:HTTP 状态码,以数字形式表示请求失败的类型。常见的状态码包括 400(Bad Request)、401(Unauthorized)、404(Not Found)等,用于反映请求的结果状态。

除了 NotFoundException,NestJS 中还提供了以下内置的 HTTP 异常:

  1. BadRequestException (400 Bad Request)

表示客户端发送的请求有错误,服务器无法处理。例如,参数缺失、无效输入等情况。

  1. UnauthorizedException (401 Unauthorized)

表示请求没有经过身份验证或认证失败。通常出现在访问需要身份验证的资源时,客户端没有提供有效的认证信息。

  1. ForbiddenException (403 Forbidden)

表示客户端的身份认证成功,但没有权限访问请求的资源。通常发生在权限不足时。

  1. NotAcceptableException (406 Not Acceptable)

表示服务器无法生成客户端可以接受的响应格式。通常出现在客户端的 Accept 头与服务器支持的响应格式不匹配时。

  1. RequestTimeoutException (408 Request Timeout)

表示客户端请求超时。服务器在等待请求时超过了设定的时间限制。

  1. ConflictException (409 Conflict)

表示请求与服务器的当前状态存在冲突。例如,创建资源时,资源已经存在。

  1. GoneException (410 Gone)

表示请求的资源已经永久不可用,并且不会再恢复。常用于删除的资源。

  1. HttpVersionNotSupportedException (505 HTTP Version Not Supported)

表示服务器不支持客户端请求的 HTTP 协议版本。

  1. PayloadTooLargeException (413 Payload Too Large)

表示客户端发送的数据体(如上传文件)超过了服务器允许的大小限制。

  1. UnsupportedMediaTypeException (415 Unsupported Media Type)

表示客户端请求的媒体类型不被服务器支持。例如,上传文件的格式不被接受。

  1. UnprocessableEntityException (422 Unprocessable Entity)

表示服务器理解客户端的请求内容,但由于语义错误而无法处理。例如,输入数据的格式正确但内容无效。

  1. InternalServerErrorException (500 Internal Server Error)

表示服务器在处理请求时遇到了内部错误。是一个通用的错误状态码,表示服务器无法处理请求。

  1. NotImplementedException (501 Not Implemented)

表示服务器不支持请求的方法。通常用于服务器不支持客户端请求的功能时。

  1. ImATeapotException (418 I’m a teapot)

这是一个愚人节玩笑性质的 HTTP 状态码,表示服务器拒绝酿茶的请求(参见 IETF RFC 2324)。常用于测试或幽默场景。

  1. MethodNotAllowedException (405 Method Not Allowed)

表示请求的方法(如 GET、POST)在目标资源中不可用。例如,资源只支持 GET 请求,而客户端使用了 POST。

  1. BadGatewayException (502 Bad Gateway)

表示服务器作为网关或代理时,从上游服务器接收到无效响应。

  1. ServiceUnavailableException (503 Service Unavailable)

表示服务器暂时无法处理请求,通常是因为过载或维护。

  1. GatewayTimeoutException (504 Gateway Timeout)

表示服务器作为网关或代理时,从上游服务器接收响应超时。

  1. PreconditionFailedException (412 Precondition Failed)

表示客户端发送的请求没有满足服务器的某些前置条件。例如,If-Match 头字段的条件不满足。

以上各种类型都可以从 @nestjs/common 包中导出使用,例如 BadRequestException

// app.controller.ts
import { BadRequestException } from '@nestjs/common';

@Controller()
export class UsersController {

  @Get('test-error')
  testError() {
    throw new BadRequestException('测试错误');
  }
 
}

image 1.png

3. 自定义 HTTP 异常过滤器

3.1 方法1:实现 ExceptionFilter 接口

自定义的异常过滤器需要实现 ExceptionFilter 接口的 catch 方法:

// src/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, 
      }); 
  } 
}

在上面的示例中通过 @Catch() 装饰器**指定要捕获的异常类型(**只捕获 HttpException)。

@Catch() 装饰器也支持可以采用单个参数或逗号分隔的列表,这可以同时为多种类型的异常设置过滤器

如果没有在 @Catch() 中指定异常类型,过滤器将捕获所有异常类型。这对于全局异常处理非常有用。

3.2 方法2:继承 HttpException

自定义的 CustomHttpException 类继承 HttpException 类:

// src/custom-http-exception.filter.ts
import { HttpException } from '@nestjs/common';

export class CustomHttpException extends HttpException { 
  constructor(message: string, statusCode: number) {
    super(message, statusCode);
  } 
}

使用 CustomHttpException 类实例抛出异常:

// app.controller.ts

@Get('custom')
getCustom(){
  throw new CustomHttpException('后山乃禁地!', HttpStatus.FORBIDDEN) 
}

image 2.png

4. 绑定范围

过滤器可以绑定的范围有:

  • HTTP 方法
  • 控制器
  • 全局范围

4.1 HTTP 方法

使用 @UseFilters() 装饰器可以使得过滤器只在指定 HTTP 方法下起作用:

// users.controller.ts
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get('custom')
  @UseFilters(new HttpExceptionFilter())
  // @UseFilters(HttpExceptionFilter)
  getCustom() {
    throw new HttpException(
      {
        status: HttpStatus.FORBIDDEN,
        message: '后山乃禁地!请速速离去!',
        error: 'Forbidden',
      },
      403,
    );
  }
}

装饰器中的参数既可以传递一个过滤器的实例(new HttpExceptionFilter()),也可以传递一个类(HttpExceptionFilter)。

image 3.png

4.2 控制器

也可以在指定的控制器下使用 @UseFilters() 装饰器:

@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
  // ...
}

这样就不用给该控制器下的所有 HTTP 方法都添加装饰器了。

4.3 全局应用

使用 app.useGlobalFilters() 方法来注册全局异常过滤器:

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

bootstrap();

设置完成后,应用的所有控制器和方法都加上了过滤器,这样应用的任何地方抛出的异常都会被全局捕获,可以确保一致性和可控的错误响应格式。

但是这种方式并不能灵活地与依赖注入的特性相结合,NestJS 提供了另一种全局绑定过滤器的方式:

// app.module.ts

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

5. 日志记录与异常过滤器

5.1 记录异常日志(Logger

通过 NestJS 的 Logger 服务记录异常详细信息,以便在应用崩溃时追踪问题。

// http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse<Response>(); // 获取响应对象
    const request = ctx.getRequest<Request>(); // 获取请求对象
    const status = exception.getStatus(); // 获取异常状态码

    const message = exception.message
      ? exception.message
      : `${status >= 500 ? '服务器错误(Service Error)' : '客户端错误(Client Error)'}`;

    const nowTime = new Date().getTime();

    const errorResponse = {
      data: {},
      message,
      code: -1,
      date: nowTime,
      path: request.url,
    };

    // 记录日志到控制台
    this.logger.error(
      `【${nowTime}${status} ${request.method} ${request.url} query:${JSON.stringify(request.query)} params:${JSON.stringify(
        request.params,
      )} body:${JSON.stringify(request.body)}`,
      JSON.stringify(errorResponse),
      HttpExceptionFilter.name,
    );

    response.status(status).json(errorResponse);
  }
}
// users.controller.ts
@Get('custom')
@UseFilters(new HttpExceptionFilter(new Logger('UsersController')))
getCustom() {
  throw new HttpException(
    {
      status: HttpStatus.FORBIDDEN,
      message: '后山乃禁地!请速速离去!',
      error: 'Forbidden',
    },
    403,
  );
}

image 4.png

或是依赖注入现有日志服务(nestjs-pino):

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private readonly logService: LogService) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    this.logService.logError(exception);
    // 处理异常
  }
}

5.2 将错误信息发送到外部监控服务(如 Sentry)

集成外部监控服务,如 Sentry,来捕获和分析错误日志:

import * as Sentry from '@sentry/node';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    Sentry.captureException(exception);
    // 处理异常
  }
}

6. 总结

异常过滤器是 NestJS 提供的强大工具,用于捕获和处理应用中的异常。通过自定义和全局应用过滤器,开发者可以确保异常处理的一致性,并提升应用的健壮性和用户体验。在复杂项目中,不同模块和服务可以使用不同的过滤器来处理特定场景下的异常。