NestJS 異步過(guò)濾器

2023-09-08 10:19 更新

內(nèi)置的異常層負(fù)責(zé)處理整個(gè)應(yīng)用程序中的所有拋出的異常。當(dāng)捕獲到未處理的異常時(shí),最終用戶將收到友好的響應(yīng)。

8

開箱即用,此操作由內(nèi)置的全局異常過(guò)濾器執(zhí)行,該過(guò)濾器處理類型 HttpException(及其子類)的異常。每個(gè)發(fā)生的異常都由全局異常過(guò)濾器處理, 當(dāng)這個(gè)異常無(wú)法被識(shí)別時(shí) (既不是 HttpException 也不是繼承的類 HttpException ) , 用戶將收到以下 JSON 響應(yīng):

{
    "statusCode": 500,
    "message": "Internal server error"
}

引發(fā)標(biāo)準(zhǔn)異常

Nest提供了一個(gè)內(nèi)置的 HttpException 類,它從 @nestjs/common 包中導(dǎo)入。對(duì)于典型的基于HTTP REST/GraphQL API的應(yīng)用程序,最佳實(shí)踐是在發(fā)生某些錯(cuò)誤情況時(shí)發(fā)送標(biāo)準(zhǔn)HTTP響應(yīng)對(duì)象。

例如,在 CatsController中,我們有一個(gè) findAll() 方法(一個(gè)GET路由處理程序)。讓我們假設(shè)這個(gè)路由處理程序由于某種原因拋出了一個(gè)異常。 為了說(shuō)明這一點(diǎn),我們將對(duì)其進(jìn)行如下硬編碼:

cats.controller.ts
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

我們?cè)谶@里使用了 HttpStatus 。它是從 @nestjs/common 包導(dǎo)入的輔助枚舉器。

現(xiàn)在當(dāng)客戶端調(diào)用這個(gè)端點(diǎn)時(shí),響應(yīng)如下所示:

{
    "statusCode": 403,
    "message": "Forbidden"
}

HttpException 構(gòu)造函數(shù)有兩個(gè)必要的參數(shù)來(lái)決定響應(yīng):

  • response 參數(shù)定義 JSON 響應(yīng)體。它可以是 string 或 object,如下所述。
  • status參數(shù)定義HTTP狀態(tài)代碼。

默認(rèn)情況下,JSON 響應(yīng)主體包含兩個(gè)屬性:

  • statusCode:默認(rèn)為 status 參數(shù)中提供的 HTTP 狀態(tài)代碼
  • message:基于狀態(tài)的 HTTP 錯(cuò)誤的簡(jiǎn)短描述

僅覆蓋 JSON 響應(yīng)主體的消息部分,請(qǐng)?jiān)?nbsp;response參數(shù)中提供一個(gè) string。

要覆蓋整個(gè) JSON 響應(yīng)主體,請(qǐng)?jiān)趓esponse 參數(shù)中傳遞一個(gè)object。 Nest將序列化對(duì)象,并將其作為JSON 響應(yīng)返回。

第二個(gè)構(gòu)造函數(shù)參數(shù)-status-是有效的 HTTP 狀態(tài)代碼。 最佳實(shí)踐是使用從@nestjs/common導(dǎo)入的 HttpStatus枚舉。

這是一個(gè)覆蓋整個(gè)響應(yīng)正文的示例:

cats.controller.ts
@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}

使用上面的代碼,響應(yīng)如下所示:

{
  "status": 403,
  "error": "This is a custom message"
}

自定義異常

在許多情況下,您無(wú)需編寫自定義異常,而可以使用內(nèi)置的 Nest HTTP異常,如下一節(jié)所述。 如果確實(shí)需要?jiǎng)?chuàng)建自定義的異常,則最好創(chuàng)建自己的異常層次結(jié)構(gòu),其中自定義異常繼承自 HttpException 基類。 使用這種方法,Nest可以識(shí)別您的異常,并自動(dòng)處理錯(cuò)誤響應(yīng)。 讓我們實(shí)現(xiàn)這樣一個(gè)自定義異常:

forbidden.exception.ts
export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

