⚗️

Flaskで全例外を共通関数で処理しつつ、デフォルトのInternal Server Errorページを表示

2023/12/13に公開

TL;DR

  • 全エラーをInternal Server Errorとしたい場合

    • 無条件でInternalServerError()を返却する
  • HTTPエラーだけはデフォルトのHTTPエラーページにしたい場合

    • エラーがwerkzeug.exceptions.HTTPExceptionのサブクラスの時はそのままインスタンスを返却し、そうでないときにInternalServerError()を返却する

要求

  • すべてのルーティングにおける全例外を、HTTPエラーとそれ以外のそれぞれでの共通関数で処理するか、あるいは両方の共通関数で処理したい
  • 例外をハンドリングした際は全てデフォルトのHTTPエラーフォーマットで返したい

実装例

  1. HTTPエラーとそれ以外のエラーで関数を分ける場合

server_sep.py
from flask import Flask
from werkzeug.exceptions import HTTPException, InternalServerError


def flask_test() -> None:
    app: Flask = Flask(__name__)

    @app.get("/")
    def hello():
        return "Hello"

    @app.get("/error")
    def error():
        raise ValueError("an error")

    @app.errorhandler(HTTPException)
    def handle_http_error(e: HTTPException):
        print("http error:", e)
        return e

    @app.errorhandler(Exception)
    def handle_general_error(e: Exception):
        print("general error:", e)
        return InternalServerError()

    app.run(host="0.0.0.0", port=5000, debug=True)


if __name__ == "__main__":
     flask_test()
  • 存在しないページにアクセスしたとき

以下のような表示

Not Found

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.


ターミナル

http error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
***.***.***.*** - - [**/***/**** **:**:**] "GET /test HTTP/1.1" 404 -
  • /errorにアクセスしたとき

以下のような表示

Internal Server Error

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.


ターミナル

general error: an error
***.***.***.*** - - [**/***/**** **:**:**] "GET /error HTTP/1.1" 500 -
  1. すべてのエラーを共通関数で処理する場合

server_com.py
from flask import Flask
from werkzeug.exceptions import HTTPException, InternalServerError


def flask_test() -> None:
    app: Flask = Flask(__name__)

    @app.get("/")
    def hello():
        return "Hello"

    @app.get("/error")
    def error():
        raise ValueError("an error")

    @app.errorhandler(Exception)
    def handle_all_error(e: Exception):
        print("error:", e)
        return e if isinstance(e, HTTPException) else InternalServerError()

    app.run(host="0.0.0.0", port=5000, debug=True)


 if __name__ == "__main__":
     flask_test()
  • 存在しないページにアクセスしたとき

以下のような表示

Not Found

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

ターミナル

error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
***.***.***.*** - - [**/***/**** **:**:**] "GET /test HTTP/1.1" 404 -
  • /errorにアクセスしたとき

以下のような表示

Internal Server Error

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

ターミナル

error: an error
***.***.***.*** - - [**/***/**** **:**:**] "GET /error HTTP/1.1" 500 -

InternalServerError()が返却された時の挙動

返却直後においてデバッガで確認できるのはflask/app.py->Flask.handle_user_exception()からであり、これはおそらくデコレータerrorhandlerにより呼ばれている関数と思われるが詳細は未確認。

flask/app.py->Flask
    def handle_user_exception(
        self, e: Exception
    ) -> HTTPException | ft.ResponseReturnValue:
       ・・・
        return self.ensure_sync(handler)(e)

最終行が処理された後の呼び出し元はflask/app.py->Flask.full_dispatch_request()で、次の処理はflask/app.py->Flask.finalize_request()となっている。

flask/app.py->Flask
    def full_dispatch_request(self) -> Response:
        ・・・
        except Exception as e:
            rv = self.handle_user_exception(e)
        return self.finalize_request(rv)

ここでfinalize_request()一行目のFlask.make_response()の引数rvにInternalServerError()が渡されることで、Flaskのレスポンスデータがこの時点で構築される。

flask/app.py->Flask
    def finalize_request(
        self,
        rv: ft.ResponseReturnValue | HTTPException,
        from_error_handler: bool = False,
    ) -> Response:
        ・・・
        response = self.make_response(rv)

この下位の関数のwerkzeug/test.py->run_wsgi_app()では、第一引数appにInternalServerError()が渡されることでHTTPExceptionインスタンスを呼び出し可能な関数とみなし、HTTPException.__call__()が呼ばれる。

werkzeug/test.py
def run_wsgi_app(
    app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False
) -> tuple[t.Iterable[bytes], str, Headers]:
    ・・・
    app_rv = app(environ, start_response)

実際にはInternalServerError()(environ, start_response)のように呼ばれる。

werkzeug/exceptions.py->HTTPException
    def __call__(
        self, environ: WSGIEnvironment, start_response: StartResponse
    ) -> t.Iterable[bytes]:
        ・・・
        response = t.cast("WSGIResponse", self.get_response(environ))

ここで呼ばれるHTTPException.get_response()において、bodyを構築する際に呼ばれるHTTPException.get_body()によってhtmlテンプレートとインスタンスクラスの変数のエラーコード及び説明テキスト、プロパティを用いてhtmlデータが構築される。

werkzeug/exceptions.py->HTTPException
    def get_body(
        self,
        environ: WSGIEnvironment | None = None,
        scope: dict | None = None,
    ) -> str:
        """Get the HTML body."""
        return (
            "<!doctype html>\n"
            "<html lang=en>\n"
            f"<title>{self.code} {escape(self.name)}</title>\n"
            f"<h1>{escape(self.name)}</h1>\n"
            f"{self.get_description(environ)}\n"
        )

InternalServerError()の場合以下のようになっているので、codeには500が入る。

werkzeug/exceptions.py->InternalServerError
class InternalServerError(HTTPException):
    ・・・
    code = 500
    description = (
        "The server encountered an internal error and was unable to"
        " complete your request. Either the server is overloaded or"
        " there is an error in the application."
    )

nameはHTTPExceptionのほうでプロパティで定義されており、ステータスコードとテキストを対応させた辞書werkzeug/http.py->HTTP_STATUS_CODESから取得される。

werkzeug/exceptions.py->HTTPException
    @property
    def name(self) -> str:
        """The status name."""
        from .http import HTTP_STATUS_CODES

        return HTTP_STATUS_CODES.get(self.code, "Unknown Error")  # type: ignore
werkzeug/http.py->HTTP_STATUS_CODES
HTTP_STATUS_CODES = {
    100: "Continue",
    ・・・
    500: "Internal Server Error",

説明テキストを構築するHTTPException.get_description()では第一引数のenvironは現状使用されていないので無視され、代わりにクラス変数のdescriptionが使用される。

werkzeug/exceptions.py->HTTPException
    def get_description(
        self,
        environ: WSGIEnvironment | None = None,
        scope: dict | None = None,
    ) -> str:
        """Get the description."""
        if self.description is None:
            description = ""
        else:
            description = self.description

        description = escape(description).replace("\n", Markup("<br>"))
        return f"<p>{description}</p>"

参考

Discussion