项目概述
本项目实现外发 Webhook 投递服务:对载荷签名、对瞬时失败按退避重试,并在端点持续失败时停止继续敲打。把 Webhook 投递到别人的服务器,意味着对方迟早会慢、会宕或带有敌意。
架构与关键决策
投递工作进程取出到期投递,以每订阅者密钥与时间戳签名载荷,带超时 POST 并分类响应;瞬时失败按指数退避加抖动重排,永久失败停止,避免被拒载荷无限重投。按端点的熔断器连续失败后打开并推迟后续投递直到探测成功,把单个坏订阅者的影响限制在其自身。每次尝试记录状态与响应摘要,使订阅者可自行诊断。签名含时间戳,使接收方能拒绝重放。
实施与测试
将投递指向“失败两次后成功”的桩断言最终成功;以连续失败打开熔断并断言后续投递被推迟而非尝试;断言永久失败不重试;断言重放旧签名载荷被拒;断言对订阅端点在注册与投递前均拒绝私有地址。
限制与后续
- 仅推送,无拉取 API 供订阅者读取积压。
- 熔断按端点,不自跨订阅者协调。
- 无逐订阅者载荷变换,各订阅者收到同一事件形状。
代码示例
pnpm init
pnpm add undici pg
pnpm add -D typescript tsx vitest
webhook-delivery/
├── src/worker.ts
├── src/sign.ts
├── src/retry.ts
├── src/breaker.ts
├── tests/delivery.test.ts
└── package.json
const sig = hmac(secret, ts + '.' + body);
headers['x-signature'] = 't=' + ts + ',v1=' + sig;
dueAt = now + Math.min(maxDelay, base * 2 ** attempt);
const ac = new AbortController(); setTimeout(() => ac.abort(), timeoutMs);
if (breaker.isOpen(endpointId)) return defer(delivery, cooldownMs);
await db.insertAttempt({ deliveryId, status, code, ms });
pnpm vitest run