Nestjs中的異常過濾器

1.異常與異常過濾器


對於互聯網項目來說,沒有處理的異常對用戶而言是很困惑的,就像Windows 98電腦每次藍屏時彈出的無法理解的故障和錯誤代碼一樣令人無所適從。因此,對應用程序來說,需要捕捉並且處理大部分異常,並以易於閱讀和理解的形式反饋給用戶,NestJs項目內置了一個全局的異常過濾器(Exception filter)來處理所有的Http異常(HttpException),對於無法處理的異常,則向用戶返回“服務器內部錯誤”的JSON信息,這也是一般互聯網項目的標準做法。本文以Nestjs官方文檔為基礎並提供部分完善與補充。


<code>{
"statusCode":500,
"message":"Internal server error"
}/<code>


2. 異常的拋出與捕獲


互聯網項目中的異常,可能是無意或者有意產生的,前者往往來自程序設計中的bug,後者則多屬於針對特殊條件需要拋出的異常(比如,當沒有權限的用戶試圖訪問某個資源時)。在程序中直接拋出異常的方法有以下幾種(以官方文檔cats.controller.ts為例)。


要在NestJs中使用HttpException錯誤,可能需要從@nestjs/common模塊中導入HttpException,HttpStatus模塊,如果(下節)自定義了異常拋出模塊和異常捕捉模塊,還需要導入相應模塊並導入UseFilter裝飾器。


<code>import { Controller,Get } from '@nestjs/common';
import {HttpException,HttpStatus,UseFilters} from '@nestjs/common';
import {CatsService} from './cats.service';
import {Cat} from './interfaces/cat.interface';
import {CreateCatDto} from './dto/create-cat.dto';
import {Observable,of} from 'rxjs';
import {ForbiddenException} from '../shared/exceptions/forbidden.exception';
import { HttpExceptionFilter } from 'src/shared/filters/http-exception.filter';

@Controller('cats')
// @UseFilters(new HttpExceptionFilter) //異常過濾器可以作用域模塊,方法或者全局
export class CatsController {
constructor(private readonly catsService:CatsService){}

@Get()
//在下節定義完HttpExceptionFilter後,可通過該裝飾器調用
// @UseFilters(new HttpExceptionFilter)
findAll():Observable{

//註釋掉默認的返回代碼
//return of(this.catsService.findAll());

//可以直接通過throw new 拋出標準的HttpException錯誤
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);

//也可以自定義拋出錯誤的內容,注意最後的http標準代碼403

/*
throw new HttpException({
status:HttpStatus.FORBIDDEN,
error:'Custom fibidden msg: you cannot find all cats'
},403);
*/

在下節定義ForbiddenException後可以導入並拋出預定義的異常
//throw new ForbiddenException();
}
}
/<code>


Nestjs中的異常過濾器


3.自定義異常模塊與異常過濾器


對於頻繁使用的功能,在上節代碼中以硬編碼的方式寫入並不是一個好習慣,不利於程序的模塊和功能劃分,也可能額外引入不確定的問題。因此,在實際項目中,可以將自定義的異常模塊單獨放置,例如forbidden.exception.ts文件,在需要拋出該自定義的位置,引入並拋出該自定義模塊即可。


<code>import {HttpException,HttpStatus} from '@nestjs/common';
export class ForbiddenException extends HttpException{
constructor(){
super('Forbidden',HttpStatus.FORBIDDEN);
//super('Unauthorized',HttpStatus.UNAUTHORIZED);

}
}/<code>


