企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 异常过滤器 Nest 带有一个内置的**异常层**,负责处理应用程序中所有未处理的异常。当您的应用程序代码未处理异常时,该层将捕获该异常,然后自动发送适当的用户友好响应。 ![](https://docs.nestjs.com/assets/Filter_1.png) 开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理类型 `HttpException`(及其子类)的异常。每个发生的异常都由全局异常过滤器处理, 当这个异常**无法被识别**时 (既不是 `HttpException` 也不是继承的类 `HttpException` ) , 用户将收到以下 `JSON` 响应: ```json { "statusCode": 500, "message": "Internal server error" } ``` > **提示**:全局异常过滤器部分支持该`http-errors`库。基本上,任何包含`statusCode`and`message`属性的抛出异常都将被正确填充并作为响应发送回(而不是默认`InternalServerErrorException`的无法识别的异常)。 > ## 基础异常类 `Nest`提供了一个内置的 `HttpException` 类,它从 `@nestjs/common` 包中导入。对于典型的基于`HTTP` `REST/GraphQL` `API`的应用程序,最佳实践是在发生某些错误情况时发送标准HTTP响应对象。 在 `CatsController`,我们有一个 `findAll()` 方法(`GET` 路由)。假设此路由处理程序由于某种原因引发异常。 为了说明这一点,我们将对其进行如下硬编码: > cats.controller.ts ```typescript @Get() async findAll() { throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); } ``` > 我们在这里使用了 `HttpStatus` 。它是从 `@nestjs/common` 包导入的辅助枚举器。 现在当客户端调用这个端点时,响应如下所示: ```json { "statusCode": 403, "message": "Forbidden" } ``` `HttpException` 构造函数有两个必要的参数来决定响应: - `response` 参数定义 `JSON` 响应体。它可以是 `string` 或 `object`,如下所述。 - `status`参数定义`HTTP`[状态代码](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)。 默认情况下,`JSON` 响应主体包含两个属性: - `statusCode`:默认为 `status` 参数中提供的 `HTTP` 状态代码 - `message`:基于状态的 `HTTP` 错误的简短描述 仅覆盖 `JSON` 响应主体的消息部分,请在 `response`参数中提供一个 `string`。 要覆盖整个 `JSON` 响应主体,请在`response` 参数中传递一个`object`。 `Nest`将序列化对象,并将其作为`JSON` 响应返回。 第二个构造函数参数-`status`-是有效的 `HTTP` 状态代码。 最佳实践是使用从`@nestjs/common`导入的 `HttpStatus`枚举。 这是一个覆盖整个响应正文的示例: > cats.controller.ts ```typescript @Get() async findAll() { throw new HttpException({ status: HttpStatus.FORBIDDEN, error: 'This is a custom message', }, HttpStatus.FORBIDDEN); } ``` 使用上面的代码,响应如下所示: ```json { "status": 403, "error": "This is a custom message" } ``` ## 自定义异常 在许多情况下,您无需编写自定义异常,而可以使用内置的 `Nest HTTP`异常,如下一节所述。 如果确实需要创建自定义的异常,则最好创建自己的**异常层次结构**,其中自定义异常继承自 `HttpException` 基类。 使用这种方法,`Nest`可以识别您的异常,并自动处理错误响应。 让我们实现这样一个自定义异常: > forbidden.exception.ts ```typescript export class ForbiddenException extends HttpException { constructor() { super('Forbidden', HttpStatus.FORBIDDEN); } } ``` 由于 `ForbiddenException` 扩展了基础 `HttpException`,它将和核心异常处理程序一起工作,因此我们可以在 `findAll()`方法中使用它。 > cats.controller.ts ```typescript @Get() async findAll() { throw new ForbiddenException(); } ``` ## 内置HTTP异常 为了减少样板代码,Nest 提供了一系列继承自核心异常 `HttpException` 的可用异常。所有这些都可以在 `@nestjs/common`包中找到: * `BadRequestException` * `UnauthorizedException` * `NotFoundException` * `ForbiddenException` * `NotAcceptableException` * `RequestTimeoutException` * `ConflictException` * `GoneException` * `HttpVersionNotSupportedException` * `PayloadTooLargeException` * `UnsupportedMediaTypeException` * `UnprocessableEntityException` * `InternalServerErrorException` * `NotImplementedException` * `ImATeapotException` * `MethodNotAllowedException` * `BadGatewayException` * `ServiceUnavailableException` * `GatewayTimeoutException` * `PreconditionFailedException` ## 异常过滤器 虽然基本(内置)异常过滤器可以为您自动处理许多情况,但有时您可能希望对异常层拥有**完全控制权**,例如,您可能希望基于某些动态因素添加日志记录或使用不同的 `JSON` 模式。 **异常过滤器**正是为此目的而设计的。 它们使您可以控制精确的控制流以及将响应的内容发送回客户端。 让我们创建一个异常过滤器,它负责捕获作为`HttpException`类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 `Request`和 `Response`。我们将访问`Request`对象,以便提取原始 `url`并将其包含在日志信息中。我们将使用 `Response.json()`方法,使用 `Response`对象直接控制发送的响应。 > http-exception.filter.ts ```typescript 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: ArgumentsHost)`方法。`T` 表示异常的类型。 `@Catch()` 装饰器绑定所需的元数据到异常过滤器上。它告诉 `Nest`这个特定的过滤器正在寻找 `HttpException` 而不是其他的。在实践中,`@Catch()` 可以传递多个参数,所以你可以通过逗号分隔来为多个类型的异常设置过滤器。 ## 参数主机 让我们看一下该 `catch()` 方法的参数。该 `exception` 参数是当前正在处理的异常对象。该host参数是一个 `ArgumentsHost` 对象。 `ArgumentsHost` 是一个功能强大的实用程序对象,我们将在[应用上下文章节](https://docs.nestjs.com/fundamentals/execution-context) *中进一步进行研究。在此代码示例中,我们使用它来获取对 `Request` 和 `Response` 对象的引用,这些对象被传递给原始请求处理程序(在异常发生的控制器中)。在此代码示例中,我们使用了一些辅助方法 `ArgumentsHost` 来获取所需的 `Request` 和 `Response` 对象。`ArgumentsHost` 在[此处](https://docs.nestjs.com/fundamentals/execution-context)了解更多信息。 之所以如此抽象,是因为它 `ArgumentsHost` 可以在所有上下文中使用(例如,我们现在正在使用的 `HTTP` 服务器上下文,以及微服务和 `WebSocket` )。在应用上下文章节中,我们将看到如何使用 `ArgumentsHost` 及其辅助函数访问任何应用上下文中相应的底层参数。这将使我们能够编写可在所有上下文中运行的通用异常过滤器。 ## 绑定过滤器 让我们将 `HttpExceptionFilter` 绑定到 `CatsController` 的 `create()` 方法上。 > cats.controller.ts ```typescript @Post() @UseFilters(new HttpExceptionFilter()) async create(@Body() createCatDto: CreateCatDto) { throw new ForbiddenException(); } ``` > `@UseFilters()` 装饰器需要从 `@nestjs/common` 包导入。 我们在这里使用了 `@UseFilters()` 装饰器。和 `@Catch()`装饰器类似,它可以使用单个过滤器实例,也可以使用逗号分隔的过滤器实例列表。 我们创建了 `HttpExceptionFilter` 的实例。另一种可用的方式是传递类(不是实例),让框架承担实例化责任并启用依赖注入。 > cats.controller.ts ```typescript @Post() @UseFilters(HttpExceptionFilter) async create(@Body() createCatDto: CreateCatDto) { throw new ForbiddenException(); } ``` > 尽可能使用类而不是实例。由于 `Nest` 可以轻松地在整个模块中重复使用同一类的实例,因此可以减少**内存使用**。 在上面的示例中,`HttpExceptionFilter` 仅应用于单个 `create()` 路由处理程序,使其成为方法范围的。 异常过滤器的作用域可以划分为不同的级别:方法范围,控制器范围或全局范围。 例如,要将过滤器设置为控制器作用域,您可以执行以下操作: > cats.controller.ts ```typescript @UseFilters(new HttpExceptionFilter()) export class CatsController {} ``` 此结构为 `CatsController` 中的每个路由处理程序设置 `HttpExceptionFilter`。 要创建一个全局范围的过滤器,您需要执行以下操作: > main.ts ```typescript async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(3000); } bootstrap(); ``` > 该 `useGlobalFilters()` 方法不会为网关和混合应用程序设置过滤器。 全局过滤器用于整个应用程序、每个控制器和每个路由处理程序。就依赖注入而言,从任何模块外部注册的全局过滤器(使用上面示例中的 `useGlobalFilters()`)不能注入依赖,因为它们不属于任何模块。为了解决这个问题,你可以注册一个全局范围的过滤器直接为任何模块设置过滤器: > app.module.ts ```typescript import { Module } from '@nestjs/common'; import { APP_FILTER } from '@nestjs/core'; @Module({ providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter, }, ], }) export class AppModule {} ``` >当使用此方法对过滤器执行依赖注入时,请注意,无论采用哪种结构的模块,过滤器实际上都是全局的。 应该在哪里做? 选择定义了过滤器(以上示例中为 `HttpExceptionFilter`)的模块。 同样,`useClass`不是处理自定义提供程序注册的唯一方法。 在[这里](https://docs.nestjs.com/fundamentals/custom-providers)了解更多。 您可以根据需要添加任意数量的过滤器;只需将每个组件添加到 `providers`(提供者)数组。 ## 捕获异常 为了捕获每一个未处理的异常(不管异常类型如何),将 `@Catch()` 装饰器的参数列表设为空,例如 `@Catch()`。 在下面的示例中,我们有一个与平台无关的代码,因为它使用[HTTP 适配器](https://docs.nestjs.com/faq/http-adapter)来传递响应,并且不直接使用任何特定于平台的对象(`Request`和`Response`): > any-exception.filter.ts ~~~typescript 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()` 方法。 >all-exceptions.filter.ts ```typescript import { Catch, ArgumentsHost } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; @Catch() export class AllExceptionsFilter extends BaseExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { super.catch(exception, host); } } ``` > 继承自基础类的过滤器必须由框架本身实例化(不要使用 `new` 关键字手动创建实例) 上面的实现只是一个演示。扩展异常过滤器的实现将包括定制的业务逻辑(例如,处理各种情况)。 全局过滤器可以扩展基本过滤器。这可以通过两种方式来实现。 第一种方法是`HttpAdapter`在实例化自定义全局过滤器时注入引用: ```typescript 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` `token`,[如下所示](#绑定过滤器)。