アプリケーションエラーの処理
アプリケーションは失敗し、サーバーは失敗します。 遅かれ早かれ、本番環境で例外が発生します。 コードが100 % c正しい場合でも、例外が発生することがあります。 どうして? 関係する他のすべてが失敗するからです。 完全に細かいコードがサーバーエラーにつながる可能性があるいくつかの状況を次に示します。
- クライアントはリクエストを早期に終了しましたが、アプリケーションはまだ受信データから読み取っていました
- データベースサーバーが過負荷になり、クエリを処理できませんでした
- ファイルシステムがいっぱいです
- ハードドライブがクラッシュしました
- バックエンドサーバーが過負荷
- 使用しているライブラリのプログラミングエラー
- サーバーの別のシステムへのネットワーク接続に失敗しました
そして、それはあなたが直面する可能性のある問題のほんの一例です。 では、そのような問題にどのように対処するのでしょうか。 デフォルトでは、アプリケーションが本番モードで実行され、例外が発生した場合、Flaskは非常に単純なページを表示し、例外をlogger
に記録します。
ただし、実行できることは他にもあります。カスタム例外やサードパーティツールなど、エラーに対処するためのより適切なセットアップについて説明します。
エラーログツール
エラーメールの送信は、たとえ重要なものであっても、十分な数のユーザーがエラーに遭遇し、通常はログファイルが表示されない場合、圧倒される可能性があります。 このため、アプリケーションエラーの処理には Sentry の使用をお勧めします。 これは、GitHub でソース利用可能なプロジェクトとして利用可能であり、無料で試すことができるホストバージョンとしても利用可能です。 Sentryは重複エラーを集約し、デバッグ用に完全なスタックトレースとローカル変数をキャプチャし、新しいエラーまたは頻度のしきい値に基づいてメールを送信します。
Sentryを使用するには、追加のflask
依存関係を持つsentry-sdk
クライアントをインストールする必要があります。
$ pip install sentry-sdk[flask]
そして、これをFlaskアプリに追加します。
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])
YOUR_DSN_HERE
値は、Sentryインストールから取得したDSN値に置き換える必要があります。
インストール後、内部サーバーエラーにつながる障害は自動的にSentryに報告され、そこからエラー通知を受け取ることができます。
参考
- Sentryは、同様の方法でワーカーキュー(RQ、Celeryなど)からのエラーのキャッチもサポートしています。 詳細については、 PythonSDKドキュメントを参照してください。
- セントリー入門
- フラスコ固有のドキュメント
エラーハンドラ
Flaskでエラーが発生すると、適切な HTTPステータスコードが返されます。 400-499は、クライアントの要求データ、または要求されたデータに関するエラーを示します。 500-599は、サーバーまたはアプリケーション自体のエラーを示します。
エラーが発生したときに、カスタムエラーページをユーザーに表示したい場合があります。 これは、エラーハンドラーを登録することで実行できます。
エラーハンドラーは、エラーのタイプが発生したときに応答を返す関数です。これは、ビューが要求URLが一致したときに応答を返す関数であるのと同様です。 処理中のエラーのインスタンスが渡されます。これはおそらくHTTPException
です。
応答のステータスコードはハンドラーのコードに設定されません。 ハンドラーから応答を返すときは、必ず適切なHTTPステータスコードを指定してください。
登録
関数をerrorhandler()
で装飾して、ハンドラーを登録します。 または、register_error_handler()
を使用して後で関数を登録します。 応答を返すときは、エラーコードを設定することを忘れないでください。
@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
return 'bad request!', 400
# or, without the decorator
app.register_error_handler(400, handle_bad_request)
werkzeug.exceptions.HTTPException
BadRequest
のようなサブクラスとそれらのHTTPコードは、ハンドラーを登録するときに交換可能です。 (BadRequest.code == 400
)
非標準のHTTPコードは、Werkzeugに認識されていないため、コードで登録できません。 代わりに、適切なコードを使用してHTTPException
のサブクラスを定義し、その例外クラスを登録して発生させます。
class InsufficientStorage(werkzeug.exceptions.HTTPException):
code = 507
description = 'Not enough storage space.'
app.register_error_handler(InsufficientStorage, handle_507)
raise InsufficientStorage()
ハンドラーは、HTTPException
サブクラスやHTTPステータスコードだけでなく、任意の例外クラスに登録できます。 ハンドラーは、特定のクラス、または親クラスのすべてのサブクラスに登録できます。
取り扱い
Flaskアプリケーションをビルドするとき、は例外に遭遇します。 リクエストの処理中にコードの一部が壊れた場合(およびエラーハンドラーが登録されていない場合)、デフォルトで「500内部サーバーエラー」(InternalServerError
)が返されます。 同様に、リクエストが未登録のルートに送信されると、「404 NotFound」(NotFound
)エラーが発生します。 ルートが許可されていない要求メソッドを受信すると、「405メソッドが許可されていません」(MethodNotAllowed
)が発生します。 これらはすべてHTTPException
のサブクラスであり、Flaskでデフォルトで提供されます。
Flaskを使用すると、Werkzeugによって登録されたHTTP例外を発生させることができます。 ただし、デフォルトのHTTP例外は、単純な例外ページを返します。 エラーが発生したときに、カスタムエラーページをユーザーに表示したい場合があります。 これは、エラーハンドラーを登録することで実行できます。
リクエストの処理中にFlaskが例外をキャッチすると、最初にコードで検索されます。 コードにハンドラーが登録されていない場合、Flaskはクラス階層によってエラーを検索します。 最も具体的なハンドラーが選択されます。 ハンドラーが登録されていない場合、HTTPException
サブクラスはコードに関する一般的なメッセージを表示し、その他の例外は一般的な「500 InternalServerError」に変換されます。
たとえば、ConnectionRefusedError
のインスタンスが発生し、ハンドラーがConnectionError
およびConnectionRefusedError
に登録されている場合、より具体的なConnectionRefusedError
ハンドラーが応答を生成するための例外インスタンス。
ブループリントが例外を発生させる要求を処理していると仮定すると、ブループリントに登録されているハンドラーは、アプリケーションにグローバルに登録されているハンドラーよりも優先されます。 ただし、ブループリントを決定する前に404がルーティングレベルで発生するため、ブループリントは404ルーティングエラーを処理できません。
ジェネリック例外ハンドラ
HTTPException
やException
などの非常に一般的な基本クラスのエラーハンドラーを登録することができます。 ただし、これらは予想以上にキャッチされることに注意してください。
たとえば、HTTPException
のエラーハンドラーは、デフォルトのHTMLエラーページをJSONに変換するのに役立つ場合があります。 ただし、このハンドラーは、ルーティング中の404エラーや405エラーなど、直接引き起こさないものに対してトリガーされます。 HTTPエラーに関する情報が失われないように、ハンドラーは慎重に作成してください。
from flask import json
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_exception(e):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
response = e.get_response()
# replace the body with JSON
response.data = json.dumps({
"code": e.code,
"name": e.name,
"description": e.description,
})
response.content_type = "application/json"
return response
Exception
のエラーハンドラーは、未処理のエラーも含め、すべてのエラーがユーザーに表示される方法を変更するのに役立つように思われる場合があります。 ただし、これはPythonでexcept Exception:
を実行するのと似ており、すべてのHTTPステータスコードを含む all その他の未処理のエラーをキャプチャします。
ほとんどの場合、より具体的な例外のハンドラーを登録する方が安全です。 HTTPException
インスタンスは有効なWSGI応答であるため、それらを直接渡すこともできます。
from werkzeug.exceptions import HTTPException
@app.errorhandler(Exception)
def handle_exception(e):
# pass through HTTP errors
if isinstance(e, HTTPException):
return e
# now you're handling non-HTTP exceptions only
return render_template("500_generic.html", e=e), 500
エラーハンドラは引き続き例外クラス階層を尊重します。 HTTPException
とException
の両方のハンドラーを登録すると、Exception
ハンドラーは、HTTPException
ハンドラーの方が多いため、HTTPException
サブクラスを処理しません。明確な。
未処理の例外
例外に登録されているエラーハンドラがない場合は、代わりに500内部サーバーエラーが返されます。 この動作については、flask.Flask.handle_exception()
を参照してください。
InternalServerError
に登録されているエラーハンドラーがある場合、これが呼び出されます。 Flask 1.1.0以降、このエラーハンドラーには、元の未処理のエラーではなく、常にInternalServerError
のインスタンスが渡されます。
元のエラーはe.original_exception
として入手できます。
「500InternalServer Error」のエラーハンドラーには、明示的な500エラーに加えて、キャッチされない例外が渡されます。 デバッグモードでは、「500 InternalServerError」のハンドラーは使用されません。 代わりに、対話型デバッガーが表示されます。
カスタムエラーページ
Flaskアプリケーションを構築するときに、HTTPException
を上げて、リクエストに問題があることをユーザーに通知したい場合があります。 幸い、Flaskには便利なabort()
関数が付属しており、必要に応じてwerkzeugからのHTTPエラーでリクエストを中止します。 また、基本的な説明が記載されたわかりやすい白黒のエラーページも表示されますが、特別なことは何もありません。
エラーコードに応じて、ユーザーが実際にそのようなエラーを目にする可能性は低くなります。
以下のコードを検討してください。ユーザープロファイルルートがある可能性があり、ユーザーがユーザー名を渡さなかった場合、「400 BadRequest」を発生させることができます。 ユーザーがユーザー名を渡しても見つからない場合は、「404NotFound」を表示します。
from flask import abort, render_template, request
# a username needs to be supplied in the query args
# a successful request would be like /profile?username=jack
@app.route("/profile")
def user_profile():
username = request.arg.get("username")
# if a username isn't supplied in the request, return a 400 bad request
if username is None:
abort(400)
user = get_user(username=username)
# if a user can't be found by their username, return 404 not found
if user is None:
abort(404)
return render_template("profile.html", user=user)
「404ページが見つかりません」例外の別の実装例を次に示します。
from flask import render_template
@app.errorhandler(404)
def page_not_found(e):
# note that we set the 404 status explicitly
return render_template('404.html'), 404
アプリケーションファクトリを使用する場合:
from flask import Flask, render_template
def page_not_found(e):
return render_template('404.html'), 404
def create_app(config_filename):
app = Flask(__name__)
app.register_error_handler(404, page_not_found)
return app
テンプレートの例は次のとおりです。
{% extends "layout.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
<h1>Page Not Found</h1>
<p>What you were looking for is just not there.
<p><a href="{{ url_for('index') }}">go somewhere nice</a>
{% endblock %}
さらなる例
上記の例は、実際にはデフォルトの例外ページを改善するものではありません。 次のようなカスタム500.htmlテンプレートを作成できます。
{% extends "layout.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block body %}
<h1>Internal Server Error</h1>
<p>Oops... we seem to have made a mistake, sorry!</p>
<p><a href="{{ url_for('index') }}">Go somewhere nice instead</a>
{% endblock %}
これは、「500 InternalServerError」でテンプレートをレンダリングすることで実装できます。
from flask import render_template
@app.errorhandler(500)
def internal_server_error(e):
# note that we set the 500 status explicitly
return render_template('500.html'), 500
アプリケーションファクトリを使用する場合:
from flask import Flask, render_template
def internal_server_error(e):
return render_template('500.html'), 500
def create_app():
app = Flask(__name__)
app.register_error_handler(500, internal_server_error)
return app
ブループリント付きモジュラーアプリケーションを使用する場合:
from flask import Blueprint
blog = Blueprint('blog', __name__)
# as a decorator
@blog.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
# or with register_error_handler
blog.register_error_handler(500, internal_server_error)
ブループリントエラーハンドラ
ブループリントを使用したモジュラーアプリケーションでは、ほとんどのエラーハンドラーが期待どおりに機能します。 ただし、404および405例外のハンドラーに関する警告があります。 これらのエラーハンドラーは、適切なraise
ステートメント、または別のブループリントのビュー関数でのabort
の呼び出しからのみ呼び出されます。 たとえば、無効なURLアクセスによって呼び出されることはありません。
これは、ブループリントが特定のURLスペースを「所有」していないため、アプリケーションインスタンスには、無効なURLが指定された場合に実行するブループリントエラーハンドラーを知る方法がないためです。 URLプレフィックスに基づいてこれらのエラーに対してさまざまな処理戦略を実行する場合は、request
プロキシオブジェクトを使用してアプリケーションレベルで定義できます。
from flask import jsonify, render_template
# at the application level
# not the blueprint level
@app.errorhandler(404)
def page_not_found(e):
# if a request is in our blog URL space
if request.path.startswith('/blog/'):
# we return a custom blog 404 page
return render_template("blog/404.html"), 404
else:
# otherwise we return our generic site-wide 404 page
return render_template("404.html"), 404
@app.errorhandler(405)
def method_not_allowed(e):
# if a request has the wrong method to our API
if request.path.startswith('/api/'):
# we return a json saying so
return jsonify(message="Method Not Allowed"), 405
else:
# otherwise we return a generic site-wide 405 page
return render_template("405.html"), 405
APIエラーをJSONとして返す
FlaskでAPIを構築するとき、一部の開発者は、組み込みの例外がAPIに対して十分に表現力がなく、発行している text / html のコンテンツタイプがAPIコンシューマーにとってあまり役に立たないことに気付きます。
上記とjsonify()
と同じ手法を使用して、APIエラーに対してJSON応答を返すことができます。 abort()
は、description
パラメーターで呼び出されます。 エラーハンドラーはそれをJSONエラーメッセージとして使用し、ステータスコードを404に設定します。
from flask import abort, jsonify
@app.errorhandler(404)
def resource_not_found(e):
return jsonify(error=str(e)), 404
@app.route("/cheese")
def get_one_cheese():
resource = get_resource()
if resource is None:
abort(404, description="Resource not found")
return jsonify(resource)
カスタム例外クラスを作成することもできます。 たとえば、人間が読める適切なメッセージ、エラーのステータスコード、およびエラーのコンテキストを増やすためのオプションのペイロードを受け取ることができるAPIの新しいカスタム例外を導入できます。
これは簡単な例です。
from flask import jsonify, request
class InvalidAPIUsage(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
super().__init__()
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
@app.errorhandler(InvalidAPIUsage)
def invalid_api_usage(e):
return jsonify(e.to_dict())
# an API app route for getting user information
# a correct request might be /api/user?user_id=420
@app.route("/api/user")
def user_api(user_id):
user_id = request.arg.get("user_id")
if not user_id:
raise InvalidAPIUsage("No user id provided!")
user = get_user(user_id=user_id)
if not user:
raise InvalidAPIUsage("No such user!", status_code=404)
return jsonify(user.to_dict())
ビューは、エラーメッセージとともにその例外を発生させることができるようになりました。 さらに、ペイロードパラメーターを使用して、追加のペイロードを辞書として提供できます。