1. 課題とユースケース
PATCH /profiles/42は何年もの間、本番環境で稼働しています。成功時には、固定のボディ{ "success": true }とともに200 OKを返します。Webアプリ、モバイルアプリ、サードパーティのソフトウェア開発キット(SDK)、そして自動化ジョブがすべてこれを呼び出しています。ボディにはビジネスデータが含まれていないため、チームは代わりに204 No Contentを返したいと考えています。
ステータス行を変更すること自体は簡単です。古いクライアントはすべての2xxレスポンスに対してresponse.json()を呼び出す可能性があり、ゲートウェイはボディからフィールドを抽出しているかもしれず、ダッシュボードは200のみを成功としてカウントしているかもしれません。この移行では、新旧のクライアントを共存させ、互換性の問題が広がる前にサーバー側で迅速にロールバックできるようにする必要があります。
2. 面接官がテストしていること
- ステータスの変更を、サーバーの1行の修正ではなく、レスポンスコントラクトの移行として扱っているかどうか。
- ブラウザ、モバイルアプリ、SDK、プロキシ、監視ツール、リトライミドルウェアを実際のコンシューマーとして棚卸ししているかどうか。
- バージョニングや
Preferネゴシエーションを通じて、200と204を共存させることができるかどうか。 - ロールアウトのメトリクス、停止条件、およびクライアントのダウングレードを必要としないサーバー側のロールバックを定義しているかどうか。
- 204レスポンスがヘッダーセクションの後に終了し、コンテンツやトレーラーを含めることができないことを理解しているかどうか。
3. 移行前に回答すべき質問
- どのクライアントが常にJSONをパースし、どのクライアントが
response.okまたは2xxクラスのみを検査しているか? - ログ収集ツール、ゲートウェイスクリプト、生成されたSDKの戻り値の型を含め、成功オブジェクトは本当にどこからも読み取られていないか?
- クライアントが自動的にリトライし、書き込み成功の後のパースエラーがリクエストの重複につながる可能性はあるか?
- すべてのクライアントをアップグレードできるか、それとも両方のコントラクトを長期間利用可能にしておく必要があるか?
- ETag、レート制限フィールド、トレース識別子など、どのヘッダーを維持する必要があるか?
4. 30秒の回答フレームワーク
私ならまずコンシューマーの棚卸しを行い、コントラクトテストを使用して200 JSONボディへのあらゆる依存関係を洗い出します。移行中、200はデフォルトのまま維持されます。対応可能なクライアントは、新しいAPIバージョンまたはPrefer: return=minimalを通じて最小限のレスポンスを選択(オプトイン)します。サーバーがそれに応じる場合、204を返してPreference-Appliedを報告します。内部トラフィックからリスクの低い既知のクライアントバージョンへとロールアウトを進め、パース失敗、重複書き込み、リトライ、クライアントごとの成功率を監視します。ロールバックはサーバー側のスイッチを200に戻すだけで行えます。移行が完了するまでJSONシリアライズパスがそのまま維持されているためです。
5. 段階的な移行計画
ステップ1:クライアントの対応能力の棚卸しを作成する
各呼び出し元について、その所有者、バージョン、HTTPライブラリ、成功判定、レスポンスパーサー、リトライポリシーを記録します。無条件のresponse.json()、厳密なstatus === 200比較、生成されたSDKの戻り値の型、ゲートウェイによるbody.successの読み取りを検索します。不明な呼び出し元やバージョン管理されていない呼び出し元は200のままにします。互換性の証拠がない場合は、204のロールアウトを行いません。
ステップ2:クライアントが両方の成功コントラクトを受け入れるようにする
まずクライアント側のサポートをリリースします。対応可能なクライアントは、合意された2xxレスポンスを受け入れ、パースする前に204または空のボディをチェックし、リクエストの成功とJSONデコードを分離します。コントラクトテストでは、200 + JSONと204 + empty bodyの両方を渡してテストします。この順序を逆にすると、書き込みが成功しているにもかかわらずクライアント側でパースエラーが発生し、重複リトライにつながるリスクがあります。
ステップ3:2つのコントラクトを共存させる方法を選択する
意図的な破壊的変更の場合、新しいAPIバージョンでは常に204を返し、古いバージョンでは200を維持することができます。ルートと操作が同じままである場合、RFC 7240のPreferenceネゴシエーションも選択肢の1つです。対応可能なクライアントはPrefer: return=minimalを送信し、サーバーは204とPreference-Applied: return=minimalでそれに応じることができます。表現を必要とする呼び出し元はPrefer: return=representationを送信するか、デフォルトの200の動作を維持します。レスポンスがキャッシュ可能な場合は、Vary: Preferを正しく宣言します。
ステップ4:ランダムなリクエストではなく、クライアント単位でロールアウトする
まずテスト環境と内部の呼び出し元でこの動作を有効にし、その後、互換性があると判明しているクライアントバージョンにのみ拡大します。各クライアントまたはアカウントを安定したコホートに維持し、200と204が交互に発生しないようにします。拡大する前に、各段階で完全なビジネスサイクルを観察します。HTTPメトリクスに問題がない短期間のデータだけでは不十分です。
ステップ5:プロトコル変更によって発生しうる障害を監視する
サーバー側では、200、204、5xx、およびリトライの回数をクライアントバージョンごとに分類します。クライアント側では、空ボディのパース失敗、リクエスト成功後に表示されるエラーUI、重複送信を記録します。特に非冪等な操作について、完了した書き込みとリクエストのリトライを比較します。集約された2xx率は互換性の障害を隠してしまうため、アラートはクライアントバージョンとロールアウトコホートを特定できるようにする必要があります。
ステップ6:即時ロールバックを維持し、移行を完了する
移行期間中、元のJSONシリアライズパスをサーバー側のスイッチの背後に保持しておきます。停止条件が発動した場合は、グローバルに200を復元します。両対応のクライアントはダウングレードなしで動作し続けます。サポートされているすべてのクライアントが最小互換バージョン以上になり、合意された観察期間中に古いトラフィックが存在せず、SDK、プロキシ、コントラクトテスト群が引き続きパスした後にのみ、古いパスを削除します。
6. 高品質な回答例
私はこれをレスポンスコントラクトの移行として扱います。まず、すべてのコンシューマーを棚卸しし、常にJSONをパースするコード、厳密に200をチェックするコード、またはbody.successを読み取るコードを特定します。サーバーの動作を変更する前に、200 JSONとボディなしの204の両方を受け入れるクライアントをリリースします。サーバーはデフォルトとして200を維持し、対応クライアントはAPIバージョンまたはPrefer: return=minimalによって204を選択し、Preference-Appliedによってそれが確認されます。ロールアウトはクライアントバージョンごとに進め、パース失敗、リトライ、重複書き込みを主要なシグナルとします。サポートされているすべてのクライアントが移行するまで200のシリアライズパスを利用可能にしておくため、ロールバックはクライアントのリリースではなくサーバー側のスイッチで対応できます。
7. よくある間違い
- 200を直接204に変更する → 古いクライアントが空のボディのパース中に失敗する → 204を有効にする前に両対応のクライアントをリリースする。
- HTTPエラー率のみを監視する → 204も成功レスポンスであるため、パース失敗はサーバーの5xxとして現れない → クライアントのパース、リトライ、重複書き込みのメトリクスを追加する。
- リクエストごとにロールアウトをランダム化する → 1つのクライアントが不安定なコントラクトを受け取ることになる → クライアントバージョンやその他の安定した識別情報でコホートを作成する。
- 結果を報告せずに
Preferを受け入れる → クライアントはPreferenceが受け入れられたかどうかを判別できない →Preference-Appliedを返し、デフォルトの動作を定義する。 - JSONシリアライズをすぐに削除する → ロールバックにコードリリースが必要になる → 移行期間が終了するまで古いパスを保持する。
- 自動リトライ動作を無視する → パースエラーによって書き込み成功が失敗に見せかけられる → 冪等性キー、リトライミドルウェア、重複送信メトリクスを確認する。
8. フォローアップの質問と回答
フォローアップ1:なぜすべてのクライアントを一度に切り替えないのですか?
サーバーは書き込みが成功したことを証明できますが、デプロイされているすべてのクライアントが204のボディを正しく処理できるかを証明することはできません。常にJSONをパースする古いバージョンが1つでもあると、プロトコル上の成功がユーザーに見える障害に変わってしまいます。クライアントの互換性を優先し、その後にバージョンごとのロールアウトを行うことで、障害の境界を観測可能な状態に保つことができます。
フォローアップ2:バージョニングとPreferのどちらを選択すべきですか?
新しいバージョンは永続的なコントラクトの変更に適しており、理解しやすいですが、バージョンのライフサイクル管理の手間が増えます。Preferは、返される表現と最小限のレスポンスの両方が有効であるような操作に適しています。サーバーはPreferenceを無視する場合があるため、クライアントには文書化されたデフォルトが必要であり、Preference-Appliedを検査する必要があります。どちらのアプローチも、User-Agent文字列から推測するより信頼性があります。
フォローアップ3:204レスポンスにはどのような情報を保持できますか?
ETag、トレース識別子、レート制限フィールドなどのヘッダーを保持できます。メッセージコンテンツやトレーラーを含めることはできません。したがって、クライアントは「JSONがないこと」と「メタデータがないこと」を別の問題として扱う必要があります。
フォローアップ4:200互換パスを安全に削除できるのはいつですか?
サポートされているすべてのクライアントがデュアルレスポンス処理をリリース済みであり、テレメトリで古いバージョンのトラフィックがないことが示され、SDK、プロキシ、自動化、ロールバックのテストに合格している必要があります。ユーザーは古いバージョンを長期間使い続ける可能性があるため、App Storeでのリリースだけでは不十分です。