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

組み込みのHTTP例外

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クラスのインスタンスである例外をキャッチし、その例外に対するカスタムのレスポンスのロジックを実装するための例外フィルタを作ってみましょう。
これを実施するためには、基盤となるプラットフォームRequestResponseオブジェクトにアクセスする必要があります。
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()デコレータは、単一のパラメータあるいはコンマ区切りのリストを受け取ります。これによって、一度に複数の種類の例外を処理するフィルタを設定できるのです。

ArgumentsHost

ここで、catch()メソッドのパラメータを見てみましょう。exceptionパラメータは、現在処理中の例外オブジェクトです。hostパラメータはArgumentsHostオブジェクトです。ArgumentsHostは強力なユーティリティオブジェクトです。このサンプルコードでは、このオブジェクトを使って元のリクエストハンドラに渡されるRequestオブジェクトとResponseオブジェクトの参照を取得します。
ArgumentsHostに関しては後の実行コンテキストのページで詳細に解説します。(現在準備中)

バインディングフィルタ

新しいHttpExceptionFilterCatsControllercreate()メソッドに関連付けましょう。
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 Evrything

種類に関係なく全ての未処理の例外をキャッチするためには、@Catch()デコレータの変数リストを空にする。
以下の例では、HTTPアダプタを使用してレスポンスを配信し、プラットフォーム固有のオブジェクト(RequestResponse)を直接使用しないので、プラットフォームに依存しないコードになっている。
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();
もうひとつの方法は、こちらに示してあるとおりAPP_FILTERトークンを活用する方法です。