Flaskで全例外を共通関数で処理しつつ、デフォルトのInternal Server Errorページを表示
TL;DR
-
- 無条件でInternalServerError()を返却する
-
- エラーがwerkzeug.exceptions.HTTPExceptionのサブクラスの時はそのままインスタンスを返却し、そうでないときにInternalServerError()を返却する
要求
- すべてのルーティングにおける全例外を、HTTPエラーとそれ以外のそれぞれでの共通関数で処理するか、あるいは両方の共通関数で処理したい
- 例外をハンドリングした際は全てデフォルトのHTTPエラーフォーマットで返したい
実装例
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 -
以下のような表示
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 -
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 -
以下のような表示
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により呼ばれている関数と思われるが詳細は未確認。
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()となっている。
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のレスポンスデータがこの時点で構築される。
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__()が呼ばれる。
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)
のように呼ばれる。
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データが構築される。
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が入る。
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から取得される。
@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
HTTP_STATUS_CODES = {
100: "Continue",
・・・
500: "Internal Server Error",
説明テキストを構築するHTTPException.get_description()では第一引数のenvironは現状使用されていないので無視され、代わりにクラス変数のdescriptionが使用される。
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