当サイトは、アフィリエイト広告を利用しています

【Python × Flask】REST APIでカスタム例外クラスでエラーハンドリングする方法

作成日:2024月07月15日
更新日:2024年08月21日

PythonのWEBフレームワークであるFlaskで実装したREST APIで
独自のカスタム例外クラスを作り、エラーハンドラー関数を使って
エラーハンドリングする方法をまとめておく

Pythonの例外処理の基本的なことについては下記記事でまとめています。

flaskでのエラーハンドリングについて

flaskでエラーハンドリングをする場合は基本的に下記の
いずれかを使って行う。

  • abort関数
  • errorhandlerデコレーター
  • register_error_handler()メソッド

この中でカスタム例外クラスを組合わせて使用できるのが
「errorhandlerデコレーター」と「register_error_handler()メソッド」になる。

当記事では「register_error_handler()メソッド」を使う。
※「errorhandlerデコレーター」でも同じことはできる

なぜカスタム例外クラスとエラーハンドラー関数を使うのか?

この二つを組み合わせることで効率的にエラーハンドリングを
行うことができる。

一貫したエラーハンドリング

エラーハンドラー関数を使うことで、エラーハンドリングのロジックをコード内で
一つに集約することができるので、コードの可読性や保守性(変更や拡張)が上がる。

柔軟なエラーハンドリング

カスタム例外クラスを使用することで仕様に合わせたエラー情報を選択できるので
柔軟なエラーハンドリングが可能。

実装

実際にflaskで簡易なREST APIを実装して動作を確認する。
開発はdockerコンテナ上で行う。

構成

プロジェクトの構成は下記のようにする

構成
.
|-- Dockerfile
|-- api
| |-- __init__.py
| |-- api.py
| |-- error_handling.py
| |-- exceptions.py
| |-- routes.py
| `-- wsgi.py
|-- docker-compose.yml
|-- .env
`-- requirements.txt

それぞれの役割について説明する

Dockerfile

コンテナのimageをつくるためのファイル

Dockerfile
FROM python:3.12
# workspaceディレクトリ作成、移動
WORKDIR /workspace
# プロジェクトディレクトリにコピー
COPY requirements.txt /workspace
# 必要モジュールのインストール
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

python:3.12を元にしてrequirements.txtにある
ライブラリをpipでインストールしたimageを作る

requirements.txt

コンテナにインストールするライブラリを記載

requirements.txt
Flask==3.0.2

.env

.env
# flask-REST APIコンテナ用
# Flaskがどのアプリケーションを実行するか指定(pyファイル:インスタンス)
FLASK_APP=api.wsgi:api
# Flaskがどの環境で動作するか指定
FLASK_ENV=development
#デバッグモードの指定
FLASK_DEBUG=true

flaskのビルトインサーバーでREST APIを起動させるための設定を行う
ここで設定した値はコンテナの環境変数に設定される

docker-compose.yml

コンテナをdocker-composeで作成する

docker-compose.yml
version: "3"
services:
flask-api-error:
container_name: "flask-api-error"
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
volumes:
# バインドマウント
- .:/workspace
# 環境変数読み込み
env_file:
- .env
tty: true
  • Dockerfileをbuildしたimageを元にコンテナを作る
  • .envファイルを読み込み、環境変数に設定する

exceptions.py

カスタム例外クラスを独自で定義する

exceptions.py
class CustomException(Exception):
status_code: str
error_message: str
def __init__(self, error_message: str):
self.error_message = error_message
class CustomBadRequestException(CustomException):
status_code = 400 # override field
class CustomNotFoundException(CustomException):
status_code = 404 # override field
class CustomInternalServerErrorException(CustomException):
status_code = 500 # override field

例外の種類ごとにカスタム例外クラスを継承したサブクラスを定義している

error_handling.py

エラーハンドラー関数を定義する
アプリケーション内で発生させたカスタム例外はすべてこの関数で処理する

