Webhookとイベントのidempotency
TL;DR(ドクター)
良いWebhookは署名された(HMAC/mTLS)で、受信者に指数関数的なバックアップと重複除外を持つ、少なくとも1回のモデルで配信される、要約された、idempotentイベントです。エンベロープ('event_id'、 'type'、 'ts'、 'version'、 'attempt'、' signature')、タイムウィンドウ(≤ 5分)、レスポンスコード、リトレイ、DLQ、ステータスエンドポイントに同意します。
1)役割と配達モデル
送信者(you/provider):イベント、サインを生成し、最大2xxを配信しようとします。3xx/4xx/5xx(明示的に「受け入れない」を除く)で再試行し、DLQをリードし、リプレイAPIを提供します。
受信者(パートナー/サービス):署名/タイムウィンドウをチェックし、dedupとidempotent処理を行い、正しいコードで応答し、/status と/ack replayを'event_id'で提供します。
保証:少なくとも一度。受信者は、重複と並べ替えを処理できる必要があります。
2)イベントの封筒
json
{
"event_id": "01HF7H9J9Q3E7DYT5Y6K3ZFD6M",
"type": "payout.processed",
"version": "2025-01-01",
"ts": "2025-11-03T12:34:56.789Z",
"attempt": 1,
"producer": "payments",
"tenant": "acme",
"data": {
"payout_id": "p_123",
"status": "processed",
"amount_minor": 10000,
"currency": "EUR"
}
}
必須フィールドは'event_id'、 'type'、 'version'、 'ts'、 'attempt'です。
進化のルール:フィールドの追加;delete/change types-新しい'バージョン'でのみ。
3)セキュリティ: 署名とバインディング
3.1 HMAC署名(デフォルト推奨)
タイトル:
X-Signature: v1=base64(hmac_sha256(<secret>, <canonical>))
X-Timestamp: 2025-11-03T12:34:56Z
X-Event-Id: 01HF7...
正規文字列:
<timestamp>\n<method>\n<path>\n<sha256(body)>
受信者に確認してください:
- abs(現在 'X-Timestamp') 300s
- 'X-Event-Id'は前に処理されない(dedup)
- 'X-Signature'マッチ(時間安全比較)
3.2追加。対策
非常に敏感なwebhookのためのmTLS。
IP/ASN allow-list。
Webhookがコールバックを開始する場合、送信者制約のためのDPoP(オプション)。
4) Idempotencyと重複除外
4.1イベントのidempotency
同じ'event_id'を持つイベントは、再度ステートを変更してはいけません。受信者:- TTL ≥ 24-72時間のidempotentキャッシュ(KV/Redis/DB)に「event_id」を格納します。
- 再リターンの処理結果(成功/エラー、アーティファクト)を保存します。
4.2コマンドidempotency(コールバック)
WebhookがクライアントにAPIをプルさせた場合(例:「confirm payout」)、 RESTコールで'Idempotency-Key'を使用して、結果をサービス側に保存します(正確に一度の結果)。
KVモデル(最低):
key: idempotency:event:01HF7...
val: { status: "ok", processed_at: "...", handler_version: "..." }
TTL: 3d
5)レトライとバックオフ
推奨プロット(ジッタ付き指数):- '5s、 15s、 30s、 1m、 2m、 5m、 10m、 30m、 1h、 6h、 12h、 24h'(その後、毎日N日まで)
- 2xx-成功、リトレイを停止します。
- '400/ 401/403/404/422'-署名/フォーマットがok(クライアントエラー)の場合は再送できません。
- '429'-retrayim by 'Retry-After'またはバックオフ。
- 5xx/network-retrayim。
送信ヘッダ:'User-Agent'、 'X-Webhook-Producer'、 'X-Attempt'。
6)受信機側処理
疑似パイプライン:pseudo verify_signature()
if abs(now - X-Timestamp) > 300s: return 401
if seen(event_id):
return 200 // идемпотентный ответ
begin transaction if seen(event_id): commit; return 200 handle(data) // доменная логика mark_seen(event_id) // запись в KV/DB commit return 200
Transactionality: 「seen」ラベルは、失敗時の二重処理を避けるために、操作の効果(または結果を修正した後)でアトミックに設定する必要があります。
7)順序およびスナップショットの保証
注文は保証されていません。'data'の't'とドメイン'seq'/'version'を使用して関連性を検証します。
長いlags/lossの場合は、送信者でadd/replay、受信者で/resyncを実行します(/IDウィンドウでスナップショットとデルタを取得します)。
8)状態、再生およびDLQ
8.1送信者エンドポイント
'POST/webhooks/replay'-'event_id'リストまたはタイムウィンドウで。
'GET/webhooks/events/: id'-ソースパッケージと試行履歴を表示します。
DLQ: 「dead」イベント(リトレイ制限が使い果たされました)→別々のストレージ、アラート。
8.2受信者エンドポイント
'GET/ webhooks/status/:event_id'-'seen=true/false'、'processed_at'、'handler_version'。
'POST/webhooks/ack'-(オプション)DLQからの手動処理の確認。
9)エラー契約(受信機の応答)
http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Retry-After: 120
X-Trace-Id: 4e3f...
{
"error": "invalid_state",
"error_description": "payout not found",
"trace_id": "4e3f..."
}
推奨事項:常に明確なコードを返し、可能であれば'Retry-After'を返します。詳細なセキュリティ情報は返さないでください。
10)監視およびSLO
メトリック(送信者):- 配信p50/p95、成功率、リトレイ/イベント、ドロップレートDLQ、シェア2xx/4xx/5xx、ディレイウィンドウ2xxまで。
- fail rate (signature/time)、 dup-rate、 latency handler p95、 5xxを確認します。
- 配達:≥ 99。イベントの9%は2xx <3 c p95を受け取ります(最初の成功した試みの後)。
- 暗号検証:署名検証≤ 2-5 ms p95。
- Dedup: 0の繰り返しエフェクト(ドメインレベルで正確に1回の結果)。
11)データセキュリティとプライバシー
Webhookの本体にPAN/PIIを送信しないでください。IDを使用してから、承認されたAPIに対して詳細をプルします。
ログの機密フィールドをマスクします。TTLでイベントボディを最小限に保存します。
DLQの保存と再生を暗号化します。
12)バージョン管理と互換性
'version' (envelope)およびtransit: '/webhooks/v1/payments'のバージョン。
新しいフィールドはオプションです。削除-'Sunset'期間の後のみ。
機械読み取り可能な変更履歴(自動チェック用)の変更を文書化します。
13)テストケース(UATチェックリスト)
- 同じ'event_id'→1つのエフェクトと'200'を重複させる。
- 署名:正しいキー、間違ったキー、古いキー(回転)、'X-Timestamp'ウィンドウの外。
- バックオフ:受信者は'429'に'Retry-After'→正しい一時停止を与えます。
- 注文:イベント'……処理された'前に来る'……created'→正しい処理/待ち。
- エフェクトと'mark_seen'→atomicity/repeatの間のレシーバでのデータベース障害。
- DLQおよび手動リプレイ→成功した配達。
- 大量の「嵐」(プロバイダがパックを送信)→損失なし、制限はクリティカルを抑制しません。
14)ミニスニペット
送信者署名(擬似):pseudo body = json(event)
canonical = ts + "\n" + "POST" + "\n" + path + "\n" + sha256(body)
sig = base64(hmac_sha256(secret, canonical))
headers = {"X-Timestamp": ts, "X-Event-Id": event.event_id, "X-Signature": "v1="+sig}
POST(url, body, headers)
チェックと宛先(擬似):
pseudo assert abs(now - X-Timestamp) <= 300 assert timingSafeEqual(hmac(secret, canonical), sig)
if kv.exists("idemp:"+event_id): return 200
begin tx if kv.exists("idemp:"+event_id): commit; return 200 handle(event.data) // доменная логика kv.set("idemp:"+event_id, "ok", ttl=259200)
commit return 200
15)頻繁なエラー
重複排除→繰り返し効果なし(二重リファンド/ペイアウト)。
タイムスタンプ/ウィンドウのない署名→リプレイの脆弱性。
すべてのパートナーに1つのHMACシークレットを格納します。
Responses '200'クラッシュイベントの結果→損失を修正する前に。
セキュリティの詳細を回答/ログに「洗い流す」。
DLQ/リプレイの欠如-インシデントは解決できません。
16)実装チートシート
セキュリティ:HMAC v1+'X-Timestamp'+'X-Event-Id'、ウィンドウ≤ 5分;必要に応じてmTLS/IP allow-list。
'event_id'、 'type'、 'version'、 'ts'、 'attempt'、' data'。
配信:最低1回、ジッタ付きバックオフ、'Retry-After'、 DLQ+リプレイAPI。
Idempotency: KV-cache 24-72 h、エフェクト+'mark_seen'の原子固定。
観測可能性:配信、署名、重複した指標;trace_idを指定します。
ドキュメント:バージョン、レスポンスコード、例、UATチェックリスト。
履歴書のサマリー
永続的なWebhookは、3つのクジラに基づいて構築されています。署名付きの封筒、少なくとも1回の配達、およびidempotent処理です。コントラクトを正式化し、HMAC/mTLSとタイムウィンドウを有効にし、retrai+DLQを実装してリプレイし、idempotentラベルを保存し、エフェクトをアトミックにキャプチャします。その後、ネットワーク障害、ロードピーク、まれな「運命の重複」でも、イベントは信頼性が維持されます。