5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-04-30
  • 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フィールドに持たせる
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ステータスコードを設定する
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することで例外を送出する
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'])

以上。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?