NestJSの例外フィルタ
NestJSには未処理のすべての例外を処理できる例外フィルタがデフォルトで組み込まれています。アプリのコードで処理されない例外が発生した場合、このフィルタが例外をキャッチしてレスポンスを自動送信してくれます。
以下の処理は、
HttpException
(およびそのサブクラス)型の例外を処理する、組み込みの例外フィルタによって、すぐに実行されます。例外が認識できない場合、組み込みの例外フィルタは次のようなデフォルトのJSONレスポンスを返します。
{
"statusCode": 500,
"message": "Internal server error"
}
メモ
グローバルの例外フィルタは
http-errors
ライブラリを部分的にサポートしています。基本的に、statusCode
およびmessage
プロパティを含む例外が投げられると、適切に値が設定されてレスポンスとして返されます。認識できない例外の場合はデフォルトの
InternalServerErrorException
が返されます。NestJSでは、
@nestjs/common
パッケージからHttpException
クラスを用意しています。典型的なREST APIやGraphQL APIベースのアプリでは、特定のエラーが発生した場合、標準的なHTTPレスポンスオブジェクトを送信します。例えば、CatsControllerにfindAll()メソッドがあるとします。このルートハンドラが何らかの理由で例外を投げたとしましょう。これを実装するために、以下のようにプログラムを書きます。
cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
メモ
上記のプログラムでは
HttpStatus
を使いました。これは、@nestjs/common
からインポートされたヘルパー列挙型です。クライアントが以下のようなエンドポイントを呼び出すと、次のようなレスポンスが返されます。
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
のコンストラクタは、レスポンスを決める2つの必須の引数を取ります。response
:JSONのレスポンスを定義。型はstring
あるいはobject
status
:HTTPステータスコードを定義。
デフォルトでは、JSONレスポンスボディは2つのプロパティを含む。
statusCode
:デフォルトは、status
引数で指定されたHTTPステータスコード。message
:statusに基づくHTTPエラーの短い説明。
JSONレスポンスの
message
部分を上書きするためには、response
引数に文字列を指定します。JSONレスポンスボディの全体を上書きするためには、response
引数にオブジェクトを渡します。NestJSはそ のオブジェクトをシリアライズして、JSONレスポンスボディとして返します。コンストラクタの2番目の引数
status
には、有効なHTTPステータスコードを指定します。@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"
}
ほとんどの場合、独自の例外を作成する必要はありません。以下のセクションで説明する組み込みのNestJSの
HttpException
を使えます。カスタムの例外を作成する必要がある場合は、独自の例外階層を作り、カスタム例外は基本HttpException
クラスを継承するようにするといいでしょう。この方法では、NestJSが例外を認識し、レスポンスを自動的に処理します。以下のようにカスタムの例外を実装してみましょう。
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();
}
NestJSではベースとなる
HttpException
を継承する標準の例外を複数提供します。これらは@nestjs/common
パッケージから公開されており、もっとも一般的なHttpException
の多くを記述しています。BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException
基本的な例外フィルタは多くのケースを自動的に処理してくれますが、例外フィルタを完全に制御したい場合もあるでしょう。
たとえば、ロギングを追加したり、動的な要素に基づいて別のJSONスキーマを使用したりというようなことです。例外フィルタはこの目的で設計されました。
このフィルタにより、制御の流れやクライアントに返すレスポンスの内容を正確に制御できます。
HttpException
クラスのインスタンスである例外をキャッチし、その例外に対するカスタムのレスポンスのロジックを実装するための例外フィルタを作ってみましょう。これを実施するためには、基盤となるプラットフォーム
Request
とResponse
オブジェクトにアクセスする必要があります。Request
オブジェクトにアクセスすることで、もとのURLを引き出してそれをロギング情報に含められます。Response
オブジェクトを活用して、response.json()
メソッドを使い送信されるレスポンスを直接制御できます。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,
});
}
}
メモ
すべての例外フィルタは、汎用的な
Excepton<T>
インターフェイスを実装する必要があります。これは、その示されたシグネチャを持つcatch(exception: T, host:ArgumentHost)
メソッドを提供することを要求しています。T
は例外の型です。Catch(HttpException)
デコレータは、必要なメタデータを例外フィルタに関連付け、このフィルタはHttpException
型の例外を探し、それ以外のものは探さないということをNestJSに知らせます。@Catch()
デコレータは、単一のパラメータあるいはコンマ区切りのリストを受け取ります。これによって、一度に複数の種類の例外を処理するフィルタを設定できるのです。ここで、
catch()
メソッドのパラメータを見てみましょう。exception
パラメータは、現在処理中の例外オブジェクトです。host
パラメータはArgumentsHost
オブジェクトです。ArgumentsHost
は強力なユーティリティオブジェクトです。このサンプルコードでは、このオブジェクトを使って元のリクエストハンドラに渡されるRequest
オブジェクトとResponse
オブジェクトの参照を 取得します。ArgumentsHost
に関しては後の実行コンテキストのページで詳細に解説します。(現在準備中)新しい
HttpExceptionFilter
をCatsController
のcreate()
メソッドに関連付けましょう。cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
メモ
@UseFilters()
デコレータは@nestjs/common
からインポートできます。ここでは、
@UseFilters()
デコレータを活用しています。@Catch()
デコレータと同様に、単一のフィルタインスタンス、あるいはコンマで 区切ったフィルタインスタンスの一覧を受け取れます。上記のプログラムでは、
HttpException
のインスタンスを適当に作っています。インスタンスの作成はフレームワークに任せて、DIパターンを可能にします。cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
メモ
可能な限り、インスタンスの代わりにクラスを使ってフィルタを適用することをおすすめします。
NestはModule全体で同じクラスのインスタンスを簡単に再利用できるので、メモリ使用量を減らせます。
上の例では、
HttpExceptionFilter
は単一のルートハンドラcreate()
関数だけに適用され、メソッドにスコープされています。例外フィルタのスコープには、メソッドスコープ、コントローラスコープ、グローバルスコープがあります。たとえば、あるフィルタをcontroller-scopedに設定するには以下のようにします。
cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
このコンストラクションでは、
CatsController
内で定義されているすべてのルートハンドラに対してHttpException
フィルタを設定します。グローバルスコープのフィルタを作るためには、以下のように設定します。
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
注意
useGlobalFilters()
メソッドでは、ゲートウェイやアプリケーションのフィルターは設定されないので十分に注意してください。グローバルフィルタは、アプリケーション全体、つまりControllerやルートハンドラごとに使います。DIパターンからの観点からは、Moduleの外部から(上記の例のように
useGlobalFilters()
で)登録したグローバルフィルタは依存性を注入できません。この問題を解決するために、以下の構文で任意のModuleから直接グローバルスコープを登録できます。app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
メモ
この方法でフィルタの依存性注入を行 う場合、この構造が採用されるModuleに関係なく、フィルタは実際にグローバルであることに留意してください。
実際にこれを行うには、フィルタ(上記の例では
HttpExceptionFilter
)が定義されているModuleを選んでください。この方法では、必要な数だけフィルタを追加できます。それぞれのフィルタをプロバイダ配列に登録するだけです。
種類に関係なく全ての未処理の例外をキャッチするためには、
@Catch()
デコレータの変数リストを空にする。以下の例では、HTTPアダプタを使用してレスポンスを配信し、プラットフォーム固有のオブジェクト(
Request
とResponse
)を直接使用しないので、プラットフォームに依存しないコードになっている。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,
});
}
}
通常のアプリケーションの要件を満たすために、完全にカスタマイズされた例外フィルタを作成します。しかし、シンプルに拡張して、特定の理由に基づいて動作を継承したいこともあるでしょう。
例外処理をベースフィルタに委譲するには、
BaseExceptionFilter
を継承して、継承されたcatch()
メソッドを呼びましょう。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);
}
}
注意
BaseException
を継承したメソッドスコープ及びコントローラスコープのフィルターはnew
でインスタンス化しないように。かわりに、フレームワークが自動的にインスタンスを作成するようにします。
上記の実装は、アプローチを示すシェルに過ぎません。拡張された例外フィルタの実装には、あなた独自のビジネスロジック(例えば、様々な条件の処理など)が含まれるでしょう。
グローバルフィルタはベースフィルタを拡張することができます。これは、2つの方法のどちらかで行うことができます。
ひとつは、カスタムグローバルフ ィルタのインスタンスを作成する際に
HttpAdapter
のリファレンスを注入する方法です。main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
Last modified 9mo ago