delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Djangoのエラーハンドリングに関して覚えておくべきこと

はじめに

この記事はDjango Advent Calendar 2020 22日目の記事です。

22日目の記事を担当しますdelhi09と申します。
本記事では「Djangoのエラーハンドリングに関して覚えておくべきこと」というタイトルで書かせて頂きます。

概要

Djangoはエラーハンドリング周りに関しても非常に高機能です。

複雑な要件がなければ、エラーハンドリングはほぼDjangoの標準機能で事足りるので、実はロジックの中でエラーハンドリングを意識したコードを書く必要はほとんどありません。

ただ、Djangoが暗黙にやってくれていることを知らないと、不要なエラーハンドリング処理を書いたり、ログを二重に出してしまったりすることがあります。

加えて、エラーハンドリング周りの部分はオンライン上でも情報が少なめだったり、公式ドキュメントに書いてないこともあったりするなと感じました。

そこで、本記事ではDjangoが暗黙にやってくれているエラー処理の中から、Djangoを使う側でも覚えておいた方がよいと思ったことを書きました。

先にまとめ

以下が本記事のまとめです。

  • エラーハンドリングに関してDjangoを使う側で意識するべきことは以下の3つである。
    • 500.htmlを配置する。
    • LOGGINGを設定する。
    • View層以下での例外は余計なハンドリングをせずにエスカレーションする。

エラーハンドリングに関してDjangoを使う側で意識するべきこと

前提

アプリケーションでサーバーサイド起因のエラーが発生した時に、最低限行わなければならない処理は以下の3つだと私は考えています。

  1. HTTPステータスコード500系を返す。
  2. ユーザーに適切なエラー画面を表示する。
  3. 発生したエラー内容をログファイルに適切なログレベルで出力する。

この3つに関しては、Djangoではほぼコーディングなしで対応することができます。

以下、順番に説明していきます。

1.HTTPステータスコード500系を返す。

DjangoではViewで例外がraiseされた場合には、handler500というものが実行されて、HTTPステータスコード500を返してくれます。

docs.djangoproject.com

従って、View層以下で例外をraiseすれば、Djangoが内部でHTTPステータスコード500でレスポンスを返してくれるので、HTTPステータスコードに関してはDjangoを使う側では意識する必要はありません。

※ 尚、handler500で実行される処理はオーバーライドすることも可能です。その場合は以下の公式ドキュメントや去年のAdvent Calendarの記事を読んで頂くと良いと思います。

docs.djangoproject.com
qiita.com

2.ユーザーに適切なエラー画面を表示する。

1で説明したhandler500は、デフォルトの挙動では500.htmlという名前のテンプレートファイルが存在する場合には、500.htmlを汎用エラー画面として返すという仕様になっています。

従って、500.htmlという名前のTemplateファイルに汎用エラー画面用のHTMLを書いて配置するだけでOKです。

加えて、覚えておいた方がよいこととして、DEBUG = Trueの場合は、500.htmlが存在したとしても、画面には汎用エラー画面ではなくデバッグ情報を表示するというのがDjangoの仕様です。

従って、DEBUG = Trueの開発環境では、汎用エラー画面が表示されることは動作確認できません。

このことを知らないと、開発時に「あれ?500.htmlを配置したのに使われないぞ?何か間違ってるのかな?」と焦ります。

docs.djangoproject.com

3.発生したエラー内容をログファイルに適切なログレベルで出力する。

Djangoは処理の過程で例外がraiseされた場合には、実はレスポンスを返すときに内部でスタックトレースのロギングを行ってくれています。
ログレベルに関しても

  • 400系に相当する例外 → Warnigレベル
  • 500系に相当する例外 → Errorレベル

HTTPステータスコードに応じてログレベルを分けてくれています。

上記の仕様に関しては、私も最近まで知らなかったのですが、ある時、ロジックの中で例外を捕捉してloggingする処理を書いていたら、同様の内容のエラーログが二重に出力されていることに気づいて、それをきっかけに調べて知りました。

本件に関しては、公式ドキュメントに該当する記述を見つけられなかったのですが、以下のソースコードresponse_for_exceptionというメソッドを読んで、その中でさらに呼ばれているdjango.utils.log.log_responseというメソッドを辿ると分かるかと思います。

github.com

github.com

(公式ドキュメントに関しては、私が見つけられていないだけかもしれないので、ここに記述があるよってご存知の方がいたら教えて頂けるとありがたいです。)

従って、View層以下で例外をraiseすれば、Djangoが内部で適切なログレベルでロギングをしてくれるので、ロギングに関してもDjangoを使う側で意識する必要はありません。

但し、settings.pyLOGGINGの設定をしておかないと、ログがファイルに出力されないので、そこはDjangoを使う側がしっかり設定を書く必要があります。

DjangoのLOGGINGの設定に関しては、私も以前に記事を書いたので、よろしければこちらも参考にしてください。

kamatimaru.hatenablog.com

(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に任せるのが良いと思います。

最後に

本記事に関しては、実は、私自身が、Djangoが内部で例外のスタックトレースをロギングしてくれているということを数日前まで知らなくて、「例外ハンドリング用のMiddlewareを自作する」という割とボリューミーな記事をAdvent Calendar用に書いていたのですが、検証している時にそのことに気づいて、「あれ?もしかしてMiddleware自作する必要ないかも?」となって記事を書き直したという経緯があったりします。

読んで頂きありがとうございました!