error_handling.py
from flask import jsonify
from api.exceptions import CustomException
def handle_custom_exception(error: CustomException):
response = {
"status_code": error.status_code,
"error_message": error.error_message
}
return jsonify(response)

カスタム例外クラスのオブジェクト(例外オブジェクト)を受け取り
レスポンスを編集して返却する

api.py

アプリケーションファクトリパターンを使って
Flaskアプリケーションの初期設定を行う

api.py
from flask import Flask
from api.routes import configure_user_routes
from api.exceptions import CustomException
from api.error_handling import handle_custom_exception
def create_app() -> Flask:
# アプリケーションインスタンスの作成
app = Flask(__name__)
# 日本語文字化け対応
app.json.ensure_ascii = False
# エラーハンドラ関数登録
app.register_error_handler(CustomException, handle_custom_exception)
# ルーティング設定
configure_user_routes(app)
return app

register_error_handlerメソッドにて

  • カスタム例外クラス
  • エラーハンドラー関数

を登録している。
これでアプリケーション内でCustomExceptionや
CustomExceptionを継承したサブクラスの例外をraiseした際は
handle_custom_exception関数が実行されるようになる
※try~exceptで捕まえた場合を除く。

routes.py

REST APIのルーティングを設定するモジュール

routes.py
from api.exceptions import CustomBadRequestException
from api.exceptions import CustomInternalServerErrorException
from api.exceptions import CustomNotFoundException
from flask import Flask
class Routes:
def __init__(self, app: Flask):
self.app = app
self.configure_routes()
def configure_routes(self):
@self.app.route('/<code>', methods=['GET'])
def get_users(code):
if code == '400':
raise CustomBadRequestException(error_message="不正なリクエストです")
elif code == '404':
raise CustomNotFoundException(error_message="リソースが見つかりません")
elif code == '500':
raise CustomInternalServerErrorException(error_message="内部サーバーエラーが発生しました")
else:
return "Code not recognized", 200
def configure_user_routes(app: Flask):
Routes(app)

GETリクエスト時、パスパラメータにて指定したコードの例外を発生させるようにする。

wsgi.py

flaskのアプリケーションのエントリーポイントとなるファイル。

wsgi.py
from api.api import create_app
api = create_app()

flaskのビルトインサーバーはこのファイルを参照して
flaskアプリケーションインスタンスを取得し起動する

例外発生時の処理の流れについて

カスタム例外を発生させた場合の処理の流れについてまとめる。
下記のようなフローになる

  • カスタム例外を発生させる
  • 例外オブジェクトが作られる
  • エラーハンドラー関数が例外オブジェクトを受け取りレスポンス返す

具体的にその部分のコードを抜粋する

routes.py

routes.py
from api.exceptions import CustomBadRequestException
from api.exceptions import CustomInternalServerErrorException
from api.exceptions import CustomNotFoundException
from flask import Flask
class Routes:
def __init__(self, app: Flask):
self.app = app
self.configure_routes()
def configure_routes(self):
@self.app.route('/<code>', methods=['GET'])
def get_users(code):
if code == '400':
raise CustomBadRequestException(error_message="不正なリクエストです")
elif code == '404':
raise CustomNotFoundException(error_message="リソースが見つかりません")
elif code == '500':
raise CustomInternalServerErrorException(error_message="内部サーバーエラーが発生しました")
else:
return "Code not recognized", 200
def configure_user_routes(app: Flask):
Routes(app)

GETリクエストのパスパラメータで設定されたコードに
したがってカスタム例外を発生させる

exceptions.py

上記でraise時に指定した例外クラスのオブジェクトが作成される

exceptions.py
class CustomException(Exception):
status_code: str
error_message: str
def __init__(self, error_message: str):
self.error_message = error_message
class CustomBadRequestException(CustomException):
status_code = 400 # override field
class CustomNotFoundException(CustomException):
status_code = 404 # override field
class CustomInternalServerErrorException(CustomException):
status_code = 500 # override field

error_handling.py

