Flask-RESTfulでの自作の例外クラスを扱うベストプラクティス

- Flask 1.1.2
- Flask-RESTful 0.3.8
- 例外クラスを自作したい人向け


- 追記
    - 2020年5月1日: `Flask`,`Flask-RESTful`の最新版で動作確認済み

# 概要

Flaskは自由度の高いフレームワークなので、例外処理の実装方法も色々と用意されている。さっとググっただけでも3パターンほど記事が見つかる。

- `abort()`関数
- `Flask#errorhandler`デコレーター
- `Flask#register_error_handler()`メソッド
- （Flask-RESTful限定） `Api#handle_error()`メソッドのオーバーライド

この記事では「**自作クラスで例外を送出したい**」という場合に、自分なりに色々試してみて辿り着いたベストプラクティスをまとめた。

# 基本方針

`Api#handle_error()`メソッドをオーバーライドする方針で進める。

以降に示すサンプルコードは次のようなプロジェクト構成を前提としている。

```
<src>
|   main.py
+---app/
    |   __init__.py
    +---dto/
    |       __init__.py
    |       exception.py
    +---api/
            __init__.py
            sample.py
```

# 例外クラスの実装

効果的に例外処理を行うためには

- どんな例外が発生したのか
- どうすれば解決できるのか（もしくは解決できないのか）

をRESTクライアントへ伝える必要がある。そのために例外クラスに独自のフィールドを設けて、構造化したオブジェクトにしたい、というニーズは確実にあるだろう。


- **ポイント**
    - `werkzeug.HTTPException`クラスを継承する
    - HTTPステータスコードは`code`フィールドに持たせる


```python:dto/exception.py
from typing import List, Union
from werkzeug.exception import HTTPException


class MyException(HTTPException):
    fault_id: str  # custom field
    message_id: str  # custom field
    arguments: List[str]  # custom field

    def __init__(self, fault_id: str, message_id: str, arguments: Union[None, List[str]] = None):
        self.fault_id = fault_id
        self.message_id = message_id
        if arguments is None:
            self.arguments = []
        else:
            self.arguments = arguments


class MyBadRequestException(MyException):
    code = 400  # override field


class MyNotFoundException(MyException):
    code = 404  # override field


class MyUnprocessableEntityException(MyException):
    code = 422  # override field


class MyInternalServerErrorException(MyException):
    code = 500  # override field

```

上記のサンプルではアプリケーションで扱う全ての例外の基底クラスとなる`MyException`クラスを定義し、HTTPステータスごとに各例外クラスを定義している。

基底クラス`MyException`にはアプリケーション固有のフィールドを持たせているが、ここなんでも良いのでアプリケーションに合わせて各自で設定する。

# `Api#handle_error()`メソッドのオーバーライド

Flask-RESTfulが提供する`Api`クラスを継承する。

- **ポイント**
    - `handle_error()`メソッドでは「自作例外処理クラスの場合とそれ以外の場合を分けて処理する
    - `make_response()`メソッドを利用し、HTTPレスポンスのペイロードを組み立て、HTTPステータスコードを設定する

```python:api/__init__.py
from flask_restful import Api
from ..dto.exception import MyException


class MyApi(Api):
    def handle_error(self, e: MyException):
        if isinstance(e, MyException):
            return self.make_response({
                'faultId': e.fault_id,
                'messageId': e.message_id,
                'arguments': e.arguments,
            }, e.code)
        else:
            return super().handle_error(e)
```

上記のサンプルコードでは自作クラスのフィールド値はそのままに、フィールド名をスネークケースからキャメルケースへ変換した上で、HTTPレスポンスボディとして返している。

# 例外の送出

ResourceクラスやServiceクラスなど、ビジネスロジックの中で自作例外クラスを送出する。

- **ポイント**
    - エンドポイントを定義する際には`Api`クラスの代わりに、`Api`クラスを継承したクラスを使用する
    - 自作の例外クラスを`raise`することで例外を送出する

```python:api/sample.py
from flask import Blueprint
from flask_restful import Resource

from . import MyApi
from ..dto.exception import MyNotFoundException

bp = Blueprint('sample', __name__)
api = MyApi(bp)


@api.resource('/api/sample/<int:sample_id>')
class SampleResource(Resource):
    def get(self, sample_id: int) -> str:
        # do something
        raise MyNotFoundException('F0001', 'M0001')

    def put(self, sample_id: int) -> bool:
        # do something
        raise MyUnprocessableEntityException('F0002', 'M0002', ['foo', 'bar'])
```

以上。