為什么會(huì)出現(xiàn)異常?
在 UsersController 我們有如下代碼:
@Get(':id')
async findOne(@Param() params): Promise<User> {
return await this.usersService.findOne(params.id);
}
我們希望接收一個(gè)以 Get 方式提交的用戶 id 值,并且期望它永遠(yuǎn)是 number 類型的。
現(xiàn)在我們的程序直接將 params.id 傳入了 service 層,并且期望 service 層能夠幫我們找到對(duì)應(yīng)的用戶并返回給客戶端。
在這里直接使用了客戶端上傳的 id,并沒有對(duì)參數(shù)的類型做校驗(yàn),如果 id 不是 number 類型,那么將可能會(huì)引發(fā)異常。
如何處理參數(shù)錯(cuò)誤
如果我們對(duì) id 進(jìn)行校驗(yàn),然后沒有得到預(yù)期的值,那么我們該如何返回錯(cuò)誤信息呢?
一種方法是使用 @Res 自己處理 HTTP 響應(yīng),將 findOne 改寫如下:
@Get(':id')
async findOne(@Res() res, @Param() params): Promise<User> {
let id = parseInt(params.id);
if(isNaN(id) || typeof id !== 'number' || id <= 0) {
return res.status(HttpStatus.BAD_REQUEST).send({
errorCode: 10001,
errorMessage: '用戶編號(hào)錯(cuò)誤'
});
}
return res.status(HttpStatus.OK).send({
errorCode: 0,
errorMessage: '請(qǐng)求成功',
data: await this.usersService.findOne(id)
});
}
這種方式的優(yōu)點(diǎn)是非常的靈活,我們可以完全控制 HTTP 響應(yīng),但是它讓代碼變得非常冗余而且耦合度太高。
聰明的軟件工程師就在思考如何用一種統(tǒng)一的方式來處理這些異常和錯(cuò)誤。
AOP
前面說過一個(gè)復(fù)雜系統(tǒng)需要自頂向下的縱向劃分成多個(gè)模塊,但是我們發(fā)現(xiàn)每個(gè)模塊都要處理類似上面的異常和錯(cuò)誤,這個(gè)時(shí)候統(tǒng)一的異常處理層將作為一個(gè)橫向的切面貫穿我們系統(tǒng)中的所有模塊,這種編程方式,我們就稱之為 面向切面編程 (Aspect Oriented Programming 簡稱:AOP)
Nest 內(nèi)置了全局異常處理層
一旦應(yīng)用程序拋出異常,Nest 便會(huì)自動(dòng)捕獲這些異常并給出 JSON 形式的響應(yīng)。
如果訪問 http://127.0.0.1:3000/member ,這將拋出一個(gè) HTTP Exception,并得到如下輸出:
{
"statusCode":404,
"error":"Not Found",
"message":"Cannot GET /member"
}
如果是未知的異常類型,則會(huì)是下面這樣:
{
"statusCode": 500,
"message": "Internal server error"
}
HttpException
Nest 內(nèi)置了 HttpException 類用來處理異常,我們改寫上面的例子:
@Get(':id')
async findOne(@Param() params): Promise<User> {
let id = parseInt(params.id);
if(isNaN(id) || typeof id !== 'number' || id <= 0) {
throw new HttpException('用戶編號(hào)錯(cuò)誤', HttpStatus.BAD_REQUEST);
}
return await this.usersService.findOne(id);
}
現(xiàn)在我們的代碼精簡了不少,但是有一個(gè)問題,我們自定義的錯(cuò)誤狀態(tài)碼沒有了,怎么辦?
推薦的做法是創(chuàng)建自己的異常類并繼承 HttpException 或它的派生類,然后使用 異常過濾器 來自定義響應(yīng)格式。
Nest 幫我們內(nèi)置了很多異常的模板:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
使用異常過濾器實(shí)現(xiàn)統(tǒng)一錯(cuò)誤處理橫切面
首先來看如何創(chuàng)建一個(gè)異常過濾器,并捕獲HttpException,然后設(shè)置我們自己的響應(yīng)邏輯:
$ nest g f common/filters/http-exception
CREATE /src/common/filters/http-exception/http-exception.filter.ts (189 bytes)
把 http-exception/ 這一層目錄去掉,修改默認(rèn)生成的代碼,如下:
src/common/filters/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus()
response
.status(status)
.json({
statusCode: status,
date: new Date().toLocaleDateString(),
path: request.url,
});
}
}
異常過濾器就是實(shí)現(xiàn)了 ExceptionFilter 接口,并且用 @Catch 裝飾器修飾的類。
我們還需要修改 main.ts,在全局范圍內(nèi)使用我們的異常過濾器:
import { NestFactory } from '@nestjs/core';
import { AppModule } from 'app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
現(xiàn)在訪問 http://127.0.0.1:3000/users/exception,我們的異常過濾器起作用了:
{
"statusCode":400,
"date":"2018-7-31",
"path":"/users/exception"
}
返回合適的HTTP狀態(tài)碼
為每一次的響應(yīng)返回合適的HTTP狀態(tài)碼,好的響應(yīng)應(yīng)該使用如下的狀態(tài)碼:
-
200:GET請(qǐng)求成功, 及DELETE或PATCH同步請(qǐng)求完成,或者PUT同步更新一個(gè)已存在的資源 -
201:POST同步請(qǐng)求完成,或者PUT同步創(chuàng)建一個(gè)新的資源 -
202:POST,PUT,DELETE, 或PATCH請(qǐng)求接收,將被異步處理 -
206:GET請(qǐng)求成功, 但是只返回一部分
使用身份認(rèn)證(authentication)和授權(quán)(authorization)錯(cuò)誤碼時(shí)需要注意:
-
401 Unauthorized: 用戶未認(rèn)證,請(qǐng)求失敗 -
403 Forbidden: 用戶無權(quán)限訪問該資源,請(qǐng)求失敗
當(dāng)用戶請(qǐng)求錯(cuò)誤時(shí),提供合適的狀態(tài)碼可以提供額外的信息:
-
422 Unprocessable Entity: 請(qǐng)求被服務(wù)器正確解析,但是包含無效字段 -
429 Too Many Requests: 因?yàn)樵L問頻繁,你已經(jīng)被限制訪問,稍后重試 -
500 Internal Server Error: 服務(wù)器錯(cuò)誤,確認(rèn)狀態(tài)并報(bào)告問題
業(yè)務(wù)狀態(tài)碼
我們也想返回像微信公眾平臺(tái)一樣的業(yè)務(wù)狀態(tài)碼時(shí)該怎么辦?