和自定義異常模塊相比,異常過濾器的使用更為廣泛,通過異常過濾器,可以捕捉並處理不同類型的異常,並根據不同異常進行處理。官方文檔中提供了一個完整的異常過濾器示例。在github的[NEST-MEAN](https://github.com/nartc/nest-mean)項目中,也使用異常過濾器針對未授權的異常訪問進行過濾並處理。一個典型的http-exception.filter.ts文件,需要導入@nestjs/common模塊中的ExceptionFilter,Catch,ArgumentsHost以及HttpException等模塊。其中,ctx變量將請求上下文轉換為Http請求,並從中獲取與Express一致的Response和Request上下文。


@Catch()裝飾器中的HttpException參數限制了要捕獲的異常類型,如果要處理所有類型的異常,則應該保持@Catch()裝飾器的參數為空,並在代碼中處理捕獲的不同類型的異常。status變量從HttpException中讀取異常狀態。可以通過status來區分處理不同的異常。下文代碼段中註釋掉的代碼,用於判斷並處理未授權訪問的異常。


在需要使用異常過濾器的地方,使用裝飾器@UseFilters(new HttpExceptionFilter)以調用異常過濾器,經過如下的異常過濾器後,返回給用戶的異常結構成為如下的JSON格式。如果將forbidden.exceptions.ts中的HttpStatus從Forbidden修改為UNAUTHORIZED,並取消以下程序代碼中處理UNAUTHORIZED異常類型的部分代碼,則針對該類異常,返回的是自定義的'You do not have permission to access this resource`消息而不是默認的消息。


  • 返回的消息


<code>{
"statusCode": 403,
"timestamp": "2020-04-04T09:00:56.129Z",
"message": "Forbidden",
"path": "/cats"

}/<code>


  • 異常過濾器文件


<code>import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
//@Catch() //如果要捕獲任意類型的異常,則此處留空
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
//catch(exception:unknown, host:ArgumentsHost){//如果要捕獲任意類型的異常,則異常類型應為any或unkown
const ctx = host.switchToHttp();
const response = ctx.getResponse<response>();
const request = ctx.getRequest<request>();
const status = exception.getStatus();

//如果要捕獲的是任意類型的異常,則可能需要對此做如下判斷來區分不同類型的異常
/*
const exceptionStatus=
exception instanceof HttpException
? exception.getStatus()
:HttpStatus.INTERNAL_SERVER_ERROR;

*/

//如果要區分不同的異常狀態,則可能需要做類似如下判斷
/*
if (status=== HttpStatus.UNAUTHORIZED) {
if (typeof response !== 'string') {
response['message'] =
response['message'] || 'You do not have permission to access this resource';

}
}
*/
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
message: response['message'] || exception.message,
path: request.url,
});
}
}/<request>/<response>/<code>


4.異常過濾器的調用


異常過濾器可以作用域方法、模塊或者全局。在第二節的代碼中,@UseFilters()裝飾器位於在@Controller之後時,針對整個Controller使用異常過濾器,而位於@Get()之後時則僅針對Get方法使用異常過濾器。


如果要在全局調用異常過濾器,則需要在項目main.ts文件中使用useGlobalFilters方法,如下:


<code>import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './shared/filters/http-exception.filter';

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

await app.listen(3000);
}
bootstrap();/<code>


類似地,也可以在模塊中註冊全局過濾器,由於之前的useGlobalFilters方法不能注入依賴(不屬於任何模塊),因此需要註冊一個全局範圍的過濾器來為模塊設置過濾器。這需要在app.module.ts文件中做如下定義:


<code>import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './shared/filters/http-exception.filter';

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


5.系統內置的Http異常類型


NestJs系統內置的Http異常包括瞭如下類型。


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


6.ArgumentsHost與應用上下文


在異常過濾器中,用到了ArgumentsHost參數,這是NestJs內置的一個應用上下文類,用於快速在不同類型的網絡應用中切換(例如Http服務器、微服務或者WebSocket服務),上下文(Context)是程序設計中較難理解的概念之一,可以簡單的理解為應用程序的運行環境,例如(一個不太嚴謹的比喻),運行在Windows中的程序和MacOS中的程序,由於操作環境的不同,因此所包含的運行環境上下文就是不一樣的。


6.1 ArgumentsHost


