Skip to content

冪等鍵

為避免網路重試導致重複開卡 / 重複扣款,寫介面要求客戶端在每次請求中提供 Idempotency-Key 頭。

哪些介面需要

介面是否需要冪等鍵
/api/v1/openapi/card/apply✅ 必填
/api/v1/openapi/card/recharge✅ 必填
所有 /list 介面、/info 介面❌ 不需要

請求頭

http
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 不同 body409 openapi_idempotency_key_conflict

服務端行為

同 key + 同 body

返回之前那次響應的副本(原狀態碼 + 原 data),即使第一次還在處理中也會等待並返回最終結果。不會觸發第二次開卡。

回放的響應會附帶一個額外的響應頭,便於客戶端區分是否是回放:

http
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 模式

python
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 與任務記錄繫結

python
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)

json
{
  "code": 400,
  "message": "缺少 Idempotency-Key 請求頭",
  "message_key": "openapi_idempotency_key_required",
  "data": null
}

衝突(409)

json
{
  "code": 409,
  "message": "Idempotency-Key 衝突",
  "message_key": "openapi_idempotency_key_conflict",
  "data": null
}

採用 MIT 等價條款釋出