「一度押せば安全、二度押せば地獄」――HTTP冪等性を理解しない者はWeb APIを語るべからず
ネットワークエンジニアとして現場を渡り歩いていると、若手から「APIのレスポンスが返ってこないからリトライ処理を入れたい」という相談をよく受けます。その時、私は必ずこう聞き返します。「そのメソッド、冪等(Idempotent)か?」と。
HTTPの冪等性は、単なる理論上の定義ではありません。大規模なトラフィックが流れるシステムにおいて、システム整合性を守るための「最後の防衛線」です。今日は、RFCの冷徹な仕様と、現場で血を流しながら学んだ「冪等性の実装術」について語りましょう。
—
1. 冪等性(Idempotency)とは何か?
端的に言えば、冪等性とは「同じ操作を何回繰り返しても、サーバー側の状態が一度だけ実行した時と同じになる」という性質のことです。
数学的な概念ですが、Web APIにおいては「リトライしても副作用(データの書き換えや重複作成)が発生しないこと」と読み替えてください。これが担保されていないと、ネットワークの瞬断やタイムアウトによる自動リトライが、データベースを破壊するトリガーになります。
冪等なメソッドとそうでないメソッド
- GET / HEAD: サーバーの状態を一切変更しない(安全なメソッド)。当然、何回叩いても副作用はないので「冪等」です。
- PUT: 指定したリソースを「その内容で置き換える」。同じ内容を何度PUTしても最終状態は同じなので「冪等」です。
- DELETE: 指定したリソースを削除する。1回目も2回目も「対象が存在しない状態」になるため「冪等」です。
- POST: 非冪等。リソースの「作成」や「処理の実行」を担うため、1回目と2回目でサーバーの状態(IDの採番や履歴の追記)が確実に異なります。
—
2. 現場で直面する「非冪等性」の罠
なぜPOSTは非冪等なのか。それは、POSTが「リソースの作成」という新しい履歴を生む操作だからです。
例えば、銀行の送金APIをPOSTで実装し、レスポンスが来る前にタイムアウトしたとしましょう。クライアントが「届かなかったからもう一度!」とリトライすれば、サーバーは2回分のお金を引き落としてしまいます。これが「二重送金」という悲劇の正体です。
リトライ時の副作用回避策:冪等キー(Idempotency-Key)の実装
POSTなどの非冪等な操作を、論理的に「冪等」として扱うためのデファクトスタンダードが冪等キーです。
クライアント側で一意なID(UUIDなど)を生成し、HTTPヘッダーに載せて送信します。サーバー側では、そのキーをRedis等の高速なキャッシュストアに保存し、リクエストが重複していないかをチェックします。
サーバー側(疑似コード・Python/FastAPI風):
async def process_payment(request: Request, body: PaymentRequest):
# クライアントから送られてきた冪等キーを取得
idempotency_key = request.headers.get(“Idempotency-Key”)
# 既に処理済みかチェック
if await cache.exists(idempotency_key):
return await cache.get(idempotency_key) # 過去のレスポンスをそのまま返す
# 処理実行(データベース更新など)
result = await bank_service.transfer(body)
# 結果をキャッシュに保存して冪等性を担保
await cache.set(idempotency_key, result, expire=3600)
return result
—
3. 実務で役立つデバッグと検証のヒント
開発中、意図した通りの冪等性が保たれているか確認するために、`curl`を使って強制的にリトライを繰り返すテストを行うことがあります。
curlによる検証例
冪等キーを固定して、連続してリクエストを送ってみる
1回目は201 Created、2回目は200 OK(キャッシュ)が返ってくるか確認する
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://api.example.com/v1/payments \
-H “Idempotency-Key: $IDEMPOTENCY_KEY” \
-H “Content-Type: application/json” \
-d ‘{“amount”: 1000, “to”: “user_b”}’
運用上の注意点:PUT vs PATCH
ここを誤解しているエンジニアが非常に多いのですが、PATCHは冪等ではありません。
PUTは「このリソースをAにする(置換)」という命令ですが、PATCHは「このリソースのこの部分を増やす/変更する(差分適用)」という命令だからです。
「今の値に1を足す」というPATCHリクエストを2回送れば、結果は+2になります。PATCHを実装する際は、その差分更新がリトライによって壊れないよう、設計段階で慎重に考慮してください。
—
まとめ:ネットワークは常に不完全であると知れ
通信プロトコルは、どんなに安定した回線でも必ず途切れます。パケットが途中で消えることもあれば、サーバーが処理を終えた後にACKが届かないこともあります。
「リトライ処理を書くときは、そのメソッドが冪等かどうかを確認し、非冪等であれば冪等キーを設計に組み込む」
この原則を守るだけで、あなたのAPIは「壊れにくい、堅牢なシステム」へと一歩近づきます。教科書の仕様を読み込むだけでなく、パケットが失敗した瞬間のサーバーの挙動を常に想像してください。それこそが、シニアエンジニアへの第一歩です。
何かトラブルがあれば、いつでもログを持って相談に来てください。現場の最前線で待っていますよ。
コメント