前文HttpExceptionFilter的catch方法中,使用了ArgumentsHost。ArgumentsHost在NestJs中用來提供處理器(handler)的參數,例如在使用Express平臺(@nestjs/platform-express)時,ArgumentsHost就是一個數組,包含Express中的[request,response,next],前兩個作為對象(Object)而最後一個作為函數。在其他使用場景中,例如GraphQL時,ArguimentsHost則表示[root,args,context,info]數組。在NestJs使用慣例中,一般用host變量調用ArgumentsHost,即host:ArgumentsHost。


  • getType()
    在NestJs的守衛(guards),過濾器(filters)和攔截器(interceptors)中,會用到ArgumentsHost的getType()方法來獲取ArgumentsHost的類型,例如:


<code>if (host.getType() === 'http') {
// do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
// do something that is only important in the context of Microservice requests
} else if (host.getType<gqlcontexttype>() === 'graphql') {
// do something that is only important in the context of GraphQL requests
}/<gqlcontexttype>/<code>


  • getArgs()
    可以使用ArgumentsHost的getArgs()方法獲取所有參數:


<code>const [req, res, next] = host.getArgs();/<code>


  • getArgByIndex()//不推薦使用


使用getArgByIndex()通過索引獲取ArgumentsHost數組中的參數:


<code>const request=host.getArgByIndex(0);
const response=host.getArgByIndex(1);/<code>


  • 轉換參數上下文


通過Arguments的方法可以將上下文轉換為不同類型如Rpc,Http或者Websocket,例如前文示例中:


<code>const ctx = host.switchToHttp();
const request = ctx.getRequest<request>();
const response = ctx.getResponse<response>();/<response>/<request>/<code>


其他幾個轉換參數上下文的實例包括:


<code>/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;/<code>


  • WsArgumentsHost和RpcArgumentsHost
    與ArgumentsHost類似地,還有WsArgumentsHost和RpcArgumentsHost。官方文檔中也提供了簡單的示例


<code>export interface WsArgumentsHost {
/**
*返回data對象.
*/
getData(): T;
/**
* 返回客戶端client對象.
*/
getClient(): T;
}
export interface RpcArgumentsHost {
/**

* 返回data對象
*/
getData(): T;

/**
*返回上下文對象
*/
getContext(): T;
}
/<code>


6.2 ExecutionContext運行上下文


ExecutionContext類從ArgumentsHost類繼承而來,主要用於提供一些當前運行進程中更細節的參數,例如,在守衛中的canActivate()方法和攔截器中的intercept()方法。


<code>export interface ExecutionContext extends ArgumentsHost {
/**
*返回當前調用對象所屬的controller類
*/
getClass(): Type;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
* 返回管道中下一個將被調用的處理器對象的引用
*/

getHandler(): Function;
}
//官方示例
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
/<code>


  • 反射和元數據(Reflection and metadata)


NestJs提供了@SetMetadata()裝飾器方法來將用戶自定義的元數據添加到路徑處理器(handler)中。如下示例通過SetMetadata方法添加了roles鍵和['admin']值對。


<code>import {SetMetadata} from '@nestjs/common';

@Post()
//@Roles('admin') //官方推薦使用本行的裝飾器方法而不是下一行的直接採用SetMetadata方法
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}/<code>


但官方文檔不建議直接通過SetMetadata方法添加元數據,針對以上操作,可以通過如下方法新建一個roles裝飾器並在controller中導入,示例的roles.decorator.ts文件如下:


<code>import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);/<code>


要訪問自定義的元數據,需要從@nestjs/core中導入並使用Reflector.例如在roles.guard.ts中:


<code>@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}

//有了如上定義之後,就可以用get方法讀取自定義元數據
//const roles = this.reflector.get<string>('roles', context.getHandler());

//自定義元數據也可以應用在整個controller中,這時需要使用context.getClass()來調用
/*
**在cats.controller.ts中定義
@Roles('admin')
@Controller('cats')
export class CatsController {}
*/
/*
**在roles.guard.ts中調用
const roles = this.reflector.get<string>('roles', context.getClass());
*//<string>/<string>/<code>


分享到:


相關文章: