5、Nest.js 中的異常處理和AOP編程

為什么會(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)求成功, 及DELETEPATCH同步請(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í)該怎么辦?


image.png

這個(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。

上一篇:4、Nest.js 中的模塊化設(shè)計(jì)
下一篇:6、Nest.js 中的管道與驗(yàn)證器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容