HTTP/1.1の「POST」という名の劇薬:非冪等性が引き起こすインフラの悪夢
Web APIの設計やインフラ運用に携わっていると、一度は「二重投稿」という悪魔に頭を抱えた経験があるはずだ。カートに商品が二つ入っている、決済が二度走る、あるいはログが重複して統計が狂う……。
これらすべての元凶は、HTTP/1.1におけるPOSTメソッドの「非冪等(Non-idempotent)性」にある。教科書的には「一度実行しても二度実行しても結果が同じなら冪等、そうでないなら非冪等」と習うが、現場ではそんな生ぬるい言葉では済まされない。POSTは、サーバーの「状態」を不可逆的に変えてしまう「劇薬」なのだ。
今日は、なぜPOSTが危険なのか、そして我々エンジニアがその爆発をどう制御すべきか、プロトコルの深層から紐解いていこう。
—
1. なぜPOSTは「非冪等」という呪いを背負ったのか
RFC 7231において、POSTは「リソースの作成(Create)」や「プロセスの実行」を行うためのメソッドと定義されている。ここでの肝は、リクエスト先が「処理の受け入れ窓口」であるという点だ。
GETやPUT(完全置換)がリソースそのものを指し示すのに対し、POSTは「このデータを処理して、よしなにやっておいてくれ」とサーバーに投げる。サーバーは、その度に新しいリソースIDを払い出したり、カウンターを回したりする。
冪等性(Idempotency)の壁
- GET/PUT/DELETE: 同じリクエストを1回送っても100回送っても、サーバー側の状態(リソースの最終結果)は変わらない。これが「冪等」だ。
- POST: 送るたびに、サーバー内部では「新しい投稿」「新しい課金」「新しい注文」が生成される。これが「非冪等」だ。
ネットワークが完全に信頼できるなら問題はない。だが、現実はパケットロス、タイムアウト、そしてクライアントの「リロードボタン連打」という避けられない人間臭いイベントが待っている。
—
2. 現場で起きる「通信のゆらぎ」を追跡する
クライアントがPOSTを投げ、サーバーが処理を終えてレスポンスを返す。この一連の流れの中で、ネットワーク越しに何が起きているのか。
クライアント -> [POST /api/orders] -> サーバー
クライアント <- [201 Created] <- サーバー (ここでパケットロス発生!)
クライアントは「レスポンスが来ない」と判断し、タイムアウト後に再送する。サーバー側では既に1回目の処理が完了しているのに、2回目のPOSTが届く。サーバーがこれを「別の注文」と認識してしまえば、悲劇の始まりだ。
---
3. 実務的な防衛術:冪等性を担保するための「Idempotency-Key」
では、この呪いをどう解くか。現代のWeb API設計において、最も標準的かつ効果的な手法が「Idempotency-Key(冪等性キー)」の導入だ。
クライアント側で一意なID(UUIDなど)を生成し、HTTPヘッダーに載せてサーバーに送る。サーバーは「このIDの処理は既にやったか?」を確認するキャッシュ層を持つ。
Python (Requests) での実装例
クライアント側でリクエストを投げる際の定石だ。
import requests
import uuid
クライアント側で一意なIDを生成
idempotency_key = str(uuid.uuid4())
headers = {
“Idempotency-Key”: idempotency_key, # サーバーに処理の重複を検知させる
“Content-Type”: “application/json”
}
data = {“item_id”: 123, “quantity”: 1}
再送時も同じキーを使い回すことで、サーバー側で重複を弾く
response = requests.post(“https://api.example.com/orders”, json=data, headers=headers)
print(response.status_code)
サーバー側のロジック(概念)
インフラやアプリケーション層では、以下のような処理を挟む必要がある。
1. `Idempotency-Key` をヘッダーから抽出。
2. Redis等の高速なKVSで `key:status` をチェック。
3. 存在しなければ処理を実行し、結果をキャッシュに格納。
4. 存在すれば「前回のレスポンス」をそのまま返す。
—
4. インフラエンジニアが監視すべきポイント
もし君たちがAPIゲートウェイやロードバランサーを運用しているなら、以下のメトリクスとログに目を光らせてほしい。
- 504 Gateway Timeout 後の POST メソッドの頻度: タイムアウトしたリクエストが即座に再送されている場合、アプリケーション側でキー管理ができていないサインだ。
- 特定のIPからの同一パスへの短時間POST: これがDDoSなのか、単なるバグによる連打なのかを切り分けるために、`X-Forwarded-For` と `Idempotency-Key` の相関をログで見られるようにしておくこと。
最後に:プロトコルと仲良くするために
POSTの非冪等性は欠陥ではなく、Webという巨大な仕組みを動かすための「仕様」だ。サーバーに副作用を求める以上、責任の所在を明確にする必要がある。
「ネットワークは信頼できない」という前提に立ち、クライアントとサーバーで共通の言語(Idempotency-Keyのような約束事)を交わすこと。それこそが、大規模なインフラを支えるエンジニアの矜持であり、バグを未然に防ぐ唯一の防御壁だ。
もし次にWeb APIの設計を任されたら、真っ先に「このPOSTに、二度押し対策はあるか?」と自問自答してほしい。その一言が、深夜の緊急呼び出しを回避する最高のチケットになるはずだ。
コメント