幂等键
为避免网络重试导致重复开卡 / 重复扣款,写接口要求客户端在每次请求中提供 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
}