Nest
有个内置的异常处理层,它的责任是处理访问应用程序没有处理的那些异常。当您的处理没有处理异常的时候,它会被这个层捕获,并且自动的返回一个对用户友好的返回。
开箱即用,此操作由内置的全局Exception filters
异常过滤器执行,该过滤器处理HttpException类型的异常(及其子类)。当有异常被忽略(既不是HttpException
也不是继承自HttpException
的类),这个内置的异常过滤器会返回默认的JSON
{
"statusCode": 500,
"message": "Internal server error"
}
抛出标准异常
Nest提供了一个内置HttpException
类,它从@nestjs/common
这个包导出。对于典型的基于HTTP REST/GraphQL API的应用程序,最佳做法是在出现某些错误情况时发送标准HTTP响应对象。
例如,在CatsController
里面,有findAll()
方法。让我们假设这个路由处理程序出于某种原因抛出了一个异常。为了证明这一点,我们将硬编码如下:
// cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
}
当客户端调用这个接口,会返回
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
构造函数必须传入2个参数来特定返回结果
response
参数用来定义JSON返回的内容。可以是字符串也可以是Object对象/status
参数用来定义HTTP status code
默认的情况下,JSON返回内容里面有2个属性
statusCode
:默认为状态参数中提供的HTTP状态代码message
:基于状态的HTTP错误的简短描述
要仅覆盖JSON响应主体的消息部分,请在response
参数中提供一个字符串。要覆盖整个JSON响应体,请在response
参数中传递一个对象。Nest将序列化对象并将其作为JSON响应体返回。
第二个构造函数参数status
应该是有效的HTTP状态代码。最佳实践是使用从@nestjs/common
导入的HttpStatus
枚举。
还有第三个构造函数参数(可选)-options
,可用于提供错误原因cause
。此cause
对象未序列化到响应对象中,但它可用于日志记录,提供有关导致引发HttpException
的内部错误的有用信息。
@Get('throw')
async findAll_throw() {
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 异常,内置 HTTP 异常会描述。如果不得不创建自定义异常,比较好的做法是继承HttpException
,Nest 会组织你的异常,并且自动生成异常返回内容。让我们来试一下
import {HttpException, HttpStatus} from '@nestjs/common'
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
因为ForbiddenException
继承了HttpException
,他的工作方式就和内置异常比较类似了。
@Get('custom')
async findAll_custom() {
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
所有的内置异常都有cause
(error)和options
(error描述)参数
throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
掉用上面Exception,我们可以看到下面的返回结果
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
异常过滤
虽然基本(内置)异常过滤器可以为您自动处理许多情况,但您可能需要对异常层进行完全控制。例如,您可能希望添加日志记录或基于某些动态因素使用不同的JSON模式。Exception filters
异常过滤器正是为此目的而设计的。它们允许您控制发送回客户端的响应的确切控制流和内容。
让我们创建一个异常过滤器,它负责捕获作为HttpException
类实例的异常,并为它们实现自定义响应逻辑。为此,我们需要访问底层平台请求Request
和响应Response
对象。我们将访问Request
对象,这样我们就可以提取原始url
并将其包含在日志信息中。我们将使用Response.json()
方法,使用Response对象直接控制发送的响应。
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:ArgumentHost)方法及其指定的签名。T表示异常的类型。
警告:
如果您使用的是@nestjs/platform-fastify,则可以使用response.send()代替response.json()。不要忘记从fastify导入正确的类型。
@Catch(HttpException)
装饰器告诉Nest
在发生HttpException
的情况下,处理过滤器的逻辑。@Catch()
可以使用单个参数或逗号分隔的列表。这允许您一次为几种类型的异常设置筛选器。
参数ArgumentsHost
让我们看看catch()
方法的参数。exception
参数是当前正在处理的异常对象。host
参数是ArgumentHost
对象。ArgumentsHost
是一个强大的实用程序对象,具体可以参照execution context chapter。在这个代码示例中,我们使用它获取对传递给原始请求处理程序(在异常发生的控制器中)的请求Request
和响应Response
对象的引用。在这个代码示例中,我们使用了ArgumentHost上的一些助手方法来获取所需的请求和响应对象。了解更多关于ArgumentsHost。
这种抽象级别的原因是ArgumentHost
在所有上下文中都起作用(例如,我们现在使用的HTTP服务器上下文,以及微服务和WebSocket)。
绑定过滤器
在CatsController
的create()
方法上new一个HttpExceptionFilter
。
/**
* 绑定过滤器
*/
@Post('bind')
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
说明:`@UserFilters`通过`@nestjs/common`包导入
我们在这里使用了@UseFilters()
。与@Catch()
装饰器类似,它可以采用单个过滤器实例,也可以采用逗号分隔的过滤器实例列表。在这里,我们就地创建了HttpExceptionFilter的实例。或者,您可以传递class类(而不是实例),将实例化的责任留给框架,并启用依赖注入。
@Post('bindNoNew')
@UseFilters(HttpExceptionFilter)
async create_no_new(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
说明:推荐尽可能使用类来绑定过滤器,这会减少内存使用率。Nest可以重复利用这些类
例子里面我们的使用方法是加在方法上的。属于方法的作用范围。我们也是使用类过滤器或者全局过滤器。
类过滤器:
@Controller('cats')
// 类过滤器
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
全局过滤器:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
警告:useGlobalFilters()方法不为gateways或混合应用程序设置筛选器。
全局范围的筛选器用于整个应用程序、每个控制器和每个路由处理程序。在依赖项注入方面,从任何模块外部注册的全局过滤器(使用上面示例中的useGlobalFilters()
)都不能注入依赖项,因为这是在任何模块的上下文之外完成的。为了解决此问题,可以使用以下构造直接从任何模块注册全局范围的筛选器:
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter
}],
})
export class AppModule {}
捕获所有异常
为了可以捕获所有的异常,我们可以不给@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 {
// 在某些情况下,“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()
方法。
@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
令牌:binding-filters。