由于 ForbiddenException 擴(kuò)展了基礎(chǔ) HttpException,它將和核心異常處理程序一起工作,因此我們可以在 findAll()方法中使用它。

cats.controller.ts
@Get()
async findAll() {
  throw new ForbiddenException();
}

內(nèi)置HTTP異常

為了減少樣板代碼,Nest 提供了一系列繼承自核心異常 HttpException 的可用異常。所有這些都可以在 @nestjs/common包中找到:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

異常過(guò)濾器

雖然基本(內(nèi)置)異常過(guò)濾器可以為您自動(dòng)處理許多情況,但有時(shí)您可能希望對(duì)異常層擁有完全控制權(quán),例如,您可能希望基于某些動(dòng)態(tài)因素添加日志記錄或使用不同的 JSON 模式。 異常過(guò)濾器正是為此目的而設(shè)計(jì)的。 它們使您可以控制精確的控制流以及將響應(yīng)的內(nèi)容發(fā)送回客戶端。

讓我們創(chuàng)建一個(gè)異常過(guò)濾器,它負(fù)責(zé)捕獲作為HttpException類實(shí)例的異常,并為它們?cè)O(shè)置自定義響應(yīng)邏輯。為此,我們需要訪問(wèn)底層平臺(tái) Request和 Response。我們將訪問(wèn)Request對(duì)象,以便提取原始 url并將其包含在日志信息中。我們將使用 Response.json()方法,使用 Response對(duì)象直接控制發(fā)送的響應(yīng)。

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,
      });
  }
}

所有異常過(guò)濾器都應(yīng)該實(shí)現(xiàn)通用的 ExceptionFilter<T> 接口。它需要你使用有效簽名提供 catch(exception: T, host: ArgumentsHost)方法。T 表示異常的類型。

@Catch() 裝飾器綁定所需的元數(shù)據(jù)到異常過(guò)濾器上。它告訴 Nest這個(gè)特定的過(guò)濾器正在尋找 HttpException 而不是其他的。在實(shí)踐中,@Catch() 可以傳遞多個(gè)參數(shù),所以你可以通過(guò)逗號(hào)分隔來(lái)為多個(gè)類型的異常設(shè)置過(guò)濾器。

參數(shù)主機(jī)

讓我們看一下該 catch() 方法的參數(shù)。該 exception 參數(shù)是當(dāng)前正在處理的異常對(duì)象。該host參數(shù)是一個(gè) ArgumentsHost 對(duì)象。 ArgumentsHost 是一個(gè)功能強(qiáng)大的實(shí)用程序?qū)ο螅覀儗⒃趹?yīng)用上下文章節(jié) *中進(jìn)一步進(jìn)行研究。在此代碼示例中,我們使用它來(lái)獲取對(duì) Request 和 Response 對(duì)象的引用,這些對(duì)象被傳遞給原始請(qǐng)求處理程序(在異常發(fā)生的控制器中)。在此代碼示例中,我們使用了一些輔助方法 ArgumentsHost 來(lái)獲取所需的 Request 和 Response 對(duì)象。ArgumentsHost 在此處了解更多信息。

之所以如此抽象,是因?yàn)樗?nbsp;ArgumentsHost 可以在所有上下文中使用(例如,我們現(xiàn)在正在使用的 HTTP 服務(wù)器上下文,以及微服務(wù)和 WebSocket )。在應(yīng)用上下文章節(jié)中,我們將看到如何使用 ArgumentsHost 及其輔助函數(shù)訪問(wèn)任何應(yīng)用上下文中相應(yīng)的底層參數(shù)。這將使我們能夠編寫可在所有上下文中運(yùn)行的通用異常過(guò)濾器。

綁定過(guò)濾器

讓我們將 HttpExceptionFilter 綁定到 CatsController 的 create() 方法上。

cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

@UseFilters() 裝飾器需要從 @nestjs/common 包導(dǎo)入。

