欢迎持续关注 NestJs 之旅
系列文章,关注公众号可以获得最新的教程!
传统的异常处理
在前面的内容中我们介绍了 NestJs 的几大常用组件,但是有一点没有做出说明,当我们的应用需要中断此次请求且输出错误信息时,我们需要怎么做?
这个问题有两种解决办法:
-
services 层直接返回中断请求的响应对象,controller 直接输出该对象即可
if(!this.allowLogin()) {return {errcode: 403, errmsg: '不允许登录'}; }
- services 层抛出异常,controller 捕获该异常,然后输出响应对象
以上两种方法都有一定的缺点:
- controller 调用多个 services 时,需要依据 services 层的返回值来进行错误判断,要是漏了判断的话会导致原本需要中断的请求处理继续运行,导致不可预料的后果
- 如果每个 controller 都需要 try/catch 掉 services 层抛出的异常的话,会多了很多“重复”代码
那有没有一个像 SpringBoot 的 ExceptionHandler
相似的解决办法呢?
NestJs 的异常处理
NestJs 提供了统一的异常处理器,来集中处理运行过程中 未捕获的异常,可以自定义响应参数,非常灵活。
默认响应
NestJs 内置了默认的 全局异常过滤器 ,该过滤器处理HttpException(及其子类) 的异常。如果抛出的异常不是上述异常,则会响应以下默认 JSON:
{
"statusCode": 500,
"message": "Interval server error"
}
内置异常过滤器
由于 NestJs 内置了默认的异常过滤器,如果在应用内抛出 HttpException,是可以被 NestJs 自动捕获的。
比如在 services 层抛出一个 HttpException:
@Injectable()
export class UserService {login(username: string, password: string) {if(!this.allowLogin()) {throw new HttpException('您无权登录', HttpStatus.FORBIDDEN);
}
return {user_id:1, token: 'fake token'}
}
}
controller 正常调用该 services 即可:
@Controller('users')
export class UserController {constructor(private readonly userService: UserService) {}
@Post('login')
login(@Body('username') username: string, @Body('password') password: string) {return this.userService.login(username, password);
}
}
客户端访问 /user/login 时,如果不允许登录,会收到以下响应:
{
"statusCode": 403,
"message": "您无权登录"
}
一般情况下,上述 JSON 的返回的信息是不够的,比如有些业务自定义的错误码没地方可以自定义。
如果你有这种需求,可以传递 object 给 HttpException 的第一个参数来实现:
throw new HttpException({errcode: 40010, errmsg: '您无权登录'}, HttpStatus.FORBIDDEN);
客户端访问时,如果不允许登录,会收到以下响应:
{
"errcode": 40010,
"errmsg": "您无权登录"
}
自定义异常
企业级应用开发过程中,使用 HttpException 进行处理对开发是不太友好的,一个比较常用的做法是自定义一个 UserException 来承载业务异常(系统运行正常,只不过当前请求不满足业务上的要求而中断,比如注册的时候用户名重复的时候打回去,此时数据库查询是正常的,这就是业务异常和系统异常的区别)。
export class UserException extends HttpException {constructor(errcode: number, errmsg: string, statusCode: number) {super({ errcode, errmsg}, statusCode);
}
}
业务层在使用该异常时直接使用以下代码即可,将原来传递对象的代码扁平化了:
throw new UserException(40010, '您无权登录', HttpStatus.FORBIDDEN);
语义化业务异常
使用自定义异常时 HTTP 协议层是正常的,抛出 403 错误有点不符合语义化的需求。对上例改造一下:
export class UserException extends HttpException {constructor(errcode: number, errmsg: string) {super({ errcode, errmsg}, HttpStatus.OK);
}
}
throw new UserException(40010, '您无权登录');
此时客户端收到的 HttpStatus 为 200,意味着此次请求在协议层面是成功的,只不过业务层返回了错误。前端在处理响应时可以直接对 errcode 是否为 0 来确定此次请求是否成功。
自定义异常过滤器
虽然内置的异常过滤器可以自动处理很多情况,但是不是“可编程”的,也就是说我们无法完全控制异常处理过程,如果我们需要记录日志的话,使用内置的异常过滤器办不到,这时候可以使用 @Catch 注解来自定义异常处理器,添加日志记录什么的。
import {ExceptionFilter, Catch, ArgumentsHost, HttpException} from '@nestjs/common';
import {Request, Response} from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {catch(exception: HttpException, host: ArgumentsHost) {const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// @todo 记录日志
console.log('%s %s error: %s', request.method, request.url, exception.message);
// 发送响应
response
.status(status)
.json({
statusCode: status,
message: exception.message
path: request.url,
});
}
}
ArgumentHost
ArgumentHost 是原始请求的包装器,由于 NestJs 支持 HTTP/GRPC/WebSocket,这三种请求的原始请求对象是有差异的,为了异常过滤器能够统一处理这三种异常,NestJs 做了包装。最终在使用时处理那种异常由开发者来决定。
ArgumentHost 接口定义如下:
export interface ArgumentsHost {getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;}
如果需要处理的是 WebSocket 异常,就使用host.switchToWs(),其他异常以此类推。
使用自定义异常过滤器
如果定义完自定义异常过滤器之后,直接去访问会抛出异常的接口,此时可以发现并没有走自定义异常过滤器。
因为我们 只是定义,并没有注册。
使用 @UseFilters 注册自定义异常过滤器。
异常过滤器有以下三种作用范围:
- 方法级别
- 控制器级别
- 全局级别
方法级别
只会处理该方法上抛出的异常,其他方法抛出的异常不会处理。
@Post('login')
@UseFilters(UserExceptionFilter)
login(@Body('username') username:string, password: string) {throw new UserException(40010, '您无权登录');
}
控制器级别
只会处理该控制器方法上抛出的异常,其他控制器抛出的异常不处理。
@Controller('user')
@UseFilters(UserExceptionFilter)
export class UserController { }
全局级别
在应用入口注册,不会对 Websocket 或者混合应用(同时支持两种应用,如 HTTP/GRPC 或者 HTTP/WebSocket)生效。一般 Web 开发中全局异常过滤器已经够用了。
在 main.ts 中注册全局异常过滤器
async function bootstrap() {const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new UserExceptionFilter());
await app.listen(3000);
}
bootstrap();
依赖注入
由于异常过滤器并不是任何模块上下文的一部分,所以 NestJs 无法对其进行依赖注入管理,如果有此种需求,比如在异常过滤器中注入 service,需要定义服务提供者。服务提供者名称为 NestJs 规定的常量 APP_FILTER
import {Module} from '@nestjs/common';
import {APP_FILTER} from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: UserExceptionFilter,
},
],
})
export class AppModule {}
捕获多种异常或者所有异常
上例中提到的自定义异常处理器只会捕获 UserException 异常,如果有系统异常,会使用内置的异常处理器。通过传入异常类型给 @Catch 装饰器来捕获多种异常。如果不传任何异常类型的话,NestJs 会捕获所有异常(也就是 Error 及其子类)。
import {ExceptionFilter, Catch, ArgumentsHost, HttpException} from '@nestjs/common';
import {Request, Response} from 'express';
@Catch() // 捕获所有异常
export class HttpExceptionFilter implements ExceptionFilter<Error> {catch(exception: Error, host: ArgumentsHost) {const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// @todo 记录日志
console.log('%s %s error: %s', request.method, request.url, exception.message);
// 发送响应
response
.status(status)
.json({
statusCode: status,
message: exception.message
path: request.url,
});
}
}
结尾
异常过滤器让应用异常有了统一的处理渠道,同时也解决文章开头提出的两个问题。通过自定义异常过滤器,开发者可以进行统一响应格式,统一记录日志等等操作。
如果您觉得有所收获,分享给更多需要的朋友,谢谢!
如果您想交流关于 NestJs 更多的知识,欢迎加群讨论!