Nestjs的过滤器使用

在前边的中间件章节我们说到过,异常的统一格式处理是通过异常过滤器来实现的,这一节我们就看看异常过滤器的内容。

异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

异常初始

HttpException基础异常类

Nest提供了一个内置的 HttpException 类,我们新建一个模块courses,在这里硬编码抛出一个异常试一下:

courses模块无实际内容,只是拿一个controller来演示过滤器,无需关注太多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// courses.controller.ts
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
// localhost:3000/nest/courses/findAll
{
"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
// courses.controller.ts
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
// localhost:3000/nest/courses/findAll
{
"message": "bad request",
"error": "BAD_REQUEST"
}

可以发现,这次返回结果就是我们传入的对象,而不是其默认格式了。

自定义异常

前边说了异常基础类HttpException,基于此,我们可以创建自己的自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad-request.exception.ts
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
// courses.controller.ts
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 HttpException(
// {
// message: 'bad request',
// error: 'BAD_REQUEST',
// },
// HttpStatus.BAD_REQUEST,
// )
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
// courses.controller.ts
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
// http-exception.filter.ts
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()
// controller抛出的异常内容
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
// courses.controller.ts
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
// localhost:3000/nest/courses/findAll
{
"timestamp": "2023/2/20 15:21:30",
"path": "/nest/courses/findAll",
"message": "bad request",
"error": "BAD_REQUEST"
}

可以看到,已经将响应内容处理成我们想要的样子了。

在前边的例子中,我们将过滤器绑定到单个路由,其实过滤器也可以绑定到一个控制器:

1
2
3
4
5
// courses.controller.ts

@Controller('courses')
@UserFilters(HttpExceptionFilter)
export class CoursesController {}

相信大家已经猜到了,过滤器也可以绑定到全局,实现全局处理:

1
2
3
4
5
6
7
// main.ts
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
// all-exception.filter.ts
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();
// 如果是HttpException,我们可以获取到状态码,否则500
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

response.status(status).json({
timestamp: new Date().toISOString(),
path: request.url,
});
}
作者

胡兆磊

发布于

2023-02-20

更新于

2023-02-20

许可协议