カスタム例外が発生した場合はこの関数が検知し
上記で指定した例外クラスのオブジェクトを受け取り処理を行う

error_handling.py
from flask import jsonify
from api.exceptions import CustomException
def handle_custom_exception(error: CustomException):
response = {
"status_code": error.status_code,
"error_message": error.error_message
}
return jsonify(response)

実行してみる

実際にcurlでGEリクエストを送信して動作を確認する。

flaskのビルトインサーバー起動

コンテナで下記コマンドで起動できる。

bash
flask run --host=0.0.0.0

リクエストを送信

コンテナ起動していて、ホストとコンテナのポートは5000でバインドしているので
ホスト側からGETリクエストを送信する

400

コード_400
$ curl localhost:5000/400
{
"error_message": "不正なリクエストです",
"status_code": 400
}

404

コード_404
$ curl localhost:5000/404
{
"error_message": "リソースが見つかりません",
"status_code": 404
}

500

コード_500
$ curl localhost:5000/500
{
"error_message": "内部サーバーエラーが発生しました",
"status_code": 500
}

エラーハンドリングできているのが確認できる

別パターン

上記までは「register_error_handler()メソッド」を使って
実装したが「errorhandlerデコレーター」を使いたい場合は
下記のように書き換えることで同様のことができる。

api.pyの変更

エラーハンドラ関数登録を削除

api.py
from flask import Flask
from api.routes import configure_user_routes
from api.exceptions import CustomException
# from api.error_handling import handle_custom_exception
def create_app() -> Flask:
# アプリケーションインスタンスの作成
app = Flask(__name__)
# 日本語文字化け対応
app.json.ensure_ascii = False
# # エラーハンドラ関数登録
# app.register_error_handler(CustomException, handle_custom_exception)
# ルーティング設定
configure_user_routes(app)
return app

ここではエラーハンドラー関数を登録しないため

routes.py

デコレーターを追加する

routes.py
from flask import Flask,jsonify
from api.exceptions import CustomException
from api.exceptions import CustomBadRequestException
from api.exceptions import CustomInternalServerErrorException
from api.exceptions import CustomNotFoundException
class Routes:
def __init__(self, app: Flask):
self.app = app
self.configure_routes()
def configure_routes(self):
@self.app.route('/<code>', methods=['GET'])
def get_users(code):
if code == '400':
raise CustomBadRequestException(error_message="不正なリクエストです")
elif code == '404':
raise CustomNotFoundException(error_message="リソースが見つかりません")
elif code == '500':
raise CustomInternalServerErrorException(error_message="内部サーバーエラーが発生しました")
else:
return "Code not recognized", 200
@self.app.errorhandler(CustomException)
def handle_custom_exception(error: CustomException):
response = {
"status_code": error.status_code,
"error_message": error.error_message
}
return jsonify(response)
def configure_user_routes(app: Flask):
Routes(app)

errorhandlerデコレーターを使ってエラーハンドラー関数を設定する
errorhandlerデコレーターでは検知するカスタム例外クラスを設定しておく。

どっちを使うほうがいいか?

結果的には同じことができるので、プロジェクトの方針や個人の好みによるかもしれない。
個人的にはルーティング処理とエラー処理で役割ごとにモジュールを分けておきたかったので
「register_error_handler()メソッド」を使うようにした。

もう一つのパターンであるabort関数に関しては下記でまとめています

まとめ

flaskのREST APIでカスタム例外をハンドリングする方法を
まとめてみた。

register_error_handler()メソッドを使うことで
try~catchを書くことなく、一元的に例外を管理できるので
開発効率と保守性は高くなると感じた。

当記事ではエラーハンドリングをメインにまとめたが
flaskのREST APIの実装、コンテナの作成、サーバー起動について詳しくは
下記でまとめています。

またより実践的なREST APIの実装については下記記事でまとめています。
※カスタムエラー例外クラスでのエラーハンドリングも行っている

参考

新着記事

タグ別一覧
top