# 前言
有读过我前面写的一些文章的同学,应该知道我最近是一直在致力于 Nest 这款框架的学习。最近刚跟着一些教程以及官方文档把 Nest.js 中的一些 API 熟悉了一下,也跟着稍微做了一些和 orm 框架的集成,顺便搭了一些 demo 小项目。一路跟下来的我基本没遇到什么特别困难的点。于是想着趁着手感还在,先把我前阵子立的项 ——Picals 的后端给赶紧开工了先。
当我跟着以前做的 demo 一步步走的时候,我发现我多了一个需求: 自定义异常 。在这个项目中,我打算引入自定义异常状态码,不仅仅局限于 nest 框架内部的异常处理,我也想要在正常执行逻辑的同时,遇到用户的一些不当操作能够主动的将异常抛出,并且赋予不同的状态码以及异常信息,以提醒用户。
但当我跟以往一样定义自定义好异常以及异常过滤器后,我发现自定义的异常虽然继承了 nest 中的 基础异常类 HttpException ,但是除了自己抛出的异常以外,其余的异常并没有走到这个过滤器。 并且就在这个时候,我突然意识到 Nest 中好像我从来没有显式的抛出过异常,那么之前那些异常都是怎么处理的呢?
因此出于好奇,我去查找了一些 nest 的官方资料,最后稍微整理了一下,当作给自己的 nest 学习之路上留点记录吧。
# 基础异常类:HttpException
# 是什么?
首先我们需要明确一点,Nest 中提供了一个最最最基础的异常类:HttpException。这也就意味着,除了你的自定义异常外,基本所有的其他异常都是继承这个类的。因此如果你自定义了一个异常过滤器并且 @Catch 了这个异常并且将其注册为全局,那么你的所有在框架内触发的异常都可以被它捕捉到,然后输出你自定义的异常 log。
那么,HttpException 究竟是什么呢?我们可以去官方文档里面看一下:
Nest
提供了一个内置的HttpException
类,它从@nestjs/common
包中导入。对于典型的基于HTTP
REST/GraphQL
API
的应用程序,最佳实践是在发生某些错误情况时发送标准 HTTP 响应对象。
# 看一看源码?
好的,我们已经了解到它就是一个 类 ,并且对于框架层面的错误基本都会触发它。那么,这个 类 到底由什么构成呢?我们可以去看一下源码(声明文件):
这个类非常单纯的继承了一个 Error 类,并且在它的基础之上新增了很多的方法。Error 类我们都知道,它是 JavaScript 中所有错误对象的基类,JS 的所有异常都离不开它。
从它的构造器也可以看出,创建它需要传入:
- 返回响应
- 错误状态
- 额外错误选项
- 原因
- 描述
除了构造本身,它也提供了一系列的功能函数,用于初始化异常 + 获取异常的相关信息。
当然,功能最为强大的是它的静态方法。列一下它包含的几个静态方法:
-
createBody
:主要用于构建异常的响应体。根据传入的参数类型和数量的不同有不同的行为:- 当传入的是一个
null
或空字符串以及一个消息和状态码时,它会创建一个包含默认错误消息和状态码的响应体。 - 如果传入的是消息、错误和状态码,它会创建一个包含这些信息的响应体。
- 如果传入的是一个自定义的对象,它将直接返回这个对象作为响应体。
- 当传入的是一个
-
getDescriptionFrom
:用于从传入的参数(可以是一个字符串或一个包含描述信息的对象)中提取错误描述。在创建HttpException
实例时有助于从不同形式的输入中统一提取描述信息。 -
getHttpExceptionOptionsFrom
:用于从传入的参数中提取HttpExceptionOptions
对象。这有助于在抛出异常时,从提供的描述或选项中提取详细的配置选项,进一步定制异常处理的行为。 -
extractDescriptionAndOptionsFrom
:将上面两个函数结合起来,从传入的参数中同时提取描述信息和选项对象。
# Nest 中的其他异常类
刚才我们看过了源码,认识了 HttpException
是 Nest 中功能强大的异常基类,并且它本身就提供了一系列功能强大的方法,供用户 / 框架本身去处理任何异常。
那么,Nest 中有哪些异常是内置的并且继承于它,并且不用我们手动进行抛出的呢?我们可以去 @nest/common
中查看一下:
- BadRequestException (400):当客户端发送的请求有错误或不能被服务器处理时,通常会抛出这个异常。
- UnauthorizedException (401):当请求需要用户认证信息,但用户未提供或提供的认证信息无效时,会抛出这个异常。
- NotFoundException (404):当请求的资源不存在时,例如,请求了一个不存在的路由或资源,通常会抛出这个异常。
- ForbiddenException (403):当服务器理解请求但拒绝执行时,通常因为权限不足,会抛出这个异常。
- NotAcceptableException (406):当请求的资源的 MIME 类型与服务器端不兼容时,会抛出这个异常。
- RequestTimeoutException (408):当服务器希望关闭一个未使用或空闲的连接时,会抛出这个异常。
- ConflictException (409):当请求与服务器当前状态冲突时(例如,请求修改的资源已被更改),会抛出这个异常。
- GoneException (410):当请求的资源已被永久删除且不再可用时,会抛出这个异常。
- PayloadTooLargeException (413):当请求提交的数据大小超过服务器愿意或能够处理的限制时,会抛出这个异常。
- UnsupportedMediaTypeException (415):当请求消息的格式不受请求资源的支持时,会抛出这个异常。
- InternalServerErrorException (500):当服务器遇到意外情况,导致无法完成请求时,通常会抛出这个异常。
- NotImplementedException (501):当请求的功能服务器无法支持时,会抛出这个异常。
- BadGatewayException (502):当服务器作为网关或代理,从上游服务器收到无效响应时,会抛出这个异常。
- ServiceUnavailableException (503):当服务器不可用,通常是因为维护或超载时,会抛出这个异常。
可以看到,从 400~503 的基本异常都被包括在内了。
这些异常类基本无一例外的都是基于 HttpException
做了一层继承,并且修改了一下构造器能够传入的参数,然后内置了一个错误码。
顺带提一下,Nest 对错误码也做了层语义化的封装:
export declare enum HttpStatus { | |
CONTINUE = 100, | |
SWITCHING_PROTOCOLS = 101, | |
PROCESSING = 102, | |
EARLYHINTS = 103, | |
OK = 200, | |
CREATED = 201, | |
ACCEPTED = 202, | |
NON_AUTHORITATIVE_INFORMATION = 203, | |
NO_CONTENT = 204, | |
RESET_CONTENT = 205, | |
PARTIAL_CONTENT = 206, | |
AMBIGUOUS = 300, | |
MOVED_PERMANENTLY = 301, | |
FOUND = 302, | |
SEE_OTHER = 303, | |
NOT_MODIFIED = 304, | |
TEMPORARY_REDIRECT = 307, | |
PERMANENT_REDIRECT = 308, | |
BAD_REQUEST = 400, | |
UNAUTHORIZED = 401, | |
PAYMENT_REQUIRED = 402, | |
FORBIDDEN = 403, | |
NOT_FOUND = 404, | |
METHOD_NOT_ALLOWED = 405, | |
NOT_ACCEPTABLE = 406, | |
PROXY_AUTHENTICATION_REQUIRED = 407, | |
REQUEST_TIMEOUT = 408, | |
CONFLICT = 409, | |
GONE = 410, | |
LENGTH_REQUIRED = 411, | |
PRECONDITION_FAILED = 412, | |
PAYLOAD_TOO_LARGE = 413, | |
URI_TOO_LONG = 414, | |
UNSUPPORTED_MEDIA_TYPE = 415, | |
REQUESTED_RANGE_NOT_SATISFIABLE = 416, | |
EXPECTATION_FAILED = 417, | |
I_AM_A_TEAPOT = 418, | |
MISDIRECTED = 421, | |
UNPROCESSABLE_ENTITY = 422, | |
FAILED_DEPENDENCY = 424, | |
PRECONDITION_REQUIRED = 428, | |
TOO_MANY_REQUESTS = 429, | |
INTERNAL_SERVER_ERROR = 500, | |
NOT_IMPLEMENTED = 501, | |
BAD_GATEWAY = 502, | |
SERVICE_UNAVAILABLE = 503, | |
GATEWAY_TIMEOUT = 504, | |
HTTP_VERSION_NOT_SUPPORTED = 505, | |
} |
至此,Nest 对于错误异常的处理与封装程度可见一斑。
# Nest 本身如何捕获异常
# 内置全局异常过滤器
正如我在前言中所说,在 Nest 这门框架中除了你的自定义异常,其余的绝大多数情况是无需用户手动抛出异常的,Nest 遇到特定的错误会自动的抛出并将其捕获,之后再将错误消息返而不会造成框架的崩溃。而这对异常的自动捕获,归功于它在源码中内置的一个全局异常过滤器。
首先,Nest 本身的异常过滤器是由 ExceptionFilter
来定义的,它只包含了最基本的 catch
方法,负责处理异常并发送响应。
在 Nest 源码中,实际是定义了一个名为 BaseExceptionFilter
的类,继承了 ExceptionFilter
。这个类实现了它的接口,并定义了如何处理未被任何特定过滤器捕获的异常。 这就是 Nest 中预置的异常层,也是异常捕获层的最底层。
BaseExceptionFilter
的 catch
方法定义了当异常发生时如何处理它:
- 确定响应状态码:首先,过滤器会确定应该返回给客户端的 HTTP 状态码。如果异常是
HttpException
的一个实例,过滤器会使用异常中定义的状态码;如果不是,它会默认使用 500(内部服务器错误)。 正是这个步骤来确定服务器内部错误的。 - 构建响应对象:接下来,过滤器会构建要发送给客户端的响应对象。这通常包括错误消息和可能的其他相关信息。
- 发送响应:最后,过滤器会将构建好的响应对象发送给客户端。
# 错误捕获的层次结构
我们在新建 nest 项目,并且将其进行初始化的步骤中一定会包含自定义封装一个异常过滤器的步骤,方便我们开发的时候对异常进行自动捕获并获取具体的报错信息。我们可以定义多个异常过滤器,并通过优先级控制它们的处理顺序。 如果一个异常没有被任何自定义过滤器捕获,它最终会被全局异常过滤器捕获。
当然,我们也可以只单纯的定义作用在某个路由上的异常过滤器,专门用于测试那一个路由的逻辑。不过大部分的异常过滤器还是设置为全局。
# 如何自定义异常 + 异常过滤器
经过上述的分析,我们已经知道了 Nest 中的异常就是继承自 HttpException
、异常过滤器就是继承自 ExceptionFilter
。我们自定义异常、自定义异常过滤器无非就是继承一下他们俩,并且调用其父构造函数、实现一下原有的方法即可。
# 自定义异常
在这里我将给出我自己的自定义源码,仅供参考:
import { HttpException, HttpStatus } from "@nestjs/common"; | |
import { errorMessages } from "./errorList"; | |
class hanaError extends HttpException { | |
constructor(code: number, message?: string) { | |
super( | |
{ | |
code, | |
message: message || errorMessages.get(code) || "Unknown Error", | |
}, | |
HttpStatus.BAD_REQUEST | |
); | |
} | |
} | |
export { hanaError }; |
这个构造函数传入两个参数:
- code:错误码
- message:错误信息,可选
如果传入了自定义的信息,那么优先展示自定义的信息;如果没有传入自定义的信息,则根据错误码从预先定义好的错误码 / 错误消息映射 Map 中取出错误信息。如果找不到这个状态码对应的信息并且也没有传入 message,那么信息则为 "Unknown Error"。状态码则是 400,表示是用户自己的操作出现了问题。
# 自定义异常过滤器
import { | |
ArgumentsHost, | |
Catch, | |
ExceptionFilter, | |
HttpStatus, | |
} from "@nestjs/common"; | |
import { hanaError } from "./hanaError"; | |
import { Response } from "express"; | |
@Catch(hanaError) | |
export class HanaErrorFilter implements ExceptionFilter { | |
catch(exception: hanaError, host: ArgumentsHost) { | |
const response = host.switchToHttp().getResponse<Response>(); | |
response.status(HttpStatus.BAD_REQUEST).json({ | |
code: exception.getResponse()["code"], | |
message: exception.getResponse()["message"], | |
}); | |
} | |
} |
自定义的异常过滤器继承自 ExceptionFilter
类,并且实现 catch 方法。
catch 方法内部的第一个参数是错误实例本身; 第二个参数是 ArgumentsHost
,是 Nest 的一个非常重要的接口(之后会单独写一篇文章来解析它) 。
ArgumentsHost
的主要作用就是 允许你直接访问底层平台(如 Express 或 Fastify)的原始请求 ( request
) 和响应 ( response
) 对象,拿到它们的实际数据。 在这里,我调用 switchToHttp()
方法将其转为 Http 请求对象,然后调用 getResponse
并传入 Express 的 Response 泛型,将返回的数据转成 Express 中的返回类型后,把错误信息取出,调用其 json
方法重新返回给客户端。
定义完成后,需要在 app.module.ts
中对其进行全局异常过滤器的注册。
@Module({ | |
imports: [ | |
//... 一些模块的导入 | |
], | |
controllers: [], | |
providers: [ | |
// 全局错误过滤器(自定义异常,由用户主动抛出) | |
{ | |
provide: APP_FILTER, | |
useClass: HanaErrorFilter, | |
}, | |
], | |
}) | |
export class AppModule {} |
这样,我们就可以在特定的地方主动抛出 hanaError
,然后由我们自定义的异常过滤器捕获错误并输出了!!!
# 总结
由于是初次入门 Nest,想要一下子找到它的最佳实践是真的有点困难,其中该踩的坑还是得主动去踩一踩的。Nest 本身的异常处理机制已经十分的完善,我们可以充分的利用其 HttpException
以及 ExceptionFilter
去把异常玩出花来(bushi)。
顺带一提,我最近已经在开工我的项目:Picals 了,如果有同学感兴趣可以去看一看哦~!
之后打算就 Nest 中几个比较重要的数据处理器单独开几篇文章,附上源码解析仔细的研究一遍吧。