Exception filters | NestJS 【翻訳】

## NestJS公式ドキュメント翻訳

[NestJS公式ドキュメント翻訳](https://qiita.com/mana-vv/items/a658051a5183183c3e50)

## 原文

[Exception filters| NestJS - A progressive Node.js web framework](https://docs.nestjs.com/exception-filters)

## 例外フィルター

Nestにはアプリケーション全体で未処理のすべての例外を処理する組み込みの**例外レイヤー**が付属しています。例外がアプリケーションコードによって処理されない場合、このレイヤーがキャッチし、適切なユーザーフレンドリーなレスポンスを自動的に送信します。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499074/1df3e7d2-be05-b1d6-61d8-3db857db1bf4.png)


この処理は組み込みのグローバルな例外フィルターによって実行されます。このフィルターは`HttpException`型の例外（およびそのサブクラス）を処理します。例外が**認識されない**場合（`HttpException`でも`HttpException`を継承するクラスでもない場合）、組み込みの例外フィルターは次のデフォルトのJSONレスポンスを生成します。

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

### 標準的な例外のスロー

Nestは`@nestjs/common`パッケージで公開されている組み込みの`HttpException`クラスを提供します。一般的なHTTP REST / GraphQL APIベースのアプリケーションでは、特定のエラー状態が発生したときに標準HTTPレスポンスオブジェクトを送信することがベストプラクティスです。たとえば、`CatsController`には`findAll()`メソッド（`GET`ルートハンドラー）があります。このルートハンドラーが何らかの理由で例外をスローすると仮定しましょう。これを実証するために、次のようにハードコードしてみましょう。

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

ここでは`HttpStatus`を使用しました。これは`@nestjs/common`パッケージからインポートされたヘルパー列挙です。

クライアントがこのエンドポイントを呼び出すと、レスポンスは次のようになります。

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

`HttpException`コンストラクタは、レスポンスを決定する2つの必須の引数を取ります。

- `response`引数はJSONレスポンスボディを定義します。以下で説明するように`string`または`object`にすることができます。
- `status`引数は[HTTPステータスコード](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)を定義します。

デフォルトではJSONレスポンスボディには2つのプロパティが含まれています。

- `statusCode`：デフォルトは`status`引数で提供されるHTTPステータスコード
- `message`：`status`に基づくHTTPエラーの簡単な説明

JSONレスポンスボディのメッセージ部分のみをオーバーライドするには、`response`引数に文字列を指定します。

JSONレスポンスボディ全体をオーバーライドするには`response`引数にオブジェクトを渡します。Nestはオブジェクトをシリアル化しJSONレスポンスボディとして返します。

2番目のコンストラクタ引数- `status` -は有効なHTTPステータスコードである必要があります。ベストプラクティスは`@nestjs/common`からインポートした`HttpStatus`列挙を使用することです。

レスポンスボディ全体をオーバーライドする例を次に示します。

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

上記を使用するとレスポンスは次のようになります。

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

### カスタム例外

多くの場合、独自の例外を記述する必要はなく、次のセクションで説明するように組み込みのNest HTTPエクセプションを使用できます。カスタマイズされた例外を作成する必要がある場合、独自の**例外レイヤー**を作成することをお勧めします。カスタム例外は`HttpException`クラスを継承します。このアプローチにより、Nestは例外を認識しエラーレスポンスを自動的に処理します。そのようなカスタム例外を実装してみましょう：

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

`ForbiddenException`は`HttpException`を継承することで組み込みの例外ハンドラーとシームレスに連携するため、`findAll()`メソッド内で使用できます。

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

### 組み込みHTTPエクセプション

Nestは`HttpException`から継承する標準例外のセットを提供します。これらは`@nestjs/common`パッケージから公開され、一般的なHTTP例外の多くを表します。

- `BadRequestException`
- `UnauthorizedException`
- `NotFoundException`
- `ForbiddenException`
- `NotAcceptableException`
- `RequestTimeoutException`
- `ConflictException`
- `GoneException`
- `PayloadTooLargeException`
- `UnsupportedMediaTypeException`
- `UnprocessableEntityException`
- `InternalServerErrorException`
- `NotImplementedException`
- `BadGatewayException`
- `ServiceUnavailableException`
- `GatewayTimeoutException`

### 例外フィルター

基本（組み込み）例外フィルターは多くのケースを自動的に処理できますが、例外レイヤーを開発者が`完全に制御`したい場合があります。例えば、ログを追加したり、いくつかの動的要因に基づいて異なるJSONスキーマを使用したりすることができます。**例外フィルター**はまさにこの目的のために設計されています。これによって、正確な制御フローとクライアントに返されるレスポンスをコントロールすることができます。

`HttpException`クラスのインスタンスである例外をキャッチし、それらのカスタムレスポンスロジックを実装する例外フィルターを作成しましょう。これには基盤となるプラットフォームの`Request`および`Response`オブジェクトにアクセスする必要があります。`Request`オブジェクトにアクセスして、元の`url`を引き出してログ情報に含めてみましょう。`response.json()`メソッドによって`Response`オブジェクトを使用して、送信されるレスポンスを直接制御します。

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

すべての例外フィルターは`ExceptionFilter<T>`インターフェースを実装する必要があります。これには`catch(exception：T、host：ArgumentsHost)`メソッドに指定のシグネチャを提供する必要があります。`T`は例外の型を示します。

`@Catch(HttpException)`デコレータは必要なメタデータを例外フィルターにバインドし、この特定のフィルターが`HttpException`型の例外を探していることをNestに伝えます。`@Catch()`デコレータは単一のパラメータ、またはコンマ区切りのリストを取ることができます。これにより、複数の種類の例外のフィルターを一度に設定することができます。

### Arguments host

`catch()`メソッドのパラメーターを見てみましょう。`exception`パラメーターは現在処理されている例外オブジェクトです。`host`パラメーターは`ArgumentsHost`オブジェクトです。`ArgumentsHost`は強力なユーティリティオブジェクトであり、他の章でさらに詳しく調べます※。ここでの主な目的は、（例外の発生元のコントローラー内の）元のリクエストハンドラーに渡される`Request`および`Response`オブジェクトへの参照を提供することです。ここでは`ArgumentsHost`でいくつかのヘルパーメソッドを使用して、目的の`Request`および`Response`オブジェクトを取得しました。

`host.switchToHttp()`ヘルパーは`HttpArgumentsHost`オブジェクトを返します。`HttpArgumentsHost`オブジェクトには2つの便利なメソッドがあります。これらのメソッドを使用して目的のオブジェクトを抽出し、この場合はExpressの型アサーションを使用してネイティブExpress型オブジェクトを返します。

```typescript
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
```

※ このレベルの抽象化をする理由は、`ArgumentsHost`がすべてのコンテキスト（たとえば、現在作業しているHTTPサーバコンテキストだけでなく、マイクロサービスやソケット）で機能するためです。後で`ArgumentsHost`とそのヘルパー関数を使用して、実行コンテキストの適切な基本引数にアクセスする方法について説明します。これにより、すべてのコンテキストで動作する一般的な例外フィルターを作成できます。

### Binding filters

新しい`HttpExceptionFilter`を`CatsController`の`create()`メソッドに関連付けましょう。

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

`@UseFilters()`デコレータは`@nestjs/common`パッケージからインポートされます。

ここでは`@UseFilters()`デコレータを使用しました。`@Catch()`デコレータと同様に、フィルターインスタンス、またはコンマ区切りのフィルターインスタンスのリストを使用できます。ここでは`HttpExceptionFilter`のインスタンスを作成しました。（インスタンスの代わりに）クラスを渡し、インスタンス化の責任をフレームワークに委ね、**依存性注入**を有効にすることもできます。

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

可能な場合、インスタンスの代わりにクラスを使用してフィルターを適用することをお勧めします。 Nestはモジュール全体で同じクラスのインスタンスを簡単に再利用できるため、メモリ使用量が削減されます。

上記の例では、`HttpExceptionFilter`は`create()`ルートハンドラーにのみ適用され、メソッドスコープになります。例外フィルターは、メソッドスコープ、コントローラスコープ、またはグローバルスコープのさまざまなレベルでスコープすることができます。例えば、フィルターをコントローラスコープとして設定するには次のようにします。

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

この構造は`CatsController`内で定義されたすべてのルートハンドラーに対して`HttpExceptionFilter`をセットアップします。

グローバルスコープのフィルターを作成するには、次の操作を行います。

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

`useGlobalFilters()`メソッドはゲートウェイまたはハイブリッドアプリケーションのフィルターを設定しません。

グローバルスコープフィルターはすべてのコントローラーとすべてのルートハンドラーに対して、アプリケーション全体で使用されます。依存性注入に関して、モジュールの外部から登録されたグローバルフィルター（上記の例での`useGlobalFilters()`）はモジュールのコンテキスト外で行われるため、依存性を注入できません。この問題を解決するには、次の構成を使用して、**任意のモジュールから**グローバルスコープフィルターを**直接**登録します。

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

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

このアプローチでフィルターの依存性注入を行う場合、この方法が使われるモジュールに関係なく、フィルターは実際にはグローバルであることに注意してください。これはどこで行われるのでしょうか？フィルター（上記の例では`HttpExceptionFilter`）が定義されているモジュールを選択します。また、カスタムプロバイダーの登録を処理する方法は`useClass`だけではありません。詳細は[こちら](https://docs.nestjs.com/fundamentals/custom-providers)をご覧ください。

この方法で必要な数だけフィルターを追加できます。それぞれをプロバイダ配列に追加するだけです。

### Catch everything

未処理の例外を**すべて**（例外の種類に関係なく）キャッチするには、`@Catch()`デコレータのパラメータリストを空のままにします（例：`@Catch()`）

```typescript
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()`メソッドを呼び出す必要があります。

```typescript: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);
  }
}
```

`BaseExceptionFilter`を拡張したメソッドスコープおよびコントローラスコープのフィルターは、`new`でインスタンス化しないでください。その代わりにフレームワークがそれらを自動的にインスタンス化します。

上記の実装は一つのアプローチを示す単なるシェルです。拡張例外フィルターの実装には**ビジネス**ロジック（さまざまな条件の処理など）が含まれます。

グローバルフィルターはベースフィルターを拡張することが**できます**。これは2つの方法で実行できます。

一つ目の方法はカスタムグローバルフィルターをインスタンス化するときに`HttpServer`参照を注入することです。

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

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

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

2つ目の方法は、[次に示すように](https://docs.nestjs.com/exception-filters#binding-filters)`APP_FILTER`トークンを使用することです。