跳到主要内容

异常过滤器

Nest带有一个内置的异常层负责处理应用程序中所有未处理的异常当应用程序代码未处理异常时,该层会捕获异常,然后自动发送适当的用户友好响应。

Nest.js 异常过滤器

开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理HttpException类型(及其子类)的异常。 当异常无法识别时,(既不是HttpException也不是继承自HttpException的类),内置异常过滤器会生成 以下默认JSON响应:

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

全局异常过滤器部分支持http-errors库。基本上,任何包含statusCodemessage属性的抛出异常都将 被正确填充并作为响应发回(而不是针对无法识别的异常的默认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构造函数采用两个必需参数来确定响应

  • 响应参数定义JSON响应正文。它可以是stringobject,如下所述。
  • status参数定义HTTP状态代码

默认情况下,JSON响应正文包含两个属性:

  • statusCode: 默认为状态参数中提供的HTTP状态码
  • message: 基于状态的HTTP错误的简短描述

要仅覆盖JSON响应正文的消息部分,请在response参数中提供一个字符串。 要覆盖整个JSON响应正文,请在响应参数中传递一个对象。 Nest将序列化该对象并将其作为JSON响应正文返回

第二个构造函数参数status应该是有效的HTTP状态码。 最佳实践是使用从@nestjs/common导入的HttpStatus枚举。

第三个构造函数参数(可选)options可用于提供错误原因。 这个cause对象不会序列化到响应对象中,但它对日志记录很有用,可以提供有关导致HttpException被 抛出的内部错误的有价值信息。

这是一个覆盖整个响应正文并提供错误原因的示例:

cats.controller.ts
@Get()
async findAll() {
try {
await this.service.findAll()
} catch (error) {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN, {
cause: error
})
}
}

使用上面的内容,响应将如下所示:

{
"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

所有内置异常还可以使用options参数提供错误cause和错误描述。

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

使用上面的内容,响应将如下所示:

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

异常过滤器

虽然基本(内置)异常过滤器可以自动为您处理许多情况,但您可能希望完全控制异常层。 例如,您可能想要添加日志记录或根据某些动态因素使用不同的JSON架构。 异常过滤器正是为此目的而设计的。 它们使您可以控制确切的控制流程以及发送回客户端的响应内容。

让我们创建一个异常过滤器,负责捕获作为HttpException类实例的异常,并为它们实现自定义逻辑响应逻辑。 为此,我们需要访问底层平台的RequestResponse对象。我们将访问Request对象,以便提取原始url 并将其包含在日志信息中。我们将使用Response对象通过response.json()方法直接控制发送的响应。

http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } '@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表示异常的类型

注意

如果您使用@nestjs/platform-fastity,您可以使用response.send()而不是response.json(). 不要忘记从fastify导入正确的类型。

@Catch(HttpException)装饰器将所需的元数据绑定到异常过滤器,告诉Nest这个特定的过滤器 正在寻找HttpException类型的异常,而不是其他任何东西。 @Catch()装饰器可以采用单个参数或逗号分隔的列表。这使得您可以同时为多种类型的异常设置过滤器。

参数主机

我们来看看catch()方法的参数。exception参数时当前正在处理的异常对象。 host参数是一个ArgumentsHost对象。ArgumentsHost是一个功能强大的实用程序对象, 我们将在执行上下文章节中进一步研究它。在此代码示例中,我们使用它来获取对传递给原始请求处理程序 (在引发异常的控制器中)的RequestResponse对象的引用。 在此代码示例中, 我们在ArgumentsHost上使用了一些辅助方法来获取所需的RequestResponse对象。

*这种抽象级别的原因是ArgumentsHost在所有上下文中都起作用(例如,我们现在正在使用的HTTP服务器 上下文,还有微服务和WebSocket)。在执行上下文一章中,我们将了解如何利用ArgumentsHost及其辅助函数 的功能来访问任何执行上下文的适当底层参数。这将使我们能够编写跨所有上下文运行的通用异常过滤器。

绑定过滤器

让我们将新的HttpExceptionFilter绑定到CatsControllercreate()方法。

cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
提示

@UseFilter()装饰器是从@nestjs/common包导入的。

我们在这里使用@UseFilter()装饰器。与@Catch()装饰器类似,它可以采用单个过滤器实例, 或以逗号分隔的过滤器实例列表。在这里,我们就地创建了HttpExceptionFilter的实例。 或者,您可以传递类(而不是实例),将实例化的责任留给框架,并启动依赖项注入

cats.controller.ts
@Post()
@UseFilter(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
提示

尽可能使用类而不是实例来应用过滤器。它减少了内存使用量, 因为Nest可以轻松地在整个模块中重用同一类的实例

在上面的示例中,HttpExceptionFilter仅应用于单个create()路由处理程序,使其具有方法范围。 异常过滤器的范围可以在不同级别

  • 方法范围(method-scoped):控制器(controller)/解析器(resolver)/网关(gateway)
  • 控制器范围(controller-scoped)
  • 全局范围(global-scoped)。

例如,要将过滤器设置为控制器范围(controller-scoped),您可以执行以下操作:

cats.controller.ts
@UseFilter(new HttpExceptionFilter())
export class CatsController {}

此构造为CatsController内定义的每个路由处理程序设置HttpExceptionFilter

要创建全局范围的过滤器(global-scoped),您需要执行以下操作:

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: [
{
provider: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
提示

在使用这种方法为过滤器执行依赖注入时,请注意,无论在哪个模块中使用此构造方式,过滤器实际上是全局的。 这应该在哪里完成?选择定义过滤器(在上面的例子中为HttpExceptionFilter)的模块。 另外,useClass并不是处理自定义提供者注册的唯一方式。

您可以根据需要使用此技术添加任意数量的过滤器;只需将每个添加到提供者数组中。

捕获一切

为了捕获每个未处理的异常(无论异常类型如何), 请将@Catch()装饰器的参数列表留空,例如@Catch()

在下面的示例中,我们有一个与平台无关的代码,因为它使用HTTP适配器来传递响应,并且不直接使用任何 特定于平台的对象(ResponseRequest)。

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 {
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 令牌, 如此处所示