异常过滤器
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路由处理程序)。
我们假设该路由处理程序由于某种原因引发异常。为了演示这一点,我们将对其进行硬编码,如下所示:
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
我们在这里使用了HttpStatus
。这是从@nestjs/common
包导入的帮助器枚举
当客户端调用此端点时,响应如下所示:
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
构造函数采用两个必需参数来确定响应:
- 响应参数定义JSON响应正文。它可以是
string
或object
,如下所述。 status
参数定义HTTP状态代码
默认情况下,JSON响应正文包含两个属性:
statusCode
: 默认为状态参数中提供的HTTP状态码message
: 基于状态的HTTP错误的简短描述
要仅覆盖JSON响应正文的消息部分,请在response
参数中提供一个字符串。
要覆盖整个JSON响应正文,请在响应参数中传递一个对象。
Nest将序列化该对象并将其作为JSON响应正文返回。
第二个构造函数参数status
应该是有效的HTTP状态码。
最佳实践是使用从@nestjs/common
导入的HttpStatus
枚举。
第三个构造函数参数(可选)options
可用于提供错误原因。
这个cause
对象不会序列化到响应对象中,但它对日志记录很有用,可以提供有关导致HttpException
被
抛出的内部错误的有价值信息。
这是一个覆盖整个响应正文并提供错误原因的示例:
@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将识别您的异常,并自动处理错误响应。让我们实现这样一个自定义异常:
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
由于ForbiddenException
扩展了基类HttpException
,因此它将与内置异常处理程序无缝协作,
因此我们可以再findAll()
方法中使用它。
@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
类实例的异常,并为它们实现自定义逻辑响应逻辑。
为此,我们需要访问底层平台的Request
和Response
对象。我们将访问Request
对象,以便提取原始url
并将其包含在日志信息中。我们将使用Response
对象通过response.json()
方法直接控制发送的响应。
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
是一个功能强大的实用程序对象,
我们将在执行上下文章节中进一步研究它。在此代码示例中,我们使用它来获取对传递给原始请求处理程序
(在引发异常的控制器中)的Request
和Response
对象的引用。
在此代码示例中,
我们在ArgumentsHost
上使用了一些辅助方法来获取所需的Request
和Response
对象。
*
这种抽象级别的原因是ArgumentsHost
在所有上下文中都起作用(例如,我们现在正在使用的HTTP服务器
上下文,还有微服务和WebSocket)。在执行上下文一章中,我们将了解如何利用ArgumentsHost
及其辅助函数
的功能来访问任何执行上下文的适当底层参数。这将使我们能够编写跨所有上下文运行的通用异常过滤器。
绑定过滤器
让我们将新的HttpExceptionFilter
绑定到CatsController
的create()
方法。
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@UseFilter()
装饰器是从@nestjs/common
包导入的。
我们在这里使用@UseFilter()
装饰器。与@Catch()
装饰器类似,它可以采用单个过滤器实例,
或以逗号分隔的过滤器实例列表。在这里,我们就地创建了HttpExceptionFilter
的实例。
或者,您可以传递类(而不是实例),将实例化的责任留给框架,并启动依赖项注入。
@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),您可以执行以下操作:
@UseFilter(new HttpExceptionFilter())
export class CatsController {}
此构造为CatsController
内定义的每个路由处理程序设置HttpExceptionFilter
。
要创建全局范围的过滤器(global-scoped),您需要执行以下操作:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
useGlobalFilters()
方法不会为网关或混合应用程序设置过滤器。
全局范围的过滤器用于整个应用程序、每个控制器和每个路由处理程序。
就依赖注入而言,从任何模块外部注册的全局过滤器(使用useGlobalFilters()
如上例所示)
无法注入依赖项,因为这是在任何模块的上下文之外完成的。
为了解决此问题,您可以使用以下结构直接从任何模 块注册全局范围的过滤器:
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适配器来传递响应,并且不直接使用任何
特定于平台的对象(Response
和Request
)。
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()
方法。
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
令牌,
如此处所示