這個(gè)時(shí)候我們需要定義自己的異常類,并用全局異常過濾器捕獲它然后更改響應(yīng),首先我們需要規(guī)劃好自己的業(yè)務(wù)狀態(tài)碼,在 common 目錄中創(chuàng)建 enums 目錄并創(chuàng)建 api-error-code.enum.ts:
src/common/enums/api-error-code.enum.ts
export enum ApiErrorCode {
TIMEOUT = -1, // 系統(tǒng)繁忙
SUCCESS = 0, // 成功
USER_ID_INVALID = 10001 // 用戶id無效
}
定義一個(gè) ApiException 繼承自 HttpException:
$ nest g e common/exceptions/api
CREATE /src/common/exceptions/api/api.exception.ts (193 bytes)
把 api/ 這一層目錄去掉,修改默認(rèn)的代碼,如下:
src/common/exceptions/api.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { ApiErrorCode } from '../enums/api-error-code.enum';
export class ApiException extends HttpException {
private errorMessage: string;
private errorCode: ApiErrorCode;
constructor(errorMessage: string, errorCode: ApiErrorCode, statusCode: HttpStatus) {
super(errorMessage, statusCode);
this.errorMessage = errorMessage;
this.errorCode = errorCode;
}
getErrorCode(): ApiErrorCode {
return this.errorCode;
}
getErrorMessage(): string {
return this.errorMessage;
}
}
我們現(xiàn)在應(yīng)該讓 UsersController 中的 findOne 拋出 ApiException:
@Get(':id')
async findOne(@Param() params): Promise<User> {
let id = parseInt(params.id);
if(isNaN(id) || typeof id !== 'number' || id <= 0) {
throw new ApiException('用戶ID無效', ApiErrorCode.USER_ID_INVALID, HttpStatus.BAD_REQUEST);
}
return await this.usersService.findOne(id);
}
這樣還沒完,我們的全局過濾器并沒有加入識(shí)別ApiException的邏輯,現(xiàn)在修改全局過濾器的邏輯:
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { ApiException } from '../exceptions/api.exception';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus()
if (exception instanceof ApiException) {
response
.status(status)
.json({
errorCode: exception.getErrorCode(),
errorMessage: exception.getErrorMessage(),
date: new Date().toLocaleDateString(),
path: request.url,
});
} else {
response
.status(status)
.json({
statusCode: status,
date: new Date().toLocaleDateString(),
path: request.url,
});
}
}
}
再進(jìn)行一次錯(cuò)誤的訪問 http://127.0.0.1:3000/users/exception, 得到下面的響應(yīng):
{
"errorCode":10001,
"errorMessage":"用戶ID無效",
"date":"2018-7-31",
"path":"/users/exception"
}
看起來不錯(cuò),我們不僅有正確的 HTTP 響應(yīng)碼還有了自定義的業(yè)務(wù)狀態(tài)碼!
更好的處理方式是創(chuàng)建 UsersException 繼承自 ApiException。