Nest 内置了一个异常处理层 ,负责处理应用程序中所有未捕获的异常。当应用程序代码未处理某个异常时,该层会捕获它并自动返回用户友好的响应。
默认情况下,这个功能由内置的全局异常过滤器实现,它能处理 HttpException
类型(及其子类)的异常。当遇到无法识别的异常(既不是 HttpException
也不是其继承类)时,内置异常过滤器会生成以下默认 JSON 响应:
info 注意 全局异常过滤器部分支持
http-errors
库。基本上,任何包含statusCode
和message
属性的异常都会被正确解析并作为响应返回(而不是对无法识别的异常默认返回InternalServerErrorException
)。
Nest 提供了一个内置的 HttpException
类,该类从 @nestjs/common
包中导出。对于基于 HTTP REST/GraphQL API 的典型应用程序,最佳实践是在发生某些错误条件时发送标准的 HTTP 响应对象。
例如,在 CatsController
中,我们有一个 findAll()
方法(一个 GET
路由处理程序)。假设这个路由处理程序由于某种原因抛出了异常。为了演示这一点,我们将其硬编码如下:
info 提示 我们在这里使用了
HttpStatus
。这是一个从@nestjs/common
包导入的辅助枚举。
当客户端调用此端点时,响应如下所示:
HttpException
构造函数接收两个必选参数来决定响应内容:
response
参数定义了 JSON 响应体,可以是如下所述的 string
或 object
类型。status
参数定义了 HTTP 状态码默认情况下,JSON 响应体包含两个属性:
statusCode
:默认为 status
参数中提供的 HTTP 状态码message
:基于 status
的 HTTP 错误简短描述若要仅覆盖 JSON 响应体中的消息部分,请在 response
参数中传入字符串。若要覆盖整个 JSON 响应体,则在 response
参数中传入对象。Nest 会将该对象序列化后作为 JSON 响应体返回。
第二个构造参数 status
应为有效的 HTTP 状态码。最佳实践是使用从 @nestjs/common
导入的 HttpStatus
枚举。
还存在第三个构造参数(可选)——options
,可用于提供错误原因 。该 cause
对象不会被序列化到响应对象中,但对日志记录很有帮助,能提供引发 HttpException
的内部错误的有价值信息。
以下是覆盖整个响应体并提供错误原因的示例:
基于上述情况,响应将呈现如下形式:
默认情况下,异常过滤器不会记录内置异常如 HttpException
(以及从其继承的任何异常)。当这些异常被抛出时,它们不会出现在控制台中,因为它们被视为正常应用流程的一部分。相同行为也适用于其他内置异常,例如 WsException
和 RpcException
。
这些异常都继承自基础类 IntrinsicException
,该类从 @nestjs/common
包导出。这个类有助于区分属于正常应用操作的异常与不属于该范畴的异常。
若需记录这些异常,可创建自定义异常过滤器。我们将在下一节说明具体实现方法。
多数情况下,您无需编写自定义异常,直接使用内置的 Nest HTTP 异常即可(详见下一节)。如需创建定制化异常,最佳实践是建立异常层级结构 ,让自定义异常继承基础 HttpException
类。通过这种方式,Nest 能识别您的异常并自动处理错误响应。下面我们实现一个自定义异常:
由于 ForbiddenException
继承自基础 HttpException
,它能与内置异常处理器无缝协作,因此我们可在 findAll()
方法中直接使用。
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
和错误描述:
使用上述方式时,响应将如下所示:
虽然基础的(内置)异常过滤器能自动处理许多情况,但您可能希望对异常层进行完全控制 。例如,您可能希望基于某些动态因素添加日志记录或使用不同的 JSON 模式。 异常过滤器正是为此目的而设计。它们让您可以精确控制流程以及返回给客户端的响应内容。
让我们创建一个异常过滤器,负责捕获 HttpException
类的实例异常,并为它们实现自定义响应逻辑。为此,我们需要访问底层平台的 Request
和 Response
对象。我们将访问 Request
对象以提取原始 url
并将其包含在日志信息中。我们将使用 Response
对象通过 response.json()
方法直接控制发送的响应。
info 提示 所有异常过滤器都应实现泛型接口
ExceptionFilter<T>
。这要求您提供带有指定签名的catch(exception: T, host: ArgumentsHost)
方法。T
表示异常的类型。
warning 警告 如果您使用
@nestjs/platform-fastify
,可以用response.send()
替代response.json()
。别忘了从fastify
导入正确的类型。
@Catch(HttpException)
装饰器将所需的元数据绑定到异常过滤器,告诉 Nest 这个特定过滤器只查找 HttpException
类型的异常。@Catch()
装饰器可接受单个参数或以逗号分隔的列表,让您能一次性为多种异常类型设置过滤器。
让我们看看 catch()
方法的参数。exception
参数是当前正在处理的异常对象。host
参数是一个 ArgumentsHost
对象。ArgumentsHost
是一个强大的工具对象,我们将在执行上下文章节 *中进一步研究。在此代码示例中,我们使用它来获取对原始请求处理程序(异常发生的控制器)中传递的 Request
和 Response
对象的引用。本示例中,我们使用了 ArgumentsHost
上的一些辅助方法来获取所需的 Request
和 Response
对象。了解更多关于 ArgumentsHost
的信息请点击这里 。
采用这种抽象层级的原因是 ArgumentsHost
能在所有上下文中运作(例如我们当前使用的 HTTP 服务器上下文,还包括微服务和 WebSockets)。在执行上下文章节中,我们将看到如何利用 ArgumentsHost
及其辅助函数的能力,为任何执行上下文获取对应的底层参数 。这将使我们能够编写适用于所有上下文的通用异常过滤器。
让我们将新的 HttpExceptionFilter
绑定到 CatsController
的 create()
方法上。
info 注意
@UseFilters()
装饰器是从@nestjs/common
包中导入的。
我们在此使用了 @UseFilters()
装饰器。与 @Catch()
装饰器类似,它可以接收单个过滤器实例或以逗号分隔的过滤器实例列表。这里我们直接创建了 HttpExceptionFilter
的实例。或者,你也可以传入类(而非实例),将实例化的责任交给框架,并启用依赖注入 。
info 提示 尽可能通过类而非实例来应用过滤器。这会降低内存消耗 ,因为 Nest 可以在整个模块中轻松复用相同类的实例。
在上面的示例中,HttpExceptionFilter
仅应用于单个 create()
路由处理器,使其成为方法作用域的。异常过滤器可以具有不同的作用域级别:控制器/解析器/网关的方法作用域、控制器作用域或全局作用域。例如,要将过滤器设置为控制器作用域,可以这样做:
此构造为 CatsController
内部定义的每个路由处理器设置了 HttpExceptionFilter
。
要创建全局作用域的过滤器,需执行以下操作:
warning 警告
useGlobalFilters()
方法不会为网关或混合应用设置过滤器。
全局作用域的过滤器用于整个应用程序,作用于每个控制器和每个路由处理器。在依赖注入方面,从任何模块外部注册的全局过滤器(如上述示例中使用 useGlobalFilters()
)无法注入依赖项,因为这是在模块上下文之外完成的。为解决此问题,您可以使用以下构造直接从任何模块注册全局作用域的过滤器:
info 注意 使用此方法执行过滤器依赖注入时,需注意无论在哪一个模块中使用该构造,过滤器实际上是全局性的。应该在哪里进行操作呢?应选择过滤器(如上例中的
HttpExceptionFilter
)被定义的模块。同时,useClass
并非处理自定义提供者注册的唯一方式。了解更多请点击此处 。
您可以根据需要使用此技术添加任意数量的过滤器,只需将每个过滤器添加到 providers 数组中即可。
为了捕获每一个未处理的异常(无论异常类型如何),只需让 @Catch()
装饰器的参数列表为空即可,例如 @Catch()
。
在以下示例中,我们有一段与平台无关的代码,因为它使用 HTTP 适配器来传递响应,而没有直接使用任何平台特定对象(Request
和 Response
):
warning 注意 当将捕获所有异常的过滤器与绑定到特定类型的过滤器结合使用时,应首先声明"捕获所有"过滤器,以便特定过滤器能正确处理绑定类型。
通常情况下,您会创建完全自定义的异常过滤器来满足应用程序需求。但在某些用例中,您可能希望直接扩展内置的全局异常过滤器 ,并根据特定因素覆盖其行为。
要将异常处理委托给基础过滤器,需要扩展 BaseExceptionFilter
并调用继承的 catch()
方法。
警告 扩展自
BaseExceptionFilter
的方法作用域和控制器作用域过滤器不应使用new
实例化,而应由框架自动实例化。
全局过滤器可以扩展基础过滤器,可通过以下两种方式之一实现。
第一种方法是在实例化自定义全局过滤器时注入 HttpAdapter
引用:
第二种方法是使用 APP_FILTER
令牌如图所示 。