Nest.js 异常过滤器
Nest 带有一个内置的异常层,负责处理应用程序中所有未处理的异常。 当我们的应用程序代码未处理异常时,该层将捕获该异常,然后自动发送适当的用户友好响应。
开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理 HttpException 类型的异常(及其子类)。 当异常无法识别(既不是 HttpException 也不是继承自 HttpException 的类)时,内置异常过滤器会生成以下默认 JSON 响应:
{
"statusCode": 500,
"message": "Internal server error"
}
提示
:全局异常过滤器部分支持http-errors
库。 基本上,任何包含 statusCode 和 message 属性的抛出异常都将被正确填充并作为响应发送回客户端(而不是默认的 InternalServerErrorException 用于无法识别的异常)。
抛出标准异常
Nest 提供了一个内置的 HttpException
类,是 @nestjs/common
包中的一个公共类。 对于典型的基于 HTTP REST/GraphQL API 的应用程序,最好在发生某些错误情况时发送标准 HTTP 响应对象。
例如,在 CatsController 中,我们有一个 findAll() 方法(一个 GET 路由处理程序)。 让我们假设这个路由处理程序由于某种原因抛出了一个异常。 为了证明这一点,我们将对其进行硬编码,如下所示:
cats.controller.ts
@Get() async findAll() { throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); }
提示
: 我们在这里使用了 HttpStatus。 这是从 @nestjs/common 包导入的。
当客户端调用此端点时,响应如下所示:
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException 构造函数接受两个确定响应的必需参数:
- response : 参数定义 JSON 响应正文。 它可以是字符串或对象,如下所述。
- status : 参数定义 HTTP 状态代码。
默认情况下,JSON response
正文包含两个属性:
- statusCode :默认为 status 参数中提供的 HTTP 状态码
- message :基于状态的 HTTP 错误的简短描述
要仅覆盖 JSON 响应正文的消息部分,请在 response 参数中提供一个字符串。 要覆盖整个 JSON 响应正文,需要在 response 参数中传递一个对象。 Nest 将序列化对象并将其作为 JSON 响应正文返回。
第二个构造函数参数 - status - 应该是一个有效的 HTTP 状态代码。 最佳实践是使用从 @nestjs/common 包导入的 HttpStatus 进行枚举。
这是一个覆盖整个 response 正文的示例:
cats.controller.ts
@Get() async findAll() { throw new HttpException({ status: HttpStatus.FORBIDDEN, error: 'This is a custom message', }, HttpStatus.FORBIDDEN); }
使用上述内容,这就是响应的内容:
{
"status": 403,
"error": "This is a custom message"
}
自定义异常
在很多情况下,我们不需要编写自定义异常,并且可以使用内置的 Nest HTTP 异常,如下一节所述。 如果确实需要创建自定义异常,最好创建自己的异常层次结构,其中自定义异常继承自基类 HttpException
。 通过这种方法,Nest 将识别我们的异常,并自动处理错误响应。 让我们实现这样一个自定义异常:
forbidden.exception.ts
export class ForbiddenException extends HttpException { constructor() { super('Forbidden', HttpStatus.FORBIDDEN); } }
由于 ForbiddenException
继承了基类 HttpException,它将与内置的异常处理程序无缝协作,因此我们可以在 findAll()
方法中使用它。
cats.controller.ts
@Get() async findAll() { throw new ForbiddenException(); }
内置的 HTTP 异常
Nest 提供了一组从基础 HttpException 继承的标准异常。 这些异常都是 @nestjs/common 包中的公共类,代表了许多最常见的 HTTP 异常:
- 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 对象直接控制发送的响应,使用 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, }); } }
提示
: 所有异常过滤器都应该实现通用的 ExceptionFilter<T> 接口。 这要求我们提供带有指定签名的catch(exception: T, host: ArgumentsHost)
方法。 T 表示异常的类型。
@Catch(HttpException)
装饰器将所需的元数据绑定到异常过滤器,告诉 Nest 这个特定的过滤器正在寻找 HttpException 类型的异常,而不是别的。 @Catch()
装饰器可以采用单个参数或逗号分隔的列表。 这使我们可以同时为多种类型的异常设置过滤器。
让我们看一下 catch()
方法的参数。 exception 参数是当前正在处理的异常对象。 host
参数是一个 ArgumentsHost
对象。 ArgumentsHost 是一个功能强大的实用程序对象,我们将在执行上下文一章中进一步研究它。在此代码示例中,我们使用它来获取对传递给原始请求处理程序(在异常产生的控制器中)的 request
和 response
对象的引用。并且我们在 ArgumentsHost 上使用了一些辅助方法来获取所需的 Request 和 Response 对象。
这种抽象级别的原因是 ArgumentsHost 在所有上下文中都起作用(例如,我们现在使用的 HTTP 服务器上下文,还有微服务和 WebSockets)。在执行上下文一章中,我们将看到如何利用 ArgumentsHost 及其辅助函数的强大功能访问任何执行上下文的适当底层参数。这将允许我们编写跨所有上下文操作的通用异常过滤器。
绑定过滤器
让我们将新的 HttpExceptionFilter
绑定到 CatsController 的 create()
方法。
cats.controller.ts
@Post() @UseFilters(new HttpExceptionFilter()) async create(@Body() createCatDto: CreateCatDto) { throw new ForbiddenException(); }
提示
: @UseFilters() 装饰器是从 @nestjs/common 包中导入的。
我们在这里使用了 @UseFilters()
装饰器。 与 @Catch()
装饰器类似,它可以采用单个过滤器实例,或以逗号分隔的过滤器实例列表。 在这里,我们创建了 HttpExceptionFilter 的实例。 或者,可以传递类(而不是实例),将实例化的责任留给框架,并启用依赖注入。
cats.controller.ts
@Post() @UseFilters(HttpExceptionFilter) async create(@Body() createCatDto: CreateCatDto) { throw new ForbiddenException(); }
提示
:尽可能使用类而不是实例来应用过滤器。 它减少了内存使用,因为 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();
警告
: 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 {}
当使用这种方法为过滤器执行依赖注入时,请注意,无论使用此构造的模型是什么情况,过滤器实际上都是全局的。 这应该在哪里完成? 选择定义过滤器(上例中的 HttpExceptionFilter)的模型。 此外,useClass 不是处理自定义提供程序注册的唯一方法。
我们可以根据需要使用此技术添加任意数量的过滤器; 只需将每个过滤器添加到提供程序数组。
捕获任意异常
为了捕获每个未处理的异常(无论异常类型如何),请将 @Catch()
装饰器的参数列表留空,例如 **@Catch()**。
在下面的示例中,我们有一个与平台无关的代码,因为它使用 HTTP 适配器来传递响应,并且不直接使用任何特定于平台的对象( request 和 response ):
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 {
// In certain situations `httpAdapter` might not be available in the
// constructor method, thus we should resolve it here.
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
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); } }
警告
: 扩展 BaseExceptionFilter 的方法范围和控制器范围的过滤器不应使用 new 实例化。 相反,让框架自动实例化它们。
上面的实现只是一个演示该方法的外壳。 我们对继承异常过滤器的实现将包括定制的业务逻辑(例如,处理各种条件)。
全局过滤器可以继承基本过滤器。 这可以通过两种方式中的任何一种来完成。
第一种方法是在实例化自定义全局过滤器时注入 HttpAdapter 引用:
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
令牌。