api-messaging-webhooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWebhook Patterns
Webhook模式
Quick Guide: Verify signatures with HMAC-SHA256 using+crypto.createHmacon the raw body bytes -- never parsed JSON. Enforce idempotency by storing processed webhook IDs. Protect against replay attacks with timestamp validation. Return 200 immediately, process asynchronously. When sending, use exponential backoff with jitter and move exhausted retries to a dead letter queue.crypto.timingSafeEqual
<critical_requirements>
快速指南: 使用+crypto.createHmac对原始请求体字节进行HMAC-SHA256签名验证——绝不要使用解析后的JSON。通过存储已处理的Webhook ID来实现幂等性。通过时间戳验证防止重放攻击。立即返回200状态码,异步处理请求。发送Webhook时,使用带抖动的指数退避策略,并将重试耗尽的请求移入死信队列。crypto.timingSafeEqual
<critical_requirements>
CRITICAL: Before Using This Skill
重要提示:使用本技能前
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
(You MUST verify signatures against the RAW request body -- never parsed/re-serialized JSON)
(You MUST use for signature comparison -- never which leaks timing information)
crypto.timingSafeEqual===(You MUST return 2xx immediately and process webhooks asynchronously -- synchronous processing causes timeouts and duplicate deliveries)
(You MUST enforce idempotency by checking a stored webhook ID before processing -- retries WILL send the same event multiple times)
</critical_requirements>
Auto-detection: webhook, webhooks, HMAC, signature verification, createHmac, timingSafeEqual, webhook-signature, webhook-id, webhook-timestamp, idempotency key, replay attack, exponential backoff, dead letter queue, event routing, webhook handler, webhook endpoint, webhook delivery, webhook retry
When to use:
- Receiving webhooks from external providers (payment processors, version control, messaging platforms)
- Building a webhook-sending system to notify external consumers of events
- Implementing signature verification for incoming webhook payloads
- Adding retry logic with exponential backoff for outbound webhook delivery
- Routing webhook events to type-safe handlers by event type
When NOT to use:
- Real-time bidirectional communication (use WebSockets or Server-Sent Events)
- Internal service-to-service communication where both sides are trusted and co-deployed
- Simple polling scenarios where the consumer controls the fetch timing
Key patterns covered:
- HMAC-SHA256 signature verification with timing-safe comparison
- Replay attack protection with timestamp validation
- Idempotency via stored webhook IDs with TTL
- Type-safe event routing with discriminated unions
- Outbound webhook delivery with exponential backoff and jitter
- Dead letter queues for exhausted retries
- Raw body handling to preserve signature integrity
Detailed Resources:
- examples/core.md - Receiving, signature verification, idempotency, event routing
- examples/sending.md - Sending webhooks, retry logic, delivery tracking
- reference.md - Decision frameworks, header conventions, status code handling
<philosophy>
所有代码必须遵循CLAUDE.md中的项目约定(kebab-case命名、具名导出、导入顺序、、具名常量)import type
(必须针对原始请求体验证签名——绝不能使用解析/重新序列化后的JSON)
(必须使用进行签名比较——绝不能使用,否则会泄露时序信息)
crypto.timingSafeEqual===(必须立即返回2xx状态码并异步处理Webhook——同步处理会导致超时和重复投递)
(必须通过检查存储的Webhook ID来实现幂等性——重试机制必然会多次发送同一事件)
</critical_requirements>
自动检测关键词: webhook、webhooks、HMAC、签名验证、createHmac、timingSafeEqual、webhook-signature、webhook-id、webhook-timestamp、幂等键、重放攻击、指数退避、死信队列、事件路由、webhook处理器、webhook端点、webhook投递、webhook重试
适用场景:
- 接收来自外部服务商的Webhook(支付处理器、版本控制系统、消息平台)
- 构建Webhook发送系统,向外部消费者通知事件
- 为传入的Webhook负载实现签名验证
- 为 outbound Webhook投递添加带指数退避的重试逻辑
- 根据事件类型将Webhook事件路由到类型安全的处理器
不适用场景:
- 实时双向通信(使用WebSockets或Server-Sent Events)
- 双方互信且共同部署的内部服务间通信
- 消费者控制拉取时机的简单轮询场景
涵盖的核心模式:
- 采用时序安全比较的HMAC-SHA256签名验证
- 基于时间戳验证的重放攻击防护
- 带TTL的Webhook ID存储实现幂等性
- 基于区分联合的类型安全事件路由
- 带指数退避和抖动的Outbound Webhook投递
- 用于耗尽重试请求的死信队列
- 保留签名完整性的原始请求体处理
详细资源:
- examples/core.md - 接收、签名验证、幂等性、事件路由
- examples/sending.md - 发送Webhook、重试逻辑、投递跟踪
- reference.md - 决策框架、头约定、状态码处理
<philosophy>
Philosophy
设计理念
Webhooks are HTTP callbacks -- a producer POSTs a payload to a consumer's URL when an event occurs. The fundamental challenge is trust and reliability: the consumer must verify the payload is authentic (signature verification), not replayed (timestamp validation), and not processed twice (idempotency). The producer must handle delivery failures gracefully (retries with backoff) and not lose events permanently (dead letter queues).
Core security principle: The signature is computed over the exact bytes transmitted. Any transformation -- JSON parsing, re-serialization, whitespace normalization -- invalidates the signature. Always verify against the raw body.
Core reliability principle: Networks are unreliable. The consumer should acknowledge receipt immediately (return 2xx) and process asynchronously. The producer should retry with exponential backoff and eventually move to a dead letter queue.
When to implement webhooks:
- Notifying external systems of events in near-real-time
- Replacing polling for event-driven integrations
- Building platform APIs that external developers consume
When NOT to implement webhooks:
- When polling is simpler and latency requirements are relaxed (minutes, not seconds)
- For internal pub/sub where a message broker is more appropriate
- When the consumer cannot expose a public HTTP endpoint
<patterns>
Webhook是HTTP回调——当事件发生时,生产者将负载POST到消费者的URL。核心挑战在于信任与可靠性:消费者必须验证负载的真实性(签名验证)、防止重放(时间戳验证)、避免重复处理(幂等性)。生产者必须优雅处理投递失败(带退避的重试),且不永久丢失事件(死信队列)。
核心安全原则: 签名基于传输的精确字节计算。任何转换——JSON解析、重新序列化、空格规范化——都会使签名失效。始终针对原始请求体进行验证。
核心可靠性原则: 网络不可靠。消费者应立即确认接收(返回2xx)并异步处理。生产者应采用指数退避重试,最终将请求移入死信队列。
何时实现Webhook:
- 近实时通知外部系统事件
- 为事件驱动集成替代轮询
- 构建供外部开发者使用的平台API
何时不应实现Webhook:
- 轮询更简单且延迟要求宽松(分钟级而非秒级)
- 内部发布/订阅场景,消息队列更合适
- 消费者无法暴露公共HTTP端点
<patterns>
Core Patterns
核心模式
Pattern 1: HMAC-SHA256 Signature Verification
模式1:HMAC-SHA256签名验证
The foundation of webhook security. The producer signs the payload with a shared secret; the consumer recomputes the signature and compares using timing-safe equality.
typescript
import { createHmac, timingSafeEqual } from "node:crypto";
const SIGNATURE_ALGORITHM = "sha256";
const SIGNATURE_ENCODING = "hex";
function verifySignature(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = createHmac(SIGNATURE_ALGORITHM, secret)
.update(rawBody)
.digest(SIGNATURE_ENCODING);
const expectedBuffer = Buffer.from(expected, "utf8");
const receivedBuffer = Buffer.from(signature, "utf8");
if (expectedBuffer.length !== receivedBuffer.length) return false;
return timingSafeEqual(expectedBuffer, receivedBuffer);
}Why good: uses to prevent timing attacks, operates on raw body bytes, length check before comparison prevents throwing on mismatched lengths
timingSafeEqualtimingSafeEqualSee examples/core.md for the full handler with raw body extraction and error responses.
Webhook安全的基础。生产者使用共享密钥对负载签名;消费者重新计算签名并使用时序安全的方式进行比较。
typescript
import { createHmac, timingSafeEqual } from "node:crypto";
const SIGNATURE_ALGORITHM = "sha256";
const SIGNATURE_ENCODING = "hex";
function verifySignature(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = createHmac(SIGNATURE_ALGORITHM, secret)
.update(rawBody)
.digest(SIGNATURE_ENCODING);
const expectedBuffer = Buffer.from(expected, "utf8");
const receivedBuffer = Buffer.from(signature, "utf8");
if (expectedBuffer.length !== receivedBuffer.length) return false;
return timingSafeEqual(expectedBuffer, receivedBuffer);
}优势: 使用防止时序攻击,基于原始请求体字节操作,比较前检查长度避免因长度不匹配抛出异常
timingSafeEqualtimingSafeEqual查看examples/core.md获取包含原始请求体提取和错误响应的完整处理器实现。
Pattern 2: Replay Attack Protection
模式2:重放攻击防护
Timestamp validation prevents attackers from re-sending captured webhook payloads. Reject payloads older than a tolerance window.
typescript
const MAX_TIMESTAMP_AGE_SECONDS = 300; // 5 minutes
const MS_PER_SECOND = 1000;
function isTimestampValid(timestampHeader: string): boolean {
const webhookTime = parseInt(timestampHeader, 10);
if (Number.isNaN(webhookTime)) return false;
const currentTime = Math.floor(Date.now() / MS_PER_SECOND);
return Math.abs(currentTime - webhookTime) <= MAX_TIMESTAMP_AGE_SECONDS;
}Why good: uses absolute difference to handle minor clock skew in both directions, rejects timestamps, named constants for tolerance window
NaNWhen to use: When the webhook producer includes a timestamp header (most major providers do). Combine with signature verification -- sign the concatenation so the timestamp itself is authenticated.
timestamp.payloadSee examples/core.md for timestamp-inclusive signature verification.
时间戳验证可防止攻击者重新发送捕获的Webhook负载。拒绝超出容忍窗口的负载。
typescript
const MAX_TIMESTAMP_AGE_SECONDS = 300; // 5分钟
const MS_PER_SECOND = 1000;
function isTimestampValid(timestampHeader: string): boolean {
const webhookTime = parseInt(timestampHeader, 10);
if (Number.isNaN(webhookTime)) return false;
const currentTime = Math.floor(Date.now() / MS_PER_SECOND);
return Math.abs(currentTime - webhookTime) <= MAX_TIMESTAMP_AGE_SECONDS;
}优势: 使用绝对差值处理双向轻微时钟偏差,拒绝无效时间戳,使用具名常量定义容忍窗口
NaN适用场景: Webhook生产者包含时间戳头时(大多数主流服务商均支持)。需与签名验证结合——对拼接内容签名,确保时间戳本身已被认证。
timestamp.payload查看examples/core.md获取包含时间戳的签名验证实现。
Pattern 3: Idempotency
模式3:幂等性
Webhook producers retry on failure, sending the same event multiple times. Store processed webhook IDs and skip duplicates.
typescript
const IDEMPOTENCY_TTL_DAYS = 7;
async function processWebhookIdempotently(
webhookId: string,
handler: () => Promise<void>,
): Promise<{ status: "processed" | "duplicate" }> {
const alreadyProcessed = await hasBeenProcessed(webhookId);
if (alreadyProcessed) return { status: "duplicate" };
await handler();
await markAsProcessed(webhookId, IDEMPOTENCY_TTL_DAYS);
return { status: "processed" };
}Why good: checks before processing, stores with TTL to prevent unbounded storage growth, returns status for logging/monitoring
Storage options: Any key-value store with TTL support works -- in-memory cache, database table, or dedicated cache service.
See examples/core.md for the complete idempotent webhook handler.
Webhook生产者会在失败时重试,多次发送同一事件。存储已处理的Webhook ID并跳过重复请求。
typescript
const IDEMPOTENCY_TTL_DAYS = 7;
async function processWebhookIdempotently(
webhookId: string,
handler: () => Promise<void>,
): Promise<{ status: "processed" | "duplicate" }> {
const alreadyProcessed = await hasBeenProcessed(webhookId);
if (alreadyProcessed) return { status: "duplicate" };
await handler();
await markAsProcessed(webhookId, IDEMPOTENCY_TTL_DAYS);
return { status: "processed" };
}优势: 处理前检查,带TTL存储防止无限增长,返回状态用于日志/监控
存储选项: 任何支持TTL的键值存储均可——内存缓存、数据库表或专用缓存服务。
查看examples/core.md获取完整的幂等Webhook处理器实现。
Pattern 4: Type-Safe Event Routing
模式4:类型安全事件路由
Route webhook events to handlers using a discriminated union on the event type. The type system enforces exhaustive handling.
typescript
type WebhookEvent =
| { type: "order.created"; data: { orderId: string; total: number } }
| { type: "order.cancelled"; data: { orderId: string; reason: string } }
| { type: "payment.completed"; data: { paymentId: string; amount: number } };
type EventHandlerMap = {
[E in WebhookEvent as E["type"]]: (data: E["data"]) => Promise<void>;
};
const handlers: EventHandlerMap = {
"order.created": async (data) => {
/* handle */
},
"order.cancelled": async (data) => {
/* handle */
},
"payment.completed": async (data) => {
/* handle */
},
};
async function routeEvent(event: WebhookEvent): Promise<void> {
const handler = handlers[event.type] as (
data: WebhookEvent["data"],
) => Promise<void>;
await handler(event.data);
}Why good: adding a new event type to the union causes a compile error until a handler is added, each handler receives correctly typed
dataSee examples/core.md for full event routing with Zod validation and unknown event handling.
基于事件类型的区分联合将Webhook事件路由到对应处理器。类型系统强制实现 exhaustive 处理。
typescript
type WebhookEvent =
| { type: "order.created"; data: { orderId: string; total: number } }
| { type: "order.cancelled"; data: { orderId: string; reason: string } }
| { type: "payment.completed"; data: { paymentId: string; amount: number } };
type EventHandlerMap = {
[E in WebhookEvent as E["type"]]: (data: E["data"]) => Promise<void>;
};
const handlers: EventHandlerMap = {
"order.created": async (data) => {
/* 处理逻辑 */
},
"order.cancelled": async (data) => {
/* 处理逻辑 */
},
"payment.completed": async (data) => {
/* 处理逻辑 */
},
};
async function routeEvent(event: WebhookEvent): Promise<void> {
const handler = handlers[event.type] as (
data: WebhookEvent["data"],
) => Promise<void>;
await handler(event.data);
}优势: 为联合类型添加新事件类型时,会触发编译错误直到添加对应处理器,每个处理器接收类型匹配的
data查看examples/core.md获取包含Zod验证和未知事件处理的完整事件路由实现。
Pattern 5: Outbound Webhook Delivery with Retry
模式5:带重试的Outbound Webhook投递
When sending webhooks, sign the payload and deliver with exponential backoff. Classify failures by HTTP status to decide whether to retry.
typescript
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 3600000; // 1 hour
function calculateDelay(attempt: number): number {
const exponential = BASE_DELAY_MS * Math.pow(2, attempt);
const capped = Math.min(exponential, MAX_DELAY_MS);
const jitter = Math.random() * BASE_DELAY_MS;
return capped + jitter;
}Why good: exponential backoff gives failing endpoints recovery time, cap prevents absurd delays, jitter prevents thundering herd when many deliveries retry simultaneously
See examples/sending.md for complete delivery with signing, status classification, and dead letter queues.
发送Webhook时,对负载签名并采用指数退避策略投递。根据HTTP状态码分类失败情况以决定是否重试。
typescript
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 3600000; // 1小时
function calculateDelay(attempt: number): number {
const exponential = BASE_DELAY_MS * Math.pow(2, attempt);
const capped = Math.min(exponential, MAX_DELAY_MS);
const jitter = Math.random() * BASE_DELAY_MS;
return capped + jitter;
}优势: 指数退避为故障端点提供恢复时间,上限避免不合理延迟,抖动防止大量投递同时重试导致的“惊群效应”
查看examples/sending.md获取包含签名、状态分类和死信队列的完整投递实现。
Pattern 6: Dead Letter Queue
模式6:死信队列
After all retry attempts are exhausted, preserve the event for manual investigation rather than dropping it silently.
typescript
interface DeadLetterEntry {
webhookId: string;
targetUrl: string;
payload: string;
lastAttempt: Date;
attempts: number;
lastError: string;
}Why good: preserves full context for debugging, enables manual replay after the target recovers
When to move to DLQ immediately (no retry): 400 Bad Request, 401 Unauthorized, 404 Not Found, 422 Unprocessable Entity -- these indicate a configuration or payload problem that retrying won't fix. Exception: 429 Too Many Requests should be retried with the header value.
Retry-AfterSee examples/sending.md for dead letter queue implementation and status code classification.
</patterns>
<decision_framework>
所有重试尝试耗尽后,保留事件供人工排查,而非静默丢弃。
typescript
interface DeadLetterEntry {
webhookId: string;
targetUrl: string;
payload: string;
lastAttempt: Date;
attempts: number;
lastError: string;
}优势: 保留调试所需的完整上下文,目标端点恢复后支持人工重放
立即移入死信队列的情况(不重试): 400 Bad Request、401 Unauthorized、404 Not Found、422 Unprocessable Entity——这些表示配置或负载问题,重试无法解决。例外:429 Too Many Requests应根据头值重试。
Retry-After查看examples/sending.md获取死信队列实现和状态码分类逻辑。
</patterns>
<decision_framework>
Decision Framework
决策框架
Receiving Webhooks
接收Webhook
Incoming webhook request:
|
+-> Is the raw body available (not parsed)?
| +-> NO -> Fix your middleware to preserve raw body FIRST
| +-> YES -> Continue
|
+-> Does the provider send a signature header?
| +-> YES -> Verify HMAC signature with timing-safe comparison
| +-> NO -> Use IP allowlisting or mutual TLS instead
|
+-> Does the provider send a timestamp?
| +-> YES -> Validate timestamp within tolerance (e.g., 5 min)
| +-> NO -> Skip replay protection (rely on idempotency)
|
+-> Does the provider send a unique event ID?
| +-> YES -> Check idempotency store, skip if duplicate
| +-> NO -> Generate a hash of the payload as a dedup key
|
+-> Process asynchronously, return 200 immediately传入Webhook请求:
|
+-> 是否可获取原始请求体(未解析)?
| +-> 否 -> 先修复中间件以保留原始请求体
| +-> 是 -> 继续
|
+-> 服务商是否发送签名头?
| +-> 是 -> 使用时序安全比较验证HMAC签名
| +-> 否 -> 改用IP白名单或双向TLS
|
+-> 服务商是否发送时间戳?
| +-> 是 -> 验证时间戳是否在容忍范围内(如5分钟)
| +-> 否 -> 跳过重放防护(依赖幂等性)
|
+-> 服务商是否发送唯一事件ID?
| +-> 是 -> 检查幂等存储,重复则跳过
| +-> 否 -> 生成负载哈希作为去重键
|
+-> 异步处理,立即返回200Sending Webhooks
发送Webhook
Need to notify external consumers of events?
|
+-> Sign every payload with HMAC-SHA256
+-> Include: webhook-id, webhook-timestamp, webhook-signature headers
+-> Deliver with retry on failure:
|
+-> Is it a 2xx response?
| +-> YES -> Delivery succeeded, done
|
+-> Is it 429, 503, or a network error?
| +-> YES -> Retry with exponential backoff + jitter
|
+-> Is it 400, 401, 404, 422?
| +-> YES -> Move to dead letter queue immediately (not retriable)
|
+-> Max retries exhausted?
+-> YES -> Move to dead letter queue需要向外部消费者通知事件?
|
+-> 使用HMAC-SHA256对所有负载签名
+-> 包含:webhook-id、webhook-timestamp、webhook-signature头
+-> 失败时重试投递:
|
+-> 是否为2xx响应?
| +-> 是 -> 投递成功,结束
|
+-> 是否为429、503或网络错误?
| +-> 是 -> 带指数退避+抖动重试
|
+-> 是否为400、401、404、422?
| +-> 是 -> 立即移入死信队列(不可重试)
|
+-> 已达最大重试次数?
+-> 是 -> 移入死信队列Status Code Quick Reference
状态码速查
| Status | Meaning | Action |
|---|---|---|
| 200-299 | Success | Mark delivered |
| 400 | Bad request | DLQ immediately |
| 401 | Unauthorized | DLQ immediately |
| 404 | Not found | DLQ immediately |
| 410 | Gone | Disable endpoint |
| 422 | Validation error | DLQ immediately |
| 429 | Rate limited | Retry with |
| 500 | Server error | Retry with backoff |
| 502/503 | Unavailable | Retry with backoff |
| Timeout | No response | Retry with backoff |
| Connection refused | Unreachable | Retry with backoff |
</decision_framework>
<red_flags>
| 状态码范围 | 含义 | 操作 |
|---|---|---|
| 200-299 | 成功 | 标记为已投递 |
| 400 | 无效请求 | 立即移入死信队列 |
| 401 | 未授权 | 立即移入死信队列 |
| 404 | 未找到 | 立即移入死信队列 |
| 410 | 已删除 | 禁用端点 |
| 422 | 验证错误 | 立即移入死信队列 |
| 429 | 请求超限 | 根据 |
| 500 | 服务器错误 | 带退避重试 |
| 502/503 | 服务不可用 | 带退避重试 |
| 超时 | 无响应 | 带退避重试 |
| 连接被拒绝 | 无法访问 | 带退避重试 |
</decision_framework>
<red_flags>
RED FLAGS
警示信号
High Priority Issues:
- Verifying signature against parsed/re-serialized JSON instead of raw body -- JSON serialization is not deterministic (key order, whitespace), so the signature will fail
- Using for signature comparison instead of
===-- leaks timing information that allows attackers to incrementally guess signatures byte by bytecrypto.timingSafeEqual - Processing webhooks synchronously before responding -- causes timeouts which trigger retries which cause duplicate processing
- No idempotency check -- retries from the producer will process the same event multiple times, causing duplicate orders, payments, etc.
- Retrying on 400/401/404 status codes -- these are permanent failures that retrying will never fix, wastes resources and delays DLQ investigation
Medium Priority Issues:
- No timestamp validation when the provider sends one -- enables replay attacks with captured payloads
- No maximum retry limit -- infinite retries on a dead endpoint waste resources permanently
- Retrying without jitter -- all failed deliveries retry at the same time (thundering herd)
- Using in-memory idempotency store in a multi-instance deployment -- each instance has a separate store, duplicates still occur
- Wildcard CORS on webhook endpoints -- webhook endpoints should not serve browser requests at all
Gotchas & Edge Cases:
- throws if buffers have different lengths -- always check
crypto.timingSafeEqualequality first or convert both to the same encoding before comparison.length - Webhook middleware order matters -- raw body capture middleware must run BEFORE any JSON parsing middleware, or the raw bytes are lost
- Some providers prefix signatures (e.g., ) -- strip the prefix before comparison
sha256=abc123 - Clock skew between producer and consumer can cause valid webhooks to fail timestamp validation -- use for the time difference and allow 5 minutes tolerance
Math.abs() - 410 Gone from a consumer means the endpoint is permanently removed -- disable the subscription instead of retrying
- Exponential backoff without a cap grows to astronomical delays -- always cap at a reasonable maximum (e.g., 1 hour)
- Webhook IDs should have a TTL in the idempotency store -- without TTL, storage grows unbounded
</red_flags>
<critical_reminders>
高优先级问题:
- 针对解析/重新序列化后的JSON验证签名,而非原始请求体——JSON序列化非确定性(键顺序、空格),会导致签名验证失败
- 使用进行签名比较,而非
===——泄露时序信息,允许攻击者逐字节猜测签名crypto.timingSafeEqual - 响应前同步处理Webhook——导致超时触发重试,进而引发重复处理
- 无幂等性检查——生产者重试会导致同一事件被多次处理,引发重复订单、支付等问题
- 对400/401/404状态码重试——这些是永久失败,重试无法解决,浪费资源且延迟死信队列排查
中优先级问题:
- 服务商发送时间戳时未验证——允许攻击者使用捕获的负载发起重放攻击
- 无最大重试次数限制——对故障端点无限重试,永久浪费资源
- 重试时无抖动——所有失败投递同时重试(惊群效应)
- 在多实例部署中使用内存幂等存储——每个实例存储独立,仍会出现重复处理
- Webhook端点使用通配符CORS——Webhook端点根本不应处理浏览器请求
陷阱与边缘情况:
- 在缓冲区长度不同时会抛出异常——比较前始终检查
crypto.timingSafeEqual是否相等,或转换为相同编码后再比较.length - Webhook中间件顺序至关重要——原始请求体捕获中间件必须在任何JSON解析中间件之前运行,否则原始字节会丢失
- 部分服务商为签名添加前缀(如)——比较前需移除前缀
sha256=abc123 - 生产者与消费者间的时钟偏差会导致有效Webhook的时间戳验证失败——使用计算时间差,允许5分钟容忍度
Math.abs() - 消费者返回410 Gone表示端点已永久移除——应禁用订阅而非重试
- 无上限的指数退避会导致延迟异常增长——始终设置合理上限(如1小时)
- 幂等存储中的Webhook ID应设置TTL——无TTL会导致存储无限增长
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
重要提醒
All code must follow project conventions in CLAUDE.md
(You MUST verify signatures against the RAW request body -- never parsed/re-serialized JSON)
(You MUST use for signature comparison -- never which leaks timing information)
crypto.timingSafeEqual===(You MUST return 2xx immediately and process webhooks asynchronously -- synchronous processing causes timeouts and duplicate deliveries)
(You MUST enforce idempotency by checking a stored webhook ID before processing -- retries WILL send the same event multiple times)
Failure to follow these rules will cause signature verification failures, timing attack vulnerabilities, duplicate event processing, and lost webhook events.
</critical_reminders>
所有代码必须遵循CLAUDE.md中的项目约定
(必须针对原始请求体验证签名——绝不能使用解析/重新序列化后的JSON)
(必须使用进行签名比较——绝不能使用,否则会泄露时序信息)
crypto.timingSafeEqual===(必须立即返回2xx状态码并异步处理Webhook——同步处理会导致超时和重复投递)
(必须通过检查存储的Webhook ID来实现幂等性——重试机制必然会多次发送同一事件)
不遵守这些规则会导致签名验证失败、时序攻击漏洞、事件重复处理和Webhook事件丢失。
</critical_reminders>