1. 前言
通过接口 GET /users/4 可以查询到 id 为 4 的用户信息,通过 GET /users/123 可以查询到 id 为 123 的用户信息,问题是数据库中并没有后者的数据,此时应该告诉前端没有这项数据。本篇将会讲解错误处理相关的内容,这将涉及到 NestJS 中的异常过滤器部分。
欢迎加入技术交流群。
- NestJS 🧑🍳 厨子必修课(一):后端的本质
- NestJS 🧑🍳 厨子必修课(二):项目创建
- NestJS 🧑🍳 厨子必修课(三):控制器
- NestJS 🧑🍳 厨子必修课(四):服务类
- NestJS 🧑🍳 厨子必修课(五):Prisma 集成(上)
- NestJS 🧑🍳 厨子必修课(六):Prisma 集成(下)
- NestJS 🧑🍳 厨子必修课(七):管道
- NestJS 🧑🍳 厨子必修课(八):异常过滤器
2. NotFoundException
通过 NotFoundException
实例直接抛出错误:
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async findOne(id: number) {
const user = await this.prisma.user.findUnique({
where: { id },
// 关联
include: {
orders: true,
},
});
if (!user) {
throw new NotFoundException('用户不存在');
}
return user;
}
}
这里做了防御性编程,如果没有查询到用户,使用 throw new NotFoundException('用户不存在');
抛出错误。
访问 http://localhost:3000/users/123 后:
返回的 JSON 对象由以下字段组成:
message
:描述具体错误信息,通常是一个字符串,解释错误的原因或背景。error
:标识发生错误的类型或类别。通常为错误的名称或类别(如 “Bad Request” 或 “Unauthorized”)。statusCode
:HTTP 状态码,以数字形式表示请求失败的类型。常见的状态码包括 400(Bad Request)、401(Unauthorized)、404(Not Found)等,用于反映请求的结果状态。
除了 NotFoundException
,NestJS 中还提供了以下内置的 HTTP 异常:
- BadRequestException (400 Bad Request)
表示客户端发送的请求有错误,服务器无法处理。例如,参数缺失、无效输入等情况。
- UnauthorizedException (401 Unauthorized)
表示请求没有经过身份验证或认证失败。通常出现在访问需要身份验证的资源时,客户端没有提供有效的认证信息。
- ForbiddenException (403 Forbidden)
表示客户端的身份认证成功,但没有权限访问请求的资源。通常发生在权限不足时。
- NotAcceptableException (406 Not Acceptable)
表示服务器无法生成客户端可以接受的响应格式。通常出现在客户端的 Accept 头与服务器支持的响应格式不匹配时。
- RequestTimeoutException (408 Request Timeout)
表示客户端请求超时。服务器在等待请求时超过了设定的时间限制。
- ConflictException (409 Conflict)
表示请求与服务器的当前状态存在冲突。例如,创建资源时,资源已经存在。
- GoneException (410 Gone)
表示请求的资源已经永久不可用,并且不会再恢复。常用于删除的资源。
- HttpVersionNotSupportedException (505 HTTP Version Not Supported)
表示服务器不支持客户端请求的 HTTP 协议版本。
- PayloadTooLargeException (413 Payload Too Large)
表示客户端发送的数据体(如上传文件)超过了服务器允许的大小限制。
- UnsupportedMediaTypeException (415 Unsupported Media Type)
表示客户端请求的媒体类型不被服务器支持。例如,上传文件的格式不被接受。
- UnprocessableEntityException (422 Unprocessable Entity)
表示服务器理解客户端的请求内容,但由于语义错误而无法处理。例如,输入数据的格式正确但内容无效。
- InternalServerErrorException (500 Internal Server Error)
表示服务器在处理请求时遇到了内部错误。是一个通用的错误状态码,表示服务器无法处理请求。
- NotImplementedException (501 Not Implemented)
表示服务器不支持请求的方法。通常用于服务器不支持客户端请求的功能时。
- ImATeapotException (418 I’m a teapot)
这是一个愚人节玩笑性质的 HTTP 状态码,表示服务器拒绝酿茶的请求(参见 IETF RFC 2324)。常用于测试或幽默场景。
- MethodNotAllowedException (405 Method Not Allowed)
表示请求的方法(如 GET、POST)在目标资源中不可用。例如,资源只支持 GET 请求,而客户端使用了 POST。
- BadGatewayException (502 Bad Gateway)
表示服务器作为网关或代理时,从上游服务器接收到无效响应。
- ServiceUnavailableException (503 Service Unavailable)
表示服务器暂时无法处理请求,通常是因为过载或维护。
- GatewayTimeoutException (504 Gateway Timeout)
表示服务器作为网关或代理时,从上游服务器接收响应超时。
- PreconditionFailedException (412 Precondition Failed)
表示客户端发送的请求没有满足服务器的某些前置条件。例如,If-Match 头字段的条件不满足。
以上各种类型都可以从 @nestjs/common
包中导出使用,例如 BadRequestException
:
// app.controller.ts
import { BadRequestException } from '@nestjs/common';
@Controller()
export class UsersController {
@Get('test-error')
testError() {
throw new BadRequestException('测试错误');
}
}
3. 自定义 HTTP 异常过滤器
3.1 方法1:实现 ExceptionFilter
接口
自定义的异常过滤器需要实现 ExceptionFilter
接口的 catch
方法:
// src/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();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
在上面的示例中通过 @Catch()
装饰器**指定要捕获的异常类型(**只捕获 HttpException
)。
@Catch()
装饰器也支持可以采用单个参数或逗号分隔的列表,这可以同时为多种类型的异常设置过滤器。
如果没有在 @Catch()
中指定异常类型,过滤器将捕获所有异常类型。这对于全局异常处理非常有用。
3.2 方法2:继承 HttpException
类
自定义的 CustomHttpException
类继承 HttpException
类:
// src/custom-http-exception.filter.ts
import { HttpException } from '@nestjs/common';
export class CustomHttpException extends HttpException {
constructor(message: string, statusCode: number) {
super(message, statusCode);
}
}
使用 CustomHttpException
类实例抛出异常:
// app.controller.ts
@Get('custom')
getCustom(){
throw new CustomHttpException('后山乃禁地!', HttpStatus.FORBIDDEN)
}
4. 绑定范围
过滤器可以绑定的范围有:
- HTTP 方法
- 控制器
- 全局范围
4.1 HTTP 方法
使用 @UseFilters()
装饰器可以使得过滤器只在指定 HTTP 方法下起作用:
// users.controller.ts
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('custom')
@UseFilters(new HttpExceptionFilter())
// @UseFilters(HttpExceptionFilter)
getCustom() {
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
message: '后山乃禁地!请速速离去!',
error: 'Forbidden',
},
403,
);
}
}
装饰器中的参数既可以传递一个过滤器的实例(new HttpExceptionFilter()
),也可以传递一个类(HttpExceptionFilter
)。
4.2 控制器
也可以在指定的控制器下使用 @UseFilters()
装饰器:
@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
// ...
}
这样就不用给该控制器下的所有 HTTP 方法都添加装饰器了。
4.3 全局应用
使用 app.useGlobalFilters()
方法来注册全局异常过滤器:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(FuelStationModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
设置完成后,应用的所有控制器和方法都加上了过滤器,这样应用的任何地方抛出的异常都会被全局捕获,可以确保一致性和可控的错误响应格式。
但是这种方式并不能灵活地与依赖注入的特性相结合,NestJS 提供了另一种全局绑定过滤器的方式:
// app.module.ts
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
5. 日志记录与异常过滤器
5.1 记录异常日志(Logger
)
通过 NestJS 的 Logger
服务记录异常详细信息,以便在应用崩溃时追踪问题。
// http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse<Response>(); // 获取响应对象
const request = ctx.getRequest<Request>(); // 获取请求对象
const status = exception.getStatus(); // 获取异常状态码
const message = exception.message
? exception.message
: `${status >= 500 ? '服务器错误(Service Error)' : '客户端错误(Client Error)'}`;
const nowTime = new Date().getTime();
const errorResponse = {
data: {},
message,
code: -1,
date: nowTime,
path: request.url,
};
// 记录日志到控制台
this.logger.error(
`【${nowTime}】${status} ${request.method} ${request.url} query:${JSON.stringify(request.query)} params:${JSON.stringify(
request.params,
)} body:${JSON.stringify(request.body)}`,
JSON.stringify(errorResponse),
HttpExceptionFilter.name,
);
response.status(status).json(errorResponse);
}
}
// users.controller.ts
@Get('custom')
@UseFilters(new HttpExceptionFilter(new Logger('UsersController')))
getCustom() {
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
message: '后山乃禁地!请速速离去!',
error: 'Forbidden',
},
403,
);
}
或是依赖注入现有日志服务(nestjs-pino):
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logService: LogService) {}
catch(exception: HttpException, host: ArgumentsHost) {
this.logService.logError(exception);
// 处理异常
}
}
5.2 将错误信息发送到外部监控服务(如 Sentry)
集成外部监控服务,如 Sentry,来捕获和分析错误日志:
import * as Sentry from '@sentry/node';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
Sentry.captureException(exception);
// 处理异常
}
}
6. 总结
异常过滤器是 NestJS 提供的强大工具,用于捕获和处理应用中的异常。通过自定义和全局应用过滤器,开发者可以确保异常处理的一致性,并提升应用的健壮性和用户体验。在复杂项目中,不同模块和服务可以使用不同的过滤器来处理特定场景下的异常。