冪等鍵
為避免網路重試導致重複開卡 / 重複扣款,寫介面要求客戶端在每次請求中提供 Idempotency-Key 頭。
哪些介面需要
| 介面 | 是否需要冪等鍵 |
|---|---|
/api/v1/openapi/card/apply | ✅ 必填 |
/api/v1/openapi/card/recharge | ✅ 必填 |
所有 /list 介面、/info 介面 | ❌ 不需要 |
請求頭
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000| 約束 | 值 |
|---|---|
| 長度 | ≤ 128 字元 |
| 字元集 | URL-safe(推薦 UUID v4 / ULID) |
| 去重視窗 | 24 小時 |
| 缺失返回 | 400 openapi_idempotency_key_required |
| 超長返回 | 400 openapi_idempotency_key_too_long |
| 非法字元 | 400 openapi_idempotency_key_invalid_chars |
| 同 key 不同 body | 409 openapi_idempotency_key_conflict |
服務端行為
同 key + 同 body
返回之前那次響應的副本(原狀態碼 + 原 data),即使第一次還在處理中也會等待並返回最終結果。不會觸發第二次開卡。
回放的響應會附帶一個額外的響應頭,便於客戶端區分是否是回放:
X-Idempotent-Replay: true識別回放
X-Idempotent-Replay: true 僅用於提示,無論有沒有這個頭,響應體本身才是權威結果。該頭主要用於打點和排錯("這次請求其實已經到達服務端了,是網路抖動導致客戶端重試")。
只有 2xx 響應會被快取
服務端只快取原始呼叫返回 2xx 的響應體。若首次呼叫返回 4xx(如 insufficient_balance)或 5xx,相同 key + 相同 body 的下次請求會重新執行,不存在"回放"。
換句話說:冪等保護的是"成功響應丟失後的安全重試",不是"反覆重試已經業務失敗的請求"。
響應快取上限
服務端最多快取 256 KB 的響應體。當前所有 OpenAPI 響應都遠低於此上限,正常使用不需關心 —— 僅作完整性披露。
同 key + 不同 body
返回 409 openapi_idempotency_key_conflict,拒絕處理。客戶端應換 key 重試或修正 body。
不同 key + 同 body
視為兩次獨立請求,會觸發兩次開卡 / 兩次扣款。這是設計如此 —— 冪等基於 key,不是 body。
不要用 body hash 當 key
不要用"請求體 hash"作為 Idempotency-Key —— 這會讓"使用者連續兩次請求同金額充值"被去重為一次。key 應代表一次業務意圖,每次新業務請求生成新 key。
推薦用法
客戶端 SDK 模式
import uuid
def apply_card(header_id, package_id):
key = str(uuid.uuid4()) # 每次新業務一個新 key
return call("/api/v1/openapi/card/apply",
{"header_id": header_id, "package_id": package_id},
idempotency_key=key)非同步任務模式
如果你的"開卡任務"會在網路失敗後被任務佇列重試,把 key 與任務記錄繫結:
def open_card_task(task_id, header_id, package_id):
# 同一任務的所有重試用同一個 key
key = f"openapi:apply:{task_id}"
return call("/api/v1/openapi/card/apply", {...}, idempotency_key=key)這樣即使任務被重試 N 次,Coinepay 端也只會真正開卡一次。
與簽名的關係
Idempotency-Key 頭不參與簽名輸入構造(簽名只覆蓋 method/path/query/timestamp/nonce/body sha256)。但每次請求仍需新的 nonce,因為 nonce 是用來防簽名重放的,與冪等是兩套機制:
| 機制 | 防的是 |
|---|---|
| Nonce | 攻擊者抓到簽名後重放請求 |
| Idempotency-Key | 客戶自己網路失敗後安全重試 |
二者協同:客戶重試同一筆業務時,用同一個 Idempotency-Key + 新的 Nonce + 新的 Timestamp + 新的 Signature。
錯誤響應示例
缺失(400)
{
"code": 400,
"message": "缺少 Idempotency-Key 請求頭",
"message_key": "openapi_idempotency_key_required",
"data": null
}衝突(409)
{
"code": 409,
"message": "Idempotency-Key 衝突",
"message_key": "openapi_idempotency_key_conflict",
"data": null
}