エラーハンドリングのベストプラクティスを考える
天下のGoogle様の記事を参考にしてエラーハンドリングのベストプラクティスを考えます。
1. Don't fail silently
アプリが何のエラーも報告せずに突然終了してしまうのは、ユーザーにとっても開発側にとっても大きなストレスとなるでしょう。エラーが発生した場合は適切に報告し、何が起きたのか、原因が何なのかを明確に伝えることが重要です。
エラーは完全に防ぐことはできないものと認識し、エラーメッセージをソフトウェア設計時に計画的に組み込むことが求められます。
2. プログラミング言語のガイドラインに従う
Googleがスタイルガイド(python)を公開しているため、そちらを参照しましょう。flaskに関して言えば公式ドキュメントも参照します。
エラーをハンドリングするだけではなく、そもそもエラーを引き起こさない設計やコーディングを優先するも重要ですね。
Googleは他の言語についてもスタイルガイドを公開しています。ご参考までに;
3. 完全なエラーモデルを実装する
エラーモデルについては、GoogleのAIP(API Improvement Proposals)で定義されている方法で実装することが推奨されています。
まとめると以下のような観点があります(適切なものだけを抜粋)
-
3-1.統一されたエラーモデル:
今回で言うと、アプリ内で統一されたモデルに従うこと -
3-2.エラー構造:
-
code
: クライアントが処理可能なエラーコード -
message
: 開発者向けの英語のエラーメッセージ。エラー説明と解決策を含む -
details
: クライアントがエラーを処理するための追加情報
-
-
3-3.標準エラーコードの使用:
既存の例外を使用し、新たなエラーコードの追加は極力避ける。他の記事でもみましたが、「運用に則って不便があれば」適宜実装する程度で良いかもしれません。 -
3-4.エラーメッセージの設計:
- 簡潔で、非エキスパートユーザーにも理解可能
- 必要に応じてリンクや
details
フィールドを使用して追加情報を提供 - 再試行のガイドラインを書く
-
3-5エラーの伝搬:
- 実装詳細や機密情報は非公開にする
4. 根本原因を隠さない
「サーバーエラー」のような曖昧なメッセージではなく、具体的な原因を特定するためのコンテキストを提供する。当たり前ですが大事ですね。
例: サービスの失敗、ネットワーク切断、ステータスの不一致、権限の問題など。
5. エラーコードを記録する
カスタマーサポートやエンジニアがエラーを監視・診断するのに役立つため、ログで記録を残します。エラーコードのリストを文書化しておくのも良いでしょう。
6. エラーは即座に報告する
デバッグコストを軽減できるため、エラーは早期に報告するようにしましょう。また報告するだけではなく、それに気づく必要もあるので外部サービスに通知することも必要でしょう。
念の為、他のサイトでもベストプラクティスを調べましたが、上記の項目でカバーできていました。
調べたサイトについては参考文献で書いています。
Flaskでどのように実装するか
さて、ベストプラクティスに基づいてどのような実装にするか考えてみます。
エラーモデルの定義
# カスタムエラーの定義
class PermissionError(Exception):
status_code = 400
message = "十分な権限がありません。"
def __init__(self, payload=None):
super().__init__()
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
# 使用
def update(self, updatee: User, updater: User) -> None:
if not updater.is_editable(updatee):
raise PermissionError({
"detail": f"あなたは{updatee.name}への編集権限がありません。",
"suggestion": "適切な権限に切り替えてください。"
})
...
ポイント:
- 標準的なエラーは
Werkzeug
でカバーされているため、そちらを使うかオーバーライドしても良いかもしれません -
detail
詳細やsuggestion
次のアクションに言及
エラーの登録
@app.errorhandler(PermissionError)
def invalid_api_usage(e):
return jsonify(e.to_dict()), e.status_code
ポイント:
- flaskの
errorhandler
デコレーターを使います - エラー後の処理はこちらに書くこと。細かいエラーはドメイン層に書かない
- 返り値はカスタムテンプレートなども返せます
メールでの通知
from flask import has_request_context, request
from flask.logging import default_handler
import logging
from logging.handlers import SMTPHandler
# メールハンドラの設定
mail_handler = SMTPHandler(
mailhost='127.0.0.1',
fromaddr='server-error@example.com',
toaddrs=['mochi@sakura.com'],
subject='アプケーションエラー'
)
mail_handler.setLevel(logging.ERROR)
class RequestFormatter(logging.Formatter):
def format(self, record):
if has_request_context():
record.url = request.url
record.remote_addr = request.remote_addr
else:
record.url = None
record.remote_addr = None
return super().format(record)
formatter = RequestFormatter(
'[%(asctime)s] %(remote_addr)s requested %(url)s\n'
'%(levelname)s in %(module)s: %(message)s'
)
default_handler.setFormatter(formatter)
mail_handler.setFormatter(formatter)
if not app.debug:
app.logger.addHandler(mail_handler)
- ローカルにメールサーバーがある想定です
-
RequestFormatter
をオーバーライドして、詳細な情報を提示します