在前边的中间件章节我们说到过,异常的统一格式处理是通过异常过滤器来实现的,这一节我们就看看异常过滤器的内容。
异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
异常初始
HttpException基础异常类
Nest提供了一个内置的 HttpException 类,我们新建一个模块courses,在这里硬编码抛出一个异常试一下:
courses模块无实际内容,只是拿一个controller来演示过滤器,无需关注太多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { Controller, Get, HttpException, HttpStatus, } from '@nestjs/common' import { CoursesService } from './courses.service' @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {}
@Get('findAll') findAll(): string { throw new HttpException('Bad Request', HttpStatus.BAD_REQUEST) } }
|
HttpStatus是一个Nest内置的枚举,我们通过他来指定状态码。
我们访问这个接口会得到如下内容:
1 2 3 4 5
| { "statusCode": 400, "message": "Bad Request" }
|
我们对HttpException构造函数传递了两个参数,第二个参数是一个状态码,很好理解,来看一下第一个参数吧:
- 第一个参数可以是一个字符串或者一个对象
- 第一个参数如果是一个字符串,那么会使用HttpException的默认格式返回,就是上边的这种
- 如果是一个对象的话nest会将其序列化作为json返回
假如我们传入的是一个对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Controller, Get, HttpException, HttpStatus, } from '@nestjs/common' import { CoursesService } from './courses.service' @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {}
@Get('findAll') findAll(): string { throw new HttpException( { message: 'bad request', error: 'BAD_REQUEST', }, HttpStatus.BAD_REQUEST, ) } }
|
此时访问该接口:
1 2 3 4 5
| { "message": "bad request", "error": "BAD_REQUEST" }
|
可以发现,这次返回结果就是我们传入的对象,而不是其默认格式了。
自定义异常
前边说了异常基础类HttpException,基于此,我们可以创建自己的自定义异常
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { HttpException, HttpStatus } from '@nestjs/common' export class DiyBadRequestException extends HttpException { constructor() { super( { message: 'bad request', error: 'BAD_REQUEST', }, HttpStatus.BAD_REQUEST, ) } }
|
我们在courses.controller.ts中使用这个自定义异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Controller, Get, } from '@nestjs/common' import { CoursesService } from './courses.service' import { DiyBadRequestException } from './bad-request.exception' @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {} @Get('findAll') findAll(): string { throw new DiyBadRequestException() } }
|
此时访问接口,我们发现可以得到相同的响应内容
内置HTTP异常
Nest 提供了一系列继承自核心异常 HttpException 的内置HTTP异常
其中包括:
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableException
InternalServerErrorException
NotImplementedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
所以HTTP异常,我们就没必要再去自定义了,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Controller, Get, BadRequestException, } from '@nestjs/common' import { CoursesService } from './courses.service' @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {}
@Get('findAll') findAll(): string { throw new BadRequestException({ message: 'bad request', error: 'BAD_REQUEST', }) } }
|
访问接口,我们依然能得到相同的响应。
异常过滤器
前边我们说的通过过滤器来统一返回错误内容,接下来就看一下吧。
HTTP异常过滤器
我们先来创建一个过滤器,统一处理HTTP异常的返回内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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() const exceptionRes = exception.getResponse()
response.status(status).json({ timestamp: new Date().toLocaleString(), path: request.url, ...(<any>exceptionRes), }) } }
|
所有异常过滤器都应该实现通用的 ExceptionFilter<T> 接口。它需要你使用有效签名提供 catch(exception: T, host: ArgumentsHost)方法。T 表示异常的类型。
@Catch() 装饰器绑定所需的元数据到异常过滤器上。它告诉 Nest这个特定的过滤器正在寻找 HttpException 而不是其他的。在实践中,@Catch() 可以传递多个参数,所以你可以通过逗号分隔来为多个类型的异常设置过滤器。
ArgumentsHost是一个应用上下文,还有一个基于ArgumentsHost扩展出来的执行上下文ExecutionContext,后者在守卫、拦截器中常用,在这里不展开讲,只看一下其基础用法即可。
在异常过滤器中,我们可以通过exception.getResponse()来拿到我们抛出的异常中的响应内容,然后通过response来格式化返回。
接下来让我们将 HttpExceptionFilter 绑定到CoursesController中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { Controller, Get, UseFilters, BadRequestException, } from '@nestjs/common' import { CoursesService } from './courses.service' import { HttpExceptionFilter } from '../http-exception.filter' @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {} @Get('findAll') @UseFilters(HttpExceptionFilter) findAll(): string { throw new BadRequestException({ message: 'bad request', error: 'BAD_REQUEST', }) } }
|
通过UseFilters绑定过滤器,可以绑定多个。
此时访问接口:
1 2 3 4 5 6 7
| { "timestamp": "2023/2/20 15:21:30", "path": "/nest/courses/findAll", "message": "bad request", "error": "BAD_REQUEST" }
|
可以看到,已经将响应内容处理成我们想要的样子了。
在前边的例子中,我们将过滤器绑定到单个路由,其实过滤器也可以绑定到一个控制器:
1 2 3 4 5
|
@Controller('courses') @UserFilters(HttpExceptionFilter) export class CoursesController {}
|
相信大家已经猜到了,过滤器也可以绑定到全局,实现全局处理:
1 2 3 4 5 6 7
| async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(3000); } bootstrap();
|
捕获任意异常
将 @Catch() 装饰器的参数列表设为空,可以捕获任意未处理的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common';
@Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
response.status(status).json({ timestamp: new Date().toISOString(), path: request.url, }); }
|