DEVGRU

プログラミングと競馬予想について書きます

ウェブサービスエラーハンドリング指針

これはなにか

現職の社内でこのテーマで書いたが、そちらは社内プロダクトの情報も混じっているので、同じテーマでブログ向けにいちから書き直してみる。 ちょっと長いが、ウェブサービスを運用するならきちんと抑えておきたい。

課題

SPA + HTTP API(概ねREST APIと言っていい)の構成において、エラーハンドリングを正しく行うのは意外と面倒だ。 しかし、特に運用中のプロダクトにおいては、障害・バグの早期検出、ユーザエクスペリエンス向上の観点から、これをきちんとするのが望ましい。

広告

前提

HTTP通信は切断されるし、インフラは落ちる

ローカルで開発をしていると忘れがちだが、HTTP通信は様々な要因で切断される。

クライアント側が公衆回線やWiFiを使っていれば、必ずそれは起きる。車両による移動、電子レンジの干渉、カフェのWiFiが混んできた、 etc.

また、長時間運用しているとどうしてもインフラの障害に襲われる。そして大抵の場合、プロバイダーの障害通知よりもサービスダウンのほうが早い。

データベースも冗長構成にしていないとクラッシュしてフェイルオーバーが終わるまではアクセスできない。

プログラムはクラッシュする

プログラムがクラッシュする可能性を完全には排除できない。

ユーザコードのバグ、ライブラリやフレームワークのバグ、言語処理系のバグ、想定外の入力、過負荷、何でもありえる。

HTTPレスポンスステータスは雄弁

HTTPレスポンスステータスは少ない情報量ながら非常に多くのことを語ってくれる。

ヘッダやボディに情報を詰め込まなくとも、それだけで多くの判断が可能になる。

開発者の時間は有限だ。横着して200系とそれ以外というような大雑把なくくりにせず、ステータスコードから更に処理を分岐して、重要度・緊急度に応じて自動的に対応するようにしよう。

広告

対応指針

主要なHTTPレスポンスステータスの意味

バックエンド開発者もフロントエンド開発者も、HTTPレスポンスステータスを理解する必要がある。

MDNの資料がわかりやすい。

以下は抑えておく必要がある。

レスポンスステータスの種類

  • 1xx - 情報レスポンス。この文脈では気にしなくて良い
  • 2xx - 成功レスポンス。この文脈では気にしなくて良い
  • 3xx - リダイレクションメッセージ。この文脈では気にしなくて良い
  • 4xx - クライアントエラーレスポンス。クライアント側起因のエラー。
  • 5xx - サーバエラーレスポンス。サーバ側起因のエラー。

主要なエラーレスポンスの意味

ステータスコード ステータスメッセージ 意味 よくある原因
400 Bad Request リクエストを理解できない 実装不備、攻撃
401 Unauthorized 未認証 クレデンシャル期限切れ、攻撃
403 Forbidden アクセス権がない ユーザのURL手打ち、攻撃
404 Not Found リソースが存在しない ユーザのURL手打ち、攻撃
500 Internal Server Error サーバがリクエストを処理できない バックエンド実装不備
502 Bad Gateway ゲートウェイなどサーバとクライアントの中間に位置するサービスがサーバから無効なレスポンスを受け取った バックエンドのウェブアプリケーションフレームワークの不具合
503 Service Unavailable サーバがリクエストを処理する準備ができていない 過負荷
504 Gateway Timeout ゲートウェイなどサーバとクライアントの中間に位置するサービスが時間内にレスポンスを得られない 過負荷

バックエンド

バックエンドの実装は、起きうるエラーを明示的にハンドルして、500を返したり、コネクション切断をしてしまわないように実装する必要がある。

RailsやDjangoなどウェブアプリケーションフレームワークの使用を前提とすると、実装起因で500が帰ってしまうのは送出された例外がハンドルされない場合である。この場合、フレームワークが例外を補足してクライアントに500を返す。

そのため、実装がどのような例外を送出しうるかを開発者は熟知して、適切なハンドリングをしなければいけない。一般に4xx系か503を返す。

以下のように例外をすべて補足して一律の対応をしてはいけない。これならフレームワークにハンドリングさせたほうがまだ良い。

try:
    # 例外を送出するかもしれないコード
except:
    # 一律で同じエラー処理をする

一般的なエラーとHTTPレスポンスステータスの対応

  • 400 Bad Request
    • リクエストが合意した仕様に沿っていない
    • バリデーションエラー
  • 401 Unauthorized
    • リクエストに認証トークンが含まれていない
  • 403 Forbidden
    • 認証はしているが、アクセスしてはいけないリソースへのリクエスト
  • 404 Not Found
    • 存在しないリソースへのリクエスト
    • アクセスしてはいけないリソースへのリクエストだが、とりわけクライアントへ存在を隠したい場合
  • 503 Service Unavailable
    • 復旧の見込みのあるデータベースエラー

ロギングと通知

バックエンドの出力はHTTPレスポンスの他に、ログと通知がある。

開発者がエラーを理解・対応するために必要な情報を入れること。また、バックエンドとは別のリソースに保存すること。

以下は抑えておきたい。

  • 日時
  • HTTPリクエストの情報
  • スタックトレース
  • エラーの情報

