內置的異常層負責處理整個應用程序中的所有拋出的異常。當捕獲到未處理的異常時,最終用戶將收到友好的響應。
開箱即用,此操作由內置的全局異常過濾器執(zhí)行,該過濾器處理類型 HttpException(及其子類)的異常。每個發(fā)生的異常都由全局異常過濾器處理, 當這個異常無法被識別時 (既不是 HttpException 也不是繼承的類 HttpException ) , 用戶將收到以下 JSON 響應:
{
"statusCode": 500,
"message": "Internal server error"
}
Nest提供了一個內置的 HttpException 類,它從 @nestjs/common 包中導入。對于典型的基于HTTP REST/GraphQL API的應用程序,最佳實踐是在發(fā)生某些錯誤情況時發(fā)送標準HTTP響應對象。
例如,在 CatsController中,我們有一個 findAll() 方法(一個GET路由處理程序)。讓我們假設這個路由處理程序由于某種原因拋出了一個異常。 為了說明這一點,我們將對其進行如下硬編碼:
cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
我們在這里使用了 HttpStatus 。它是從 @nestjs/common 包導入的輔助枚舉器。
現(xiàn)在當客戶端調用這個端點時,響應如下所示:
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException 構造函數(shù)有兩個必要的參數(shù)來決定響應:
默認情況下,JSON 響應主體包含兩個屬性:
僅覆蓋 JSON 響應主體的消息部分,請在 response參數(shù)中提供一個 string。
要覆蓋整個 JSON 響應主體,請在response 參數(shù)中傳遞一個object。 Nest將序列化對象,并將其作為JSON 響應返回。
第二個構造函數(shù)參數(shù)-status-是有效的 HTTP 狀態(tài)代碼。 最佳實踐是使用從@nestjs/common導入的 HttpStatus枚舉。
這是一個覆蓋整個響應正文的示例:
cats.controller.ts
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}
使用上面的代碼,響應如下所示:
{
"status": 403,
"error": "This is a custom message"
}
在許多情況下,您無需編寫自定義異常,而可以使用內置的 Nest HTTP異常,如下一節(jié)所述。 如果確實需要創(chuàng)建自定義的異常,則最好創(chuàng)建自己的異常層次結構,其中自定義異常繼承自 HttpException 基類。 使用這種方法,Nest可以識別您的異常,并自動處理錯誤響應。 讓我們實現(xiàn)這樣一個自定義異常:
forbidden.exception.ts
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
由于 ForbiddenException 擴展了基礎 HttpException,它將和核心異常處理程序一起工作,因此我們可以在 findAll()方法中使用它。
cats.controller.ts
@Get()
async findAll() {
throw new ForbiddenException();
}
為了減少樣板代碼,Nest 提供了一系列繼承自核心異常 HttpException 的可用異常。所有這些都可以在 @nestjs/common包中找到:
雖然基本(內置)異常過濾器可以為您自動處理許多情況,但有時您可能希望對異常層擁有完全控制權,例如,您可能希望基于某些動態(tài)因素添加日志記錄或使用不同的 JSON 模式。 異常過濾器正是為此目的而設計的。 它們使您可以控制精確的控制流以及將響應的內容發(fā)送回客戶端。
讓我們創(chuàng)建一個異常過濾器,它負責捕獲作為HttpException類實例的異常,并為它們設置自定義響應邏輯。為此,我們需要訪問底層平臺 Request和 Response。我們將訪問Request對象,以便提取原始 url并將其包含在日志信息中。我們將使用 Response.json()方法,使用 Response對象直接控制發(fā)送的響應。
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,
});
}
}
所有異常過濾器都應該實現(xiàn)通用的 ExceptionFilter<T> 接口。它需要你使用有效簽名提供 catch(exception: T, host: ArgumentsHost)方法。T 表示異常的類型。
@Catch() 裝飾器綁定所需的元數(shù)據(jù)到異常過濾器上。它告訴 Nest這個特定的過濾器正在尋找 HttpException 而不是其他的。在實踐中,@Catch() 可以傳遞多個參數(shù),所以你可以通過逗號分隔來為多個類型的異常設置過濾器。
讓我們看一下該 catch() 方法的參數(shù)。該 exception 參數(shù)是當前正在處理的異常對象。該host參數(shù)是一個 ArgumentsHost 對象。 ArgumentsHost 是一個功能強大的實用程序對象,我們將在應用上下文章節(jié) *中進一步進行研究。在此代碼示例中,我們使用它來獲取對 Request 和 Response 對象的引用,這些對象被傳遞給原始請求處理程序(在異常發(fā)生的控制器中)。在此代碼示例中,我們使用了一些輔助方法 ArgumentsHost 來獲取所需的 Request 和 Response 對象。ArgumentsHost 在此處了解更多信息。
之所以如此抽象,是因為它 ArgumentsHost 可以在所有上下文中使用(例如,我們現(xiàn)在正在使用的 HTTP 服務器上下文,以及微服務和 WebSocket )。在應用上下文章節(jié)中,我們將看到如何使用 ArgumentsHost 及其輔助函數(shù)訪問任何應用上下文中相應的底層參數(shù)。這將使我們能夠編寫可在所有上下文中運行的通用異常過濾器。
讓我們將 HttpExceptionFilter 綁定到 CatsController 的 create() 方法上。
cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@UseFilters() 裝飾器需要從 @nestjs/common 包導入。
我們在這里使用了 @UseFilters() 裝飾器。和 @Catch()裝飾器類似,它可以使用單個過濾器實例,也可以使用逗號分隔的過濾器實例列表。 我們創(chuàng)建了 HttpExceptionFilter 的實例。另一種可用的方式是傳遞類(不是實例),讓框架承擔實例化責任并啟用依賴注入。
cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
盡可能使用類而不是實例。由于 Nest 可以輕松地在整個模塊中重復使用同一類的實例,因此可以減少內存使用。
在上面的示例中,HttpExceptionFilter 僅應用于單個 create() 路由處理程序,使其成為方法范圍的。 異常過濾器的作用域可以劃分為不同的級別:方法范圍,控制器范圍或全局范圍。 例如,要將過濾器設置為控制器作用域,您可以執(zhí)行以下操作:
cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
此結構為 CatsController 中的每個路由處理程序設置 HttpExceptionFilter。
要創(chuàng)建一個全局范圍的過濾器,您需要執(zhí)行以下操作:
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
該 useGlobalFilters() 方法不會為網(wǎng)關和混合應用程序設置過濾器。
全局過濾器用于整個應用程序、每個控制器和每個路由處理程序。就依賴注入而言,從任何模塊外部注冊的全局過濾器(使用上面示例中的 useGlobalFilters())不能注入依賴,因為它們不屬于任何模塊。為了解決這個問題,你可以注冊一個全局范圍的過濾器直接為任何模塊設置過濾器:
app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
當使用此方法對過濾器執(zhí)行依賴注入時,請注意,無論采用哪種結構的模塊,過濾器實際上都是全局的。 應該在哪里做? 選擇定義了過濾器(以上示例中為 HttpExceptionFilter)的模塊。 同樣,useClass不是處理自定義提供程序注冊的唯一方法。 在這里了解更多。
您可以根據(jù)需要添加任意數(shù)量的過濾器;只需將每個組件添加到 providers(提供者)數(shù)組。
為了捕獲每一個未處理的異常(不管異常類型如何),將 @Catch() 裝飾器的參數(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,
});
}
}
在上面的示例中,過濾器將捕獲拋出的每個異常,而不管其類型(類)如何。
通常,您將創(chuàng)建完全定制的異常過濾器,以滿足您的應用程序需求。如果您希望重用已經實現(xiàn)的核心異常過濾器,并基于某些因素重寫行為,請看下面的例子。
為了將異常處理委托給基礎過濾器,需要繼承 BaseExceptionFilter 并調用繼承的 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);
}
}
繼承自基礎類的過濾器必須由框架本身實例化(不要使用 new 關鍵字手動創(chuàng)建實例)
上面的實現(xiàn)只是一個演示。擴展異常過濾器的實現(xiàn)將包括定制的業(yè)務邏輯(例如,處理各種情況)。
全局過濾器可以擴展基本過濾器。這可以通過兩種方式來實現(xiàn)。
您可以通過注入 HttpServer 來使用繼承自基礎類的全局過濾器。
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。
更多建議: