一覧WB-03-2 · HTTPの操作と応答

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を返すURL

303 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/admin

readerは認証されても管理権限がないため403、adminは200になります。403は要求を理解したうえでの拒否で、認証済みの場合にだけ限るコードではありません。Basicは資格情報をBase64で表し、それ自体では暗号化しないため、運用ではTLSによる保護が必要です。

7対象に許されないメソッド

商品URLへ、ここでは対応していないPOSTを送ります。

curl -sS -i -X POSTこの対象では未対応のメソッド http://127.0.0.1:18080/items/pen

405 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/reserve

201となり、予約数量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

1
curl -sS -i --data 'qty=2' http://127.0.0.1:18080/redirect/303
HTTP/1.1 303 See Other
Content-Length: 0
Location: /echo
2
curl -sS -i -L --data 'qty=2' http://127.0.0.1:18080/redirect/303 python3 requests.py last
HTTP/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 ''
3
curl -sS -i -L --data 'qty=2' http://127.0.0.1:18080/redirect/307 python3 requests.py last
HTTP/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'
4
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"}
5
curl -sS -i http://127.0.0.1:18080/admin
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 33
WWW-Authenticate: Basic realm="demo"

{"error":"credentials required"}
6
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/admin
HTTP/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"}
7
curl -sS -i -X POST http://127.0.0.1:18080/items/pen
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 31
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS

{"error":"method not allowed"}
8
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/reserve
HTTP/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}
9
curl -sS -i -H "Content-Type: application/json" --data-binary '{"qty":2}' http://127.0.0.1:18080/reserve
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 29

{"reserved":2,"remaining":0}
10
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-timeout
HTTP/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"}
11
curl -sS -i http://127.0.0.1:18080/busy
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Content-Length: 36
Retry-After: 2

{"error":"temporarily unavailable"}
12
sleep 2 curl -sS -i http://127.0.0.1:18080/busy
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 19

{"status":"ready"}
13
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

演習完了