LTSVよりも構造化ログを使うと情報を多く含めることができ、機械的な処理がしやすい。

HTTPリクエストやデータベースのエラーの情報には個人情報やパスワードなど機微なものが含まれることについては気をつけなければいけない。

この場合は、よりセキュアなリソースに機微な情報を隔離してそこへのポインタのみをログに含めると良い。

具体的には、S3にHTTPリクエストやデータベースへのクエリを保存して、保存先のキーをログに出力する、などである。

また、エラーのうち対応が必要なものについては開発者、運営者やユーザへの通知を行う必要がある。

サービスへの影響を考慮して適切なレベルとチャネルで通知を自動的に行うようにしておこう。

検知できないエラー

特に 502, 504 など、一部のエラーはバックエンドで検出できない。

そのため、フロントエンド側でも対応が必要になる。

フロントエンド

フロントエンドのエラー

フロントエンドは大別してHTTP APIのエラーと、それ以外の実行時エラーについて対応する必要がある。

HTTP APIのエラーは前の節で示している。

実装が完全でなく、補足されない例外が発生した場合は、ブラウザは以下のイベントのいずれかを発火させる。

この2つのイベントはエラーが発生したコンテキストから離れておりまた一律に発火されるため、各々のエラーに対する適切なハンドリングが難しいため、このイベントが起きるような実装には原則してはならない。

また、HTTP APIへのリクエストが、HTTPの層より下(TCP/IP など)で失敗した場合や、CORS エラーの場合、HTTPレスポンスステータスが0としてブラウザに返ってくる。

フィードバック

フロントエンドがエラーを捕捉した場合には、ユーザと開発者(または運営者)へそれぞれフィードバックする必要がある。

一例

  • ユーザへのフィードバック
    • ダイアログやトースト、エラーページへの遷移によるユーザの誘導
    • 自動的な障害ページの更新(非常に高頻度・深刻な場合)
  • 開発者へのフィードバック
    • ログ
    • 通知

リトライ

一部のHTTPレスポンスステータスに関しては、リトライで成功することがある。

ユーザエクスペリエンスのためにも、そのほうが良い。

ただし、利用者の多いサービスにおいてフロントエンドから一斉にリトライが来た場合にバックエンドが過負荷に陥ることがあるため、リトライの実装は注意深くしなければいけない。

必ず Exponential Backoff と Jitter を組み合わせること。

エラーとフィードバックの対応表

ステータス リトライの有無 ユーザへのフィードバック 開発者へのフィードバックレベル 備考
400 Bad Request なし ダイアログ表示、エラーページへ遷移など フロントエンド・バックエンドの食い違いがあり得る
401 Unauthorized なし ログインページへ遷移など クレデンシャルの期限切れなど
403 Forbidden なし ダイアログ表示、エラーページへ遷移など ユーザがURLを手打ちしたなど。高頻度なら通知
404 Not Found なし ダイアログ表示、エラーページへ遷移など ユーザがURLを手打ちしたなど。高頻度なら通知
その他4xx なし ダイアログ表示、エラーページへ遷移など API実装にもよるが、普通ならあまり使わない。実装を確認すること。
500 Internal Server Error なし ダイアログ表示、エラーページへ遷移など 緊急 バックエンド異常のため、早めに状況を確認したほうが良い。
502 Bad Gateway あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
503 Service Unavailable あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
504 Gateway Timeout あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
その他5xx なし ダイアログ表示、エラーページへ遷移など 普通は使わない。実装やフレームワークの仕様を確認すること。
onerror 不可 ダイアログ表示、エラーページへ遷移など 緊急 ユーザに何らかの不利益があり得る。早めに原因を特定し、対処する。
unhandledrejection 不可 ダイアログ表示、エラーページへ遷移など 緊急 ユーザに何らかの不利益があり得る。早めに原因を特定し、対処する。
0 ダイアログ表示、エラーページへの遷移など なし まともに検証していればCORSエラーの可能性は低いので、通信エラーの可能性が高い。

広告

重要なサービス

ユーザへのフィードバック

Atlassian Statuspage

サービスステータスをユーザに報告するSaaS。

いわゆるステータスページの他、問題があったときにサービス上にアイコンを表示する、インシデントのメール・SNSによる報告など、基本的な機能が揃っている。

開発者へのフィードバック

Sentry

バックエンド・フロントエンドでのエラーを収集する。

主要な言語処理系、フレームワークをカバーしており、簡単な設定で補足されない例外を自動的に集めてくれる。また、カスタムな通知も可能。

オンプレミス版は無料。

Datadog

モニタリングプラットフォーム。

エラー収集、ログなど多数の機能が揃っている。

こちらも主要な言語処理系、フレームワークをカバーしており、簡単な設定で補足されない例外を自動的に集めてくれる。また、カスタムな通知も可能。

さらに、時間あたりのエラー回数など設定した条件に一致したら別のサービスに通知することができる。

New Relic

パフォーマンス分析プラットフォーム。

Datadogと一部競合するところがある。

PageDuty

インシデント対応プラットフォーム。

こちらも様々な機能があるが、Datadog から通知を受けて当番の開発者のスマートフォンに電話を掛け、反応がなかったら別の開発者にエスカレーションする、といったことができる。