はじめに
外部 API や下流サービスの一時的な障害で、画面全体をエラーにしたくない場面があります。BFF やバックエンドでフォールバックを返せば、利用者は操作を続けられます。
一方で、取得失敗を空配列や初期値へ置き換えると、「データがない」と「データを取得できなかった」を区別できなくなります。見た目の可用性は上がっても、どの層で問題が起きたのかを追えず、障害の検知、調査、復旧判断が難しくなります。
この記事では、フォールバックを使いながら障害を隠さず、デバッグと原因切り分けをしやすくするために、レスポンス、ログ、キャッシュをどう設計するかを整理します。
問題は空配列を返すことではない
一覧 API を考えます。正常に検索し、対象が0件だった場合のレスポンスは次のとおりです。
200 OK
{
"items": []
}
この [] は「検索を実行した結果、該当データがなかった」という意味です。
ここで、下流 API のタイムアウトやレート制限を同じ [] に変換すると、意味が変わります。
async function listOrders() {
try {
return await orderApi.list();
} catch {
return { items: [] };
}
}
この実装では、呼び出し側から次の2つを見分けられません。
- 検索は成功し、注文が0件だった
- 検索に失敗し、注文の有無は不明である
フォールバックそのものが問題なのではありません。異なる状態を同じ値に畳み込むことが問題です。
エラーを消すと原因の切り分けが止まる
障害調査では、画面から下流へ向かって事実を確認します。各層の結果が残っていれば、問題の範囲を少しずつ狭められます。
画面では0件
↓
BFFは正常な0件として返している
↓
業務サービスは下流のタイムアウトを記録している
↓
外部サービスの応答遅延を確認する
しかし、業務サービスや BFF が例外を空配列へ変換し、ログにも残さなければ、確認できる事実は「画面が0件」のみです。この状態では、データが存在しないのか、検索条件が誤っているのか、認可で除外されたのか、下流が失敗したのかを区別できません。
障害時に必要なのは、原因を最初から当てることではありません。各層の入力、出力、失敗の有無を確認し、問題がない範囲を確定していくことです。エラー情報を残す設計は、この確認を可能にします。
可用性と正しさは別の要件
障害時の設計では、少なくとも次の要件を分けて考える必要があります。
- 可用性: 一部の依存先が失敗しても、画面や主要な操作を利用できる
- 正しさ: 表示したデータが、取得できた事実にもとづいている
- 観測可能性: 障害が起きたことと、その影響範囲を運用側で把握できる
「HTTP 200 を返す」は可用性のための手段になりえます。しかし、それだけで正しさや観測可能性まで満たせるわけではありません。
特に、利用者の判断に影響する一覧、残高、請求額、在庫などでは、未取得の値を「0件」や「0円」として表示しないことが重要です。
先に状態を分ける
まず、API が表す状態を分けます。取得結果は少なくとも次の3つに分かれます。
| 状態 | 意味 | 利用者への扱いの例 |
|---|---|---|
| 成功・データあり | 取得に成功し、データがある | データを表示する |
| 成功・0件 | 取得に成功し、該当データがない | 空状態を表示する |
| 取得失敗 | データの有無を確認できない | 再試行案内や一時的なエラーを表示する |
この区別を内部だけに持つか、クライアントへ返すかは、画面の役割と障害時の振る舞いで決めます。
たとえば、補助情報の取得に失敗しても画面本体を表示したい場合は、成功したデータと取得失敗を同じレスポンスに含められます。
{
"profile": {
"name": "田中"
},
"notifications": null,
"partial_errors": [
{
"target": "notifications",
"code": "UPSTREAM_TIMEOUT",
"retryable": true
}
]
}
この形なら、画面はプロフィールを表示しつつ、通知欄には再読み込みの導線を出せます。null を使う場合も、未設定ではなく「今回は取得できなかった」という意味を API 仕様で固定する必要があります。
すべての項目で部分成功を表現する必要はありません。処理を続けると誤った操作につながる場合は、エラーとして処理を中断し、適切なステータスコードを返す方が安全です。
フォールバックを使ってよい条件
フォールバックは、依存先の失敗があっても安全に機能を縮退できる場合に有効です。判断の目安は次のとおりです。
- 代替値が「実際の値」と誤認されない
- 利用者がその値をもとに誤った判断や更新をしない
- 失敗した事実をログ、メトリクス、トレースで追跡できる
- 復旧後に再取得できる
- フォールバックの利用が、仕様としてチームに共有されている
たとえば、ランキングの並び順を既定順に戻す、補助的なおすすめ枠を非表示にする、といった縮退は扱いやすい例です。
一方で、権限、残高、注文可能数、請求状態のように業務判断へ直結する値は、安易に代替値へ置き換えない方が安全です。
エラーを分類して扱う
すべての例外を同じように握りつぶすと、再試行すべき障害と、利用者へ通知すべき不正な入力が混ざります。少なくとも次のように分けると、振る舞いを決めやすくなります。
| 種類 | 例 | 基本的な扱い |
|---|---|---|
| 一時的な障害 | タイムアウト、接続エラー、一時的な 5xx | タイムアウト、限定的な再試行、縮退を検討する |
| 利用制限 | レート制限、同時実行数の上限 | 待機や再試行の案内を返す |
| 呼び出し側の不備 | バリデーションエラー、権限不足 | フォールバックせず原因を返す |
| 恒久的な異常 | 契約不備、仕様不整合、存在しない連携先 | 障害として記録し、明示的に失敗させる |
再試行を入れる場合も、無制限には行いません。短いタイムアウト、回数上限、指数バックオフ、呼び出し先ごとの遮断を組み合わせないと、障害時に依存先へ負荷を集中させます。
UI、BFF、業務サービスで責務を分ける
各層で同じ例外処理を重ねると、どこで失敗が消えたのか分からなくなります。責務を次のように分けると追跡しやすくなります。
- 業務サービス: 通信エラーを分類し、業務上安全なフォールバックの可否を決める
- BFF: 画面を継続表示できるかを判断し、部分成功や画面向けのエラー形式へ変換する
- UI: 空状態、再試行、問い合わせ案内など、利用者に必要な状態を表示する
BFF が下流の例外を無条件で成功レスポンスへ変換すると、画面は壊れにくくなります。しかし、業務サービスが返した失敗の意味も消えます。成功へ変換する場合でも、画面と監視基盤の両方に失敗を残します。
切り分けできる形で情報をつなぐ
ログを各層で増やすだけでは、1つの利用者操作に対応する記録を追えません。UI から外部サービスまで、同じリクエスト ID またはトレース ID を引き継ぎます。
たとえば、問い合わせを受けたときに次の順で確認できる状態を作ります。
- 画面または BFF のログで、対象操作のリクエスト ID を確認する
- 同じ ID を使って、BFF がどの業務サービスを呼んだか確認する
- 業務サービスで、外部サービスへのリクエスト、応答時間、エラー種別を確認する
- フォールバックを使ったか、キャッシュを返したかを確認する
この流れがあれば、画面に表示された結果から調査を始めても、どの境界で期待と実際がずれたかを追えます。
逆に、各層が別々の ID を使う、エラーを成功へ変換した記録がない、キャッシュヒットだけを残して元の取得結果を残さない、といった状態では、調査のたびにコードや DB を広く確認することになります。
障害時の判断を暗黙知にしない
エラーを握りつぶす実装が残る背景には、「この API は補助的な情報だから空でよい」「障害時はこのログを見ればよい」といった判断が、詳しい人の頭の中にだけあることが多いです。
この状態では、新しく参加した人がレスポンスを見ても、正常な0件なのか、障害時の代替値なのかを判断できません。障害対応も、知っている人へ聞くことから始まります。調査に時間がかかるだけでなく、担当者の不在時に復旧判断ができなくなります。
暗黙知を増やさないために、少なくとも次の内容をコードと運用資料に残します。
- API で返す成功、0件、取得失敗の意味
- 部分成功にしてよい項目と、必ず処理を失敗させる項目
- フォールバックの内容と利用条件
- 障害時に確認するログ、メトリクス、ダッシュボード
- キャッシュを使う場合の保存条件と無効化方法
すべてを長いドキュメントにまとめる必要はありません。OpenAPI のレスポンス定義、エラーコード、構造化ログの項目、運用手順に分けて、実装と一緒に更新できる形で残すことが大切です。
たとえば、items: [] が正常な0件だけを表す仕様であれば、API 定義と契約テストで固定します。取得失敗時に部分成功を返すなら、partial_errors の対象、エラーコード、再試行可否を定義します。仕様として読める状態にしておけば、実装を変更する人も障害を調査する人も、同じ前提で判断できます。
ログと監視に残す情報
障害時に必要なのは、スタックトレースだけではありません。少なくとも次の情報を構造化ログやトレースへ残します。
- リクエスト ID とトレース ID
- 呼び出し先と操作名
- エラー種別、HTTP ステータス、タイムアウトの有無
- 再試行回数とフォールバックを使ったかどうか
- 画面や API に与えた影響
「何を返したか」も重要です。たとえば items: [] を返したなら、それが正常な検索結果なのか、部分成功として返した値なのか、フォールバックなのかを記録します。レスポンス本文を無制限に保存するのではなく、結果区分や件数のように調査に必要な情報を構造化して残します。
個人情報、認証情報、決済情報などをそのままログへ出してはいけません。識別子も必要最小限にし、マスキングやアクセス制御を前提にします。
アラートは「例外が1件出た」だけで鳴らすとノイズになりやすいため、失敗率、フォールバック率、依存先ごとの遅延、利用者影響のある失敗数を組み合わせて設計します。フォールバック率の上昇は、HTTP 200 が増えていても障害の兆候を捉えるための重要な指標です。
失敗結果をキャッシュしない
取得失敗を空配列に変換してキャッシュすると、依存先が復旧した後も誤った空状態を返し続けます。
外部 API のタイムアウト
↓
空配列へ変換
↓
空配列をキャッシュ
↓
復旧後も有効期限まで0件として表示
そのため、取得失敗と正常な0件は、キャッシュに保存する前に必ず区別します。空結果をキャッシュするかどうかも、データが本当に存在しないことを確認できた場合だけ検討します。
キャッシュを使う場合は、次の点も決めておくと安全です。
- 正常結果、正常な0件、取得失敗を別の状態として扱う
- 取得失敗の結果は原則キャッシュしない
- 古いキャッシュを返す場合は、鮮度と利用者への表示を決める
- 復旧後に再取得できる経路と無効化方法を用意する
テストで確認すること
正常系だけでは、障害を成功に見せていないかを確認できません。少なくとも次のケースをテストします。
- 正常にデータを取得できる
- 正常に取得でき、0件である
- タイムアウト時に、空結果と区別できる応答になる
- 再試行の上限を超えたときに呼び出しが止まる
- 部分成功時に、表示可能なデータと失敗情報が両方返る
- 取得失敗の結果がキャッシュされない
- フォールバックした事実がログやメトリクスに残る
- 同じリクエスト ID で、UI、BFF、業務サービスの記録をたどれる
特に「0件」と「取得失敗」のレスポンスが異なることは、API 契約テストで固定しておくと変更時に壊れにくくなります。
まとめ
障害時にシステムを止めない設計は重要です。しかし、取得失敗を成功や0件として扱うと、利用者と運用者の両方が障害に気づけなくなります。
フォールバックを導入するときは、次の点を守ると安全です。
- 成功、正常な0件、取得失敗を別の状態として表す
- 業務判断に使う値は、未取得を代替値へ置き換えない
- フォールバックの利用をログ、メトリクス、トレースに残す
- 取得失敗をキャッシュしない
- UI に再試行や一時的な障害を伝える手段を用意する
- 障害時の振る舞いと確認方法を、個人の経験ではなく仕様と運用手順に残す
可用性を高めることと、障害を見えなくすることは別です。安全に縮退しながら、異常だった事実と判断の根拠を残せる設計にすることが、誰でも復旧しやすいシステムにつながります。