我們?cè)谶@里使用了 @UseFilters() 裝飾器。和 @Catch()裝飾器類似,它可以使用單個(gè)過(guò)濾器實(shí)例,也可以使用逗號(hào)分隔的過(guò)濾器實(shí)例列表。 我們創(chuàng)建了 HttpExceptionFilter 的實(shí)例。另一種可用的方式是傳遞類(不是實(shí)例),讓框架承擔(dān)實(shí)例化責(zé)任并啟用依賴注入。

cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

盡可能使用類而不是實(shí)例。由于 Nest 可以輕松地在整個(gè)模塊中重復(fù)使用同一類的實(shí)例,因此可以減少內(nèi)存使用。

在上面的示例中,HttpExceptionFilter 僅應(yīng)用于單個(gè) create() 路由處理程序,使其成為方法范圍的。 異常過(guò)濾器的作用域可以劃分為不同的級(jí)別:方法范圍,控制器范圍或全局范圍。 例如,要將過(guò)濾器設(shè)置為控制器作用域,您可以執(zhí)行以下操作:

cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

此結(jié)構(gòu)為 CatsController 中的每個(gè)路由處理程序設(shè)置 HttpExceptionFilter。

要?jiǎng)?chuàng)建一個(gè)全局范圍的過(guò)濾器,您需要執(zhí)行以下操作:

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

該 useGlobalFilters() 方法不會(huì)為網(wǎng)關(guān)和混合應(yīng)用程序設(shè)置過(guò)濾器。

全局過(guò)濾器用于整個(gè)應(yīng)用程序、每個(gè)控制器和每個(gè)路由處理程序。就依賴注入而言,從任何模塊外部注冊(cè)的全局過(guò)濾器(使用上面示例中的 useGlobalFilters())不能注入依賴,因?yàn)樗鼈儾粚儆谌魏文K。為了解決這個(gè)問(wèn)題,你可以注冊(cè)一個(gè)全局范圍的過(guò)濾器直接為任何模塊設(shè)置過(guò)濾器:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

當(dāng)使用此方法對(duì)過(guò)濾器執(zhí)行依賴注入時(shí),請(qǐng)注意,無(wú)論采用哪種結(jié)構(gòu)的模塊,過(guò)濾器實(shí)際上都是全局的。 應(yīng)該在哪里做? 選擇定義了過(guò)濾器(以上示例中為 HttpExceptionFilter)的模塊。 同樣,useClass不是處理自定義提供程序注冊(cè)的唯一方法。 在這里了解更多。

您可以根據(jù)需要添加任意數(shù)量的過(guò)濾器;只需將每個(gè)組件添加到 providers(提供者)數(shù)組。

捕獲異常

為了捕獲每一個(gè)未處理的異常(不管異常類型如何),將 @Catch() 裝飾器的參數(shù)列表設(shè)為空,例如 @Catch()。

any-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();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

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

在上面的示例中,過(guò)濾器將捕獲拋出的每個(gè)異常,而不管其類型(類)如何。

繼承

通常,您將創(chuàng)建完全定制的異常過(guò)濾器,以滿足您的應(yīng)用程序需求。如果您希望重用已經(jīng)實(shí)現(xiàn)的核心異常過(guò)濾器,并基于某些因素重寫行為,請(qǐng)看下面的例子。

為了將異常處理委托給基礎(chǔ)過(guò)濾器,需要繼承 BaseExceptionFilter 并調(diào)用繼承的 catch() 方法。

all-exceptions.filter.ts
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

繼承自基礎(chǔ)類的過(guò)濾器必須由框架本身實(shí)例化(不要使用 new 關(guān)鍵字手動(dòng)創(chuàng)建實(shí)例)

上面的實(shí)現(xiàn)只是一個(gè)演示。擴(kuò)展異常過(guò)濾器的實(shí)現(xiàn)將包括定制的業(yè)務(wù)邏輯(例如,處理各種情況)。

全局過(guò)濾器可以擴(kuò)展基本過(guò)濾器。這可以通過(guò)兩種方式來(lái)實(shí)現(xiàn)。

您可以通過(guò)注入 HttpServer 來(lái)使用繼承自基礎(chǔ)類的全局過(guò)濾器。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

第二種方法是使用 APP_FILTER token。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)