エラー応答もAPIの契約
目標
業務例外と内部エラーを区別し、一貫した問題レスポンスを提供します。
なぜ重要なのか
クライアントは、エラーが出ると、detailの文字列を正規表現で読んでいました。ある日、文言が変わると、在庫不足も決済のリトライとして処理されました。別の経路では、例外の文字列がそのまま公開されて、SQLと内部のパスが漏れました。エラーには、成功のレスポンスに劣らず、明示的な契約が必要です。
ステップ
/root/work/fa-problem-lab/service.pyで、DomainError(code, message)は、Exceptionのサブクラスで、.codeにcodeを保持します。str(例外)は、messageです。
最初に一度だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
-
/root/work/fa-problem-lab/service.pyで、status_for(code)は、missing=404、conflict=409、invalid=422、それ以外=500です。 -
/root/work/fa-problem-lab/service.pyで、public_message(code)は、missing='Resource not found'、conflict='State conflict'、invalid='Invalid request'、それ以外='Internal error'です。 -
/root/work/fa-problem-lab/service.pyで、request_id(value)は、ASCIIの英字・数字・アンダースコア・ハイフンだけで構成された1–32文字の文字列ならそのまま、そうでなければ'untracked'です。 -
/root/work/fa-problem-lab/service.pyで、problem(code, rid)は、type='urn:labhub:problem:'+code、titleとdetail=public_message(code)、status=status_for(code)、request_id=request_id(rid)だけを持つ辞書です。 -
/root/work/fa-problem-lab/service.pyで、response_for(code, rid)は、problemを本文に、status_forをステータスに、application/problem+jsonをmedia_typeに、X-Request-IDを正規化したridにしたJSONResponseです。 -
/root/work/fa-problem-lab/service.pyで、install_handlers(app)は、DomainErrorのハンドラーを登録します。ヘッダーX-Request-IDを読んで、exc.codeに対してresponse_forを返します。excのmessageは、レスポンスに入れません。 -
/root/work/fa-problem-lab/service.pyで、create_app()は、ハンドラーをインストールして、GET /fail/{code}でDomainError(code, 内部の文言)を出します。ただしcode=boomならRuntimeErrorを出します。RuntimeErrorのハンドラーは、code=internalの固定の500の問題レスポンスを返します。
参考
- インターネットやパッケージのインストールなしで、既存のlab-dev環境で行います。
- 各ステップは、45秒の採点バジェットの中で実行されます。実際のsleepやネットワーク呼び出しを追加しないでください。
- 採点は、提出されたモジュールを新しく読み込んで、独立した入力と一時的なDBで検査します。期待値を定数で返す代わりに、契約を実装してください。
- FastAPI公式ドキュメント・pytest公式ドキュメント・Python sqlite3
- 限界: このラボの問題レスポンスは、type・title・status・detail・request_idを持つ、学習用の契約です。汎用の国際化や、標準への完全な適合性は主張しません。request idは、追跡に使う文字列であって、認証の手段ではなく、本番のログにも、秘密の値をそのまま記録してはいけません。
業務例外にcodeを残す
/root/work/fa-problem-lab/service.pyで、DomainError(code, message)は、Exceptionのサブクラスで、.codeにcodeを保持します。str(例外)は、messageです。
最初に一度だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
機械が判断するcodeと、人が見る内部メッセージを分離します。
保存したら、bash /opt/lab/checks/fa-problem-lab/01-contract.shで確認してください。
codeをステータスにマッピングする
/root/work/fa-problem-lab/service.pyで、status_for(code)は、missing=404、conflict=409、invalid=422、それ以外=500です。
未知のcodeを、成功として扱いません。
保存したら、bash /opt/lab/checks/fa-problem-lab/02-contract.shで確認してください。
公開用の文言を固定する
/root/work/fa-problem-lab/service.pyで、public_message(code)は、missing='Resource not found'、conflict='State conflict'、invalid='Invalid request'、それ以外='Internal error'です。
例外の文字列にDBのアドレスや内部のパスが入っていても、公開しません。
保存したら、bash /opt/lab/checks/fa-problem-lab/03-contract.shで確認してください。
リクエストidを制限する
/root/work/fa-problem-lab/service.pyで、request_id(value)は、ASCIIの英字・数字・アンダースコア・ハイフンだけで構成された1–32文字の文字列ならそのまま、そうでなければ'untracked'です。
任意のヘッダーをそのまま返さないように、長さと文字集合を一緒に制限します。
保存したら、bash /opt/lab/checks/fa-problem-lab/04-contract.shで確認してください。
問題の本文を作る
/root/work/fa-problem-lab/service.pyで、problem(code, rid)は、type='urn:labhub:problem:'+code、titleとdetail=public_message(code)、status=status_for(code)、request_id=request_id(rid)だけを持つ辞書です。
本文とHTTPのステータスが食い違うと、クライアントはどちらの値を信じるか決められません。
保存したら、bash /opt/lab/checks/fa-problem-lab/05-contract.shで確認してください。
レスポンスの形式を一貫させる
/root/work/fa-problem-lab/service.pyで、response_for(code, rid)は、problemを本文に、status_forをステータスに、application/problem+jsonをmedia_typeに、X-Request-IDを正規化したridにしたJSONResponseです。
単純な辞書を返すだけでは、エラーも200になってしまうことがあります。
保存したら、bash /opt/lab/checks/fa-problem-lab/06-contract.shで確認してください。
業務例外のハンドラーをつなぐ
/root/work/fa-problem-lab/service.pyで、install_handlers(app)は、DomainErrorのハンドラーを登録します。ヘッダーX-Request-IDを読んで、exc.codeに対してresponse_forを返します。excのmessageは、レスポンスに入れません。
例外を捕まえる場所を散らさず、アプリの共通の境界に置きます。
保存したら、bash /opt/lab/checks/fa-problem-lab/07-contract.shで確認してください。
予想していないエラーも隠す
/root/work/fa-problem-lab/service.pyで、create_app()は、ハンドラーをインストールして、GET /fail/{code}でDomainError(code, 内部の文言)を出します。ただしcode=boomならRuntimeErrorを出します。RuntimeErrorのハンドラーは、code=internalの固定の500の問題レスポンスを返します。
テストで例外の再送出を無効にして、実際の500のレスポンスのバイト列を確認します。
保存したら、bash /opt/lab/checks/fa-problem-lab/08-contract.shで確認してください。