はじめに
この記事はDjango Advent Calendar 2020 22日目の記事です。
22日目の記事を担当しますdelhi09と申します。
本記事では「Djangoのエラーハンドリングに関して覚えておくべきこと」というタイトルで書かせて頂きます。
概要
Djangoはエラーハンドリング周りに関しても非常に高機能です。
複雑な要件がなければ、エラーハンドリングはほぼDjangoの標準機能で事足りるので、実はロジックの中でエラーハンドリングを意識したコードを書く必要はほとんどありません。
ただ、Djangoが暗黙にやってくれていることを知らないと、不要なエラーハンドリング処理を書いたり、ログを二重に出してしまったりすることがあります。
加えて、エラーハンドリング周りの部分はオンライン上でも情報が少なめだったり、公式ドキュメントに書いてないこともあったりするなと感じました。
そこで、本記事ではDjangoが暗黙にやってくれているエラー処理の中から、Djangoを使う側でも覚えておいた方がよいと思ったことを書きました。
先にまとめ
以下が本記事のまとめです。
エラーハンドリングに関してDjangoを使う側で意識するべきこと
前提
アプリケーションでサーバーサイド起因のエラーが発生した時に、最低限行わなければならない処理は以下の3つだと私は考えています。
- HTTPステータスコードは500系を返す。
- ユーザーに適切なエラー画面を表示する。
- 発生したエラー内容をログファイルに適切なログレベルで出力する。
この3つに関しては、Djangoではほぼコーディングなしで対応することができます。
以下、順番に説明していきます。
1.HTTPステータスコードは500系を返す。
DjangoではViewで例外がraiseされた場合には、handler500
というものが実行されて、HTTPステータスコード500を返してくれます。
従って、View層以下で例外をraiseすれば、Djangoが内部でHTTPステータスコード500でレスポンスを返してくれるので、HTTPステータスコードに関してはDjangoを使う側では意識する必要はありません。
※ 尚、handler500で実行される処理はオーバーライドすることも可能です。その場合は以下の公式ドキュメントや去年のAdvent Calendarの記事を読んで頂くと良いと思います。
2.ユーザーに適切なエラー画面を表示する。
1で説明したhandler500
は、デフォルトの挙動では500.html
という名前のテンプレートファイルが存在する場合には、500.htmlを汎用エラー画面として返すという仕様になっています。
従って、500.htmlという名前のTemplateファイルに汎用エラー画面用のHTMLを書いて配置するだけでOKです。
加えて、覚えておいた方がよいこととして、DEBUG = True
の場合は、500.htmlが存在したとしても、画面には汎用エラー画面ではなくデバッグ情報を表示するというのがDjangoの仕様です。
従って、DEBUG = True
の開発環境では、汎用エラー画面が表示されることは動作確認できません。
このことを知らないと、開発時に「あれ?500.htmlを配置したのに使われないぞ?何か間違ってるのかな?」と焦ります。
3.発生したエラー内容をログファイルに適切なログレベルで出力する。
Djangoは処理の過程で例外がraiseされた場合には、実はレスポンスを返すときに内部でスタックトレースのロギングを行ってくれています。
ログレベルに関しても
- 400系に相当する例外 → Warnigレベル
- 500系に相当する例外 → Errorレベル
とHTTPステータスコードに応じてログレベルを分けてくれています。
上記の仕様に関しては、私も最近まで知らなかったのですが、ある時、ロジックの中で例外を捕捉してloggingする処理を書いていたら、同様の内容のエラーログが二重に出力されていることに気づいて、それをきっかけに調べて知りました。
本件に関しては、公式ドキュメントに該当する記述を見つけられなかったのですが、以下のソースコードのresponse_for_exception
というメソッドを読んで、その中でさらに呼ばれているdjango.utils.log.log_response
というメソッドを辿ると分かるかと思います。
response_for_exception
のソースコード
django.utils.log.log_response
のソースコード
(公式ドキュメントに関しては、私が見つけられていないだけかもしれないので、ここに記述があるよってご存知の方がいたら教えて頂けるとありがたいです。)
従って、View層以下で例外をraiseすれば、Djangoが内部で適切なログレベルでロギングをしてくれるので、ロギングに関してもDjangoを使う側で意識する必要はありません。
但し、settings.py
にLOGGING
の設定をしておかないと、ログがファイルに出力されないので、そこはDjangoを使う側がしっかり設定を書く必要があります。
DjangoのLOGGINGの設定に関しては、私も以前に記事を書いたので、よろしければこちらも参考にしてください。
(1〜3を受けて)View層以下では余計な例外ハンドリングはせずにエスカレーションするのが良い。
1〜3で見てきたように、Viewで例外をraiseすれば、あとはDjangoが内部でエラーレスポンスの作成やロギングをやってくれるので、View層以下では例外をエスカレーションするだけ(=何もハンドリング処理を書かない)にするのが良いのではないかと考えられます。
例えば、Djangoが内部で例外のスタックトレースをロギングしてくれているということを知らずに、以下のようなコードを書いたとします。
class SampleView(View): def get(self, request, *args, **kwargs): service = SampleService() try: service.do_business_logic() except BusinessLogicException as e: logger.exception(e) raise return render(request, "index.html")
このコードは結果的に
- 同じ内容のエラーログを二重に出力してしまっている。
- 本来1行で済むはずのコードを5行かけて書いてしまっている。
ことになります。
この例であれば
class SampleView(View): def get(self, request, *args, **kwargs): service = SampleService() service.do_business_logic() return render(request, "index.html")
とtry...except
で囲わずに書き、例外のハンドリングはDjangoに任せるのが良いと思います。