HTTPステータスコードと再試行
POSTに対する転送応答をcurlで追い、転送先へ届いたメソッドと本文を確認します。続いて認証・入力・資源状態・サーバー側の失敗を試し、要求を修正する場合と一時的な障害への対応を考えます。
説明
HTTPの応答には処理結果を表す状態コードと、転送先や認証方法、再試行時期などを伝えるヘッダーがあります。コードだけを見て同じ要求を繰り返すと、転送時に操作の意味が変わったり、原因を残したまま失敗を繰り返したりします。
POSTに対する転送応答をcurlで追い、転送先へ届いたメソッドと本文を確認します。続いて認証・入力・資源状態・サーバー側の失敗を試し、要求を修正する場合と一時的な障害への対応を考えます。
1転送先をヘッダーで確認
まず-Lを付けずに、POSTへの303応答を取得します。
curl -sS -i --dataフォーム形式の本文でPOST 'qty=2' http://127.0.0.1:18080/redirect/303303を返すURL303 See OtherとLocation: /echoが返ります。curlは-Lなしではこの転送先へ進みません。303は結果の参照先を示し、POSTからの追跡ではGETで取得します。
2303を追跡した要求
-Lを付けて同じPOSTを行い、転送先が受け取った内容を表示します。
curl -sS -i -LLocationを追跡 --data 'qty=2' http://127.0.0.1:18080/redirect/303
python3 requests.py last受信した直近二要求最初のPOSTにはqty=2があり、/echoへのGETには本文がありません。303を追うと、POSTの操作を繰り返すのではなく結果を取得します。-X POSTでメソッド名を固定するとcurlの通常の切替に影響するため、ここでは--dataでPOSTを選ばせています。
3307ではメソッドと本文を維持
転送応答を307へ変えます。
curl -sS -i -L --data 'qty=2' http://127.0.0.1:18080/redirect/307メソッドを維持する一時転送
python3 requests.py last転送前後の要求/echoへもPOSTとqty=2が届きます。307 Temporary Redirectは転送時にメソッドを変更しません。転送先で更新が実行される場合は、参照先をGETする303とは効果が異なります。
4302と308の追跡結果
curlの302追跡と、恒久的転送の308を続けて確認します。
curl -sS -L --data 'qty=2' http://127.0.0.1:18080/redirect/302curlの通常の302追跡
curl -sS -L --data 'qty=2' http://127.0.0.1:18080/redirect/308メソッドを維持する恒久転送このcurlでは302を追うPOSTがGETへ変わり、308ではPOSTと本文を維持します。301・302にはPOSTからGETへの変更が許容される経緯があり、メソッド維持を明確に求める転送は307・308で表します。308は恒久的、307は一時的な転送です。
5認証情報がない要求
/adminを認証情報なしで取得します。
curl -sS -i http://127.0.0.1:18080/admin管理用の資源を取得401 UnauthorizedとWWW-Authenticate: Basicが返り、認証情報の提示を要求しています。ここでのUnauthorizedは、認証済み利用者の権限不足を表すものではありません。
6認証済みでも権限がない場合
演習用のreader:demoとadmin:demoを使い、同じURLを取得します。
curl -sS -i -u reader:demo演習用の閲覧者 http://127.0.0.1:18080/admin
curl -sS -i -u admin:demo演習用の管理者 http://127.0.0.1:18080/adminreaderは認証されても管理権限がないため403、adminは200になります。403は要求を理解したうえでの拒否で、認証済みの場合にだけ限るコードではありません。Basicは資格情報をBase64で表し、それ自体では暗号化しないため、運用ではTLSによる保護が必要です。
7対象に許されないメソッド
商品URLへ、ここでは対応していないPOSTを送ります。
curl -sS -i -X POSTこの対象では未対応のメソッド http://127.0.0.1:18080/items/pen405 Method Not AllowedとAllowが返ります。URLが存在しない404とは異なり、対象はあるもののメソッドが許可されていません。Allowに従って目的に合う操作を選び直します。
8構文・値・現在状態の失敗
在庫は2です。壊れたJSON、負の数量、在庫を超える数量を順に送ります。
curl -sS -i -H "Content-Type: application/json" --data-binary '{'閉じていないJSON http://127.0.0.1:18080/reserve
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":-1}不正な数量' http://127.0.0.1:18080/reserve
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":3}在庫2を超える数量' http://127.0.0.1:18080/reserveこのAPIはJSON構文の不正に400、意味上受け付けられない数量に422、現在の在庫との競合に409を返します。400は構文不正専用のコードではありません。具体的な使い分けはAPIの契約と本文を確認し、同じ入力の再送だけで直ると考えないようにします。
9条件を満たす要求へ修正
在庫内の数量2で予約します。
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":2}現在の在庫内へ変更' http://127.0.0.1:18080/reserve201となり、予約数量2、残数0が返ります。先ほどの失敗は在庫を減らしていません。失敗応答の意味と状態更新の有無をAPIの契約として把握すると、どの条件を修正するか判断できます。
10アプリと上流通信の失敗
アプリ内部、上流の不正応答、上流のタイムアウトという三つの失敗応答を取得します。
curl -sS -i http://127.0.0.1:18080/fault/application500の応答
curl -sS -i http://127.0.0.1:18080/fault/upstream-response502の応答
curl -sS -i http://127.0.0.1:18080/fault/upstream-timeout504の応答順に500、502、504です。500は予期しないサーバー側の状態、502はゲートウェイなどが受けた上流の不正応答、504は必要な上流応答が間に合わない状態を表します。X-Request-IDはこの配信側が付けた照合用IDです。コードだけでは、更新が処理済みかどうかまでは確定しません。
11一時的な利用不可と待機時間
一時的に受け付けられない資源を取得します。
curl -sS -i http://127.0.0.1:18080/busy一時的な利用不可の応答503 Service UnavailableにRetry-After: 2が付きます。この形式は再試行前に待つ秒数で、HTTP日時で指定される形式もあります。再試行回数と待機を制限し、更新要求なら重複実行の可能性も確認します。
12案内された待機後に取得
2秒待ってから同じGETを送ります。
sleep 22秒待機
curl -sS -i同じGETで再取得 http://127.0.0.1:18080/busyこの配信側は二回目の要求で200を返します。待機しても必ず成功するわけではありません。自動再試行を設計する場合は上限を決め、最後まで失敗した場合の扱いも用意します。
13HTTP失敗とcurl終了コード
404を通常のcurlと--fail付きで取得し、シェルの終了コードを比較します。
curl -sS -o /dev/null -w 'HTTP=%{http_code}\n'HTTP状態コードを表示 http://127.0.0.1:18080/missing
printf直前の終了コードを表示 'exit=%s\n' "$?"
curl -s --failHTTP失敗を終了コードにも反映 -o /dev/null -w 'HTTP=%{http_code}\n' http://127.0.0.1:18080/missing
printf 'exit=%s\n' "$?"通常はHTTP=404でもcurlの終了コードは0です。通信と応答受信には成功したためです。--failを付けるとHTTP失敗に終了コード22が返ります。シェルで失敗を判定するときは、HTTP状態とクライアントの終了条件を対応させます。
まとめ
転送ではLocationだけでなく、次の要求のメソッドと本文が維持されるかを確認します。
認証、権限、入力、現在状態、上流通信の失敗では、修正すべき条件が異なります。
再試行は待機と上限、処理済みの可能性を考えて設計します。HTTP状態とcurlの終了コードも別々に確認します。
操作履歴と解説を振り返れます。
シェル / curl・HTTP/1.1
curl -sS -i --data 'qty=2' http://127.0.0.1:18080/redirect/303HTTP/1.1 303 See Other Content-Length: 0 Location: /echo
curl -sS -i -L --data 'qty=2' http://127.0.0.1:18080/redirect/303
python3 requests.py lastHTTP/1.1 303 See Other
Content-Length: 0
Location: /echo
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 27
{"method":"GET","body":""}
1 POST /redirect/303 'qty=2'
2 GET /echo ''curl -sS -i -L --data 'qty=2' http://127.0.0.1:18080/redirect/307
python3 requests.py lastHTTP/1.1 307 Temporary Redirect
Content-Length: 0
Location: /echo
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 33
{"method":"POST","body":"qty=2"}
1 POST /redirect/307 'qty=2'
2 POST /echo 'qty=2'curl -sS -L --data 'qty=2' http://127.0.0.1:18080/redirect/302
curl -sS -L --data 'qty=2' http://127.0.0.1:18080/redirect/308{"method":"GET","body":""}
{"method":"POST","body":"qty=2"}curl -sS -i http://127.0.0.1:18080/adminHTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 33
WWW-Authenticate: Basic realm="demo"
{"error":"credentials required"}curl -sS -i -u reader:demo http://127.0.0.1:18080/admin
curl -sS -i -u admin:demo http://127.0.0.1:18080/adminHTTP/1.1 403 Forbidden
Content-Type: application/json
Content-Length: 32
{"error":"admin role required"}
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 19
{"report":"ready"}curl -sS -i -X POST http://127.0.0.1:18080/items/penHTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 31
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
{"error":"method not allowed"}curl -sS -i -H "Content-Type: application/json" --data-binary '{' http://127.0.0.1:18080/reserve
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":-1}' http://127.0.0.1:18080/reserve
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":3}' http://127.0.0.1:18080/reserveHTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 25
{"error":"invalid JSON"}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
Content-Length: 43
{"error":"qty must be a positive integer"}
HTTP/1.1 409 Conflict
Content-Type: application/json
Content-Length: 45
{"error":"insufficient stock","available":2}curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":2}' http://127.0.0.1:18080/reserveHTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 29
{"reserved":2,"remaining":0}curl -sS -i http://127.0.0.1:18080/fault/application
curl -sS -i http://127.0.0.1:18080/fault/upstream-response
curl -sS -i http://127.0.0.1:18080/fault/upstream-timeoutHTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 24
X-Request-ID: r-17
{"error":"application"}
HTTP/1.1 502 Bad Gateway
Content-Type: application/json
Content-Length: 30
X-Request-ID: r-17
{"error":"upstream-response"}
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
Content-Length: 29
X-Request-ID: r-17
{"error":"upstream-timeout"}curl -sS -i http://127.0.0.1:18080/busyHTTP/1.1 503 Service Unavailable
Content-Type: application/json
Content-Length: 36
Retry-After: 2
{"error":"temporarily unavailable"}sleep 2
curl -sS -i http://127.0.0.1:18080/busyHTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 19
{"status":"ready"}curl -sS -o /dev/null -w 'HTTP=%{http_code}\n' http://127.0.0.1:18080/missing
printf 'exit=%s\n' "$?"
curl -s --fail -o /dev/null -w 'HTTP=%{http_code}\n' http://127.0.0.1:18080/missing
printf 'exit=%s\n' "$?"HTTP=404 exit=0 HTTP=404 exit=22
演習完了