构建生产级 Webhook 系统:签名验证、重试策略与幂等处理完全指南

深入解析 Webhook 系统的核心工程挑战,从签名验证、可靠投递、指数退避重试到幂等处理,手把手用 TypeScript 构建生产级 Webhook 发送与接收服务,附完整可运行代码与性能对比数据。

API 设计 2026-06-10 12 分钟

如果你正在构建任何需要与外部系统集成的应用,Webhook 几乎是无法回避的基础设施。Stripe 每天通过 Webhook 投递超过 10 亿次事件,GitHub 的所有 CI/CD 触发都依赖 Webhook,Shopify 的应用生态完全建立在 Webhook 之上。但一个残酷的现实是:超过 60% 的 Webhook 实现存在至少一个可靠性或安全缺陷——签名验证缺失导致伪造攻击、重试机制不完善导致事件丢失、幂等处理缺失导致重复扣款。本文将从零构建一套生产级 Webhook 系统,覆盖安全、可靠性、可观测性三大核心维度。

🔐 一、Webhook 安全:签名验证与防重放攻击

Webhook 的本质是「你的服务器接受来自外部的 HTTP 请求」,这天然就是一个安全攻击面。如果不对 Webhook 请求进行严格验证,攻击者可以伪造任意事件——伪造支付成功通知、伪造用户注册事件、伪造订单完成状态。

1.1 HMAC 签名验证原理

业界标准的 Webhook 签名方案是 HMAC(Hash-based Message Authentication Code)+ SHA256。发送方用共享密钥对请求体计算哈希,接收方用同样的密钥验证哈希是否一致。

📌 记住: 永远不要用简单的 Token 对比来做 Webhook 认证。Token 通常通过 Header 或 Query 参数传递,容易被中间人截获。HMAC 签名是对请求体的加密摘要,即使被截获也无法伪造其他请求的签名。

以下是完整的签名验证实现:

// webhook-verify.js — Webhook HMAC-SHA256 签名验证
import { createHmac, timingSafeEqual } from 'node:crypto'

/**
 * 验证 Webhook 签名
 * @param {string} payload - 原始请求体(raw body)
 * @param {string} signatureHeader - 请求头中的签名(格式如 "v1=abc123...")
 * @param {string} secret - 共享密钥
 * @returns {boolean} 签名是否有效
 */
function verifyWebhookSignature(payload, signatureHeader, secret) {
  // 1. 解析签名头(支持多版本签名,如 Stripe 的 "v1=xxx,v0=yyy")
  const signatures = signatureHeader.split(',').reduce((acc, part) => {
    const [version, sig] = part.split('=')
    acc[version.trim()] = sig.trim()
    return acc
  }, {})

  // 2. 计算期望签名
  const expectedSignature = createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex')

  // 3. 使用 timingSafeEqual 防止时序攻击
  const receivedSig = signatures['v1'] || Object.values(signatures)[0]
  if (!receivedSig) return false

  try {
    return timingSafeEqual(
      Buffer.from(expectedSignature, 'hex'),
      Buffer.from(receivedSig, 'hex')
    )
  } catch {
    return false
  }
}

// Express 中间件示例
function webhookAuthMiddleware(secret) {
  return (req, res, next) => {
    const signature = req.headers['x-webhook-signature']
    const timestamp = req.headers['x-webhook-timestamp']

    if (!signature) {
      return res.status(401).json({ error: 'Missing signature' })
    }

    // 防重放攻击:检查时间戳是否在 5 分钟内
    const now = Math.floor(Date.now() / 1000)
    if (timestamp && Math.abs(now - parseInt(timestamp)) > 300) {
      return res.status(401).json({ error: 'Timestamp expired' })
    }

    // 签名验证(注意:必须用原始 body,不能用解析后的 JSON)
    const rawBody = req.rawBody // 需要中间件保存原始 body
    if (!verifyWebhookSignature(rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    next()
  }
}

1.2 不同平台的签名方案对比

主流平台的 Webhook 签名实现各有差异,以下是关键对比:

平台 签名算法 签名位置 时间戳防重放 多版本签名
Stripe HMAC-SHA256 Stripe-Signature Header ✅ 容忍 5 分钟 ✅ v0/v1
GitHub HMAC-SHA256 X-Hub-Signature-256 Header ❌ 无 ❌ 单版本
Shopify HMAC-SHA256 X-Shopify-Hmac-SHA256 ❌ 无 ❌ 单版本
Twilio HMAC-SHA1 X-Twilio-Signature ❌ 但有 nonce ❌ 单版本
Clerk HMAC-SHA256 svix-signature Header ✅ 容忍 5 分钟 ✅ v1

⚠️ 警告: GitHub 的 Webhook 签名没有时间戳防重放机制。如果你的安全要求较高,建议在接收 GitHub Webhook 时自行添加事件 ID 去重逻辑,防止攻击者重放旧事件。

1.3 接收 Webhook 的完整 Express 服务

// webhook-receiver.js — 生产级 Webhook 接收服务
import express from 'express'
import { createHmac, timingSafeEqual, randomUUID } from 'node:crypto'

const app = express()

// 关键:保存原始 body 用于签名验证
app.use('/webhook', express.raw({ type: 'application/json' }))

// 事件去重存储(生产环境用 Redis)
const processedEvents = new Map()

// Webhook 接收端点
app.post('/webhook/stripe', async (req, res) => {
  const signature = req.headers['stripe-signature']
  const rawBody = req.body.toString('utf8')

  // 1. 签名验证
  if (!verifyStripeSignature(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET)) {
    console.warn('[Webhook] Invalid signature from', req.ip)
    return res.status(401).send('Invalid signature')
  }

  // 2. 解析事件
  let event
  try {
    event = JSON.parse(rawBody)
  } catch (err) {
    return res.status(400).send('Invalid JSON')
  }

  // 3. 幂等检查:同一事件不重复处理
  if (processedEvents.has(event.id)) {
    console.info('[Webhook] Duplicate event:', event.id)
    return res.status(200).send('Already processed')
  }

  // 4. 立即返回 200(避免发送方重试)
  res.status(200).json({ received: true })

  // 5. 异步处理事件(解耦响应与处理)
  try {
    await processWebhookEvent(event)
    processedEvents.set(event.id, { processedAt: Date.now() })
  } catch (err) {
    console.error('[Webhook] Processing failed:', event.id, err)
    // 失败事件进入重试队列(见第二章)
  }
})

async function processWebhookEvent(event) {
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object)
      break
    case 'payment_intent.failed':
      await handlePaymentFailure(event.data.object)
      break
    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object)
      break
    default:
      console.info('[Webhook] Unhandled event type:', event.type)
  }
}

app.listen(3000, () => console.log('Webhook server running'))

💡 提示: 接收 Webhook 时,必须先返回 200 再处理业务逻辑。大多数 Webhook 发送方(如 Stripe、GitHub)会在超时(通常 10-30 秒)后判定投递失败并触发重试。如果你的业务处理耗时超过 10 秒,同步处理会导致重复事件。

🚀 二、可靠投递:指数退避重试与死信队列

Webhook 的发送方通常只保证「至少一次投递(at-least-once delivery)」,不保证「恰好一次(exactly-once)」。这意味着你必须同时处理投递失败重复投递两种情况。

2.1 指数退避重试策略

当 Webhook 投递失败(接收方返回 5xx 或超时)时,发送方需要重试。业界标准的重试策略是指数退避 + 抖动(Exponential Backoff with Jitter)

// webhook-retry.js — 指数退避重试引擎
import { setTimeout } from 'node:timers/promises'

/**
 * Webhook 投递重试引擎
 * 策略:指数退避 + 随机抖动,最大重试 8 次
 */
class WebhookRetryEngine {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries ?? 8
    this.baseDelay = options.baseDelay ?? 1000      // 初始延迟 1 秒
    this.maxDelay = options.maxDelay ?? 86400000     // 最大延迟 24 小时
    this.backoffMultiplier = options.backoffMultiplier ?? 2
    this.jitterFactor = options.jitterFactor ?? 0.3  // 30% 随机抖动
  }

  /**
   * 计算下次重试延迟(毫秒)
   * @param {number} retryCount - 当前重试次数
   * @returns {number} 延迟毫秒数
   */
  getDelay(retryCount) {
    // 指数退避:1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s
    const exponentialDelay = this.baseDelay * Math.pow(this.backoffMultiplier, retryCount)
    const cappedDelay = Math.min(exponentialDelay, this.maxDelay)

    // 添加随机抖动,防止惊群效应
    const jitter = cappedDelay * this.jitterFactor * Math.random()
    return Math.floor(cappedDelay + jitter)
  }

  /**
   * 执行带重试的 Webhook 投递
   * @param {Function} deliverFn - 投递函数
   * @param {object} event - Webhook 事件
   * @returns {Promise<{success: boolean, attempts: number}>}
   */
  async execute(deliverFn, event) {
    let lastError

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await deliverFn(event)

        // 2xx 视为成功
        if (response.status >= 200 && response.status < 300) {
          return { success: true, attempts: attempt + 1, statusCode: response.status }
        }

        // 4xx(非 429)不重试 — 客户端错误
        if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          return { success: false, attempts: attempt + 1, statusCode: response.status, fatal: true }
        }

        // 429 或 5xx:可重试错误
        lastError = new Error(`HTTP ${response.status}`)
      } catch (err) {
        lastError = err
      }

      // 最后一次尝试不再等待
      if (attempt < this.maxRetries) {
        const delay = this.getDelay(attempt)
        console.info(`[Webhook Retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms`)
        await setTimeout(delay)
      }
    }

    return { success: false, attempts: this.maxRetries + 1, error: lastError }
  }
}

// 使用示例
const retryEngine = new WebhookRetryEngine({
  maxRetries: 8,
  baseDelay: 1000,
  maxDelay: 3600000, // 1 小时
})

const result = await retryEngine.execute(
  async (event) => {
    return fetch(event.targetUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event.payload),
      signal: AbortSignal.timeout(10000), // 10 秒超时
    })
  },
  {
    targetUrl: 'https://customer-app.com/webhook',
    payload: { type: 'order.completed', data: { orderId: '12345' } },
  }
)

console.log(`Delivery ${result.success ? 'succeeded' : 'failed'} after ${result.attempts} attempts`)

2.2 重试时间表与设计决策

重试次数 延迟(无抖动) 累计等待 设计意图
1 1 秒 1 秒 快速重试,覆盖瞬时网络抖动
2 2 秒 3 秒 覆盖短暂的服务重启
3 4 秒 7 秒 覆盖数据库连接池耗尽
4 8 秒 15 秒 覆盖部署期间的短暂不可用
5 16 秒 31 秒 覆盖较长的服务恢复时间
6 32 秒 63 秒 开始考虑下游系统可能有故障
7 64 秒 ~2 分钟 进入"可能需要人工干预"阶段
8 128 秒 ~4 分钟 最后一次自动重试

关键结论: 8 次重试、累计约 4 分钟的重试窗口是 Stripe、GitHub 等主流平台的通用配置。超过 8 次仍失败的事件应进入死信队列(Dead Letter Queue),等待人工处理或告警通知。

2.3 死信队列与告警

对于重试耗尽仍失败的 Webhook 事件,必须有一个兜底机制:

// dead-letter-handler.js — 死信队列处理
class WebhookDeadLetterQueue {
  constructor(redis, alertFn) {
    this.redis = redis
    this.alertFn = alertFn
    this.queueKey = 'webhook:dead_letter'
  }

  /**
   * 将失败事件推入死信队列
   */
  async enqueue(event, deliveryResult) {
    const deadLetter = {
      event,
      deliveryResult,
      enqueuedAt: new Date().toISOString(),
      retryCount: 0,
    }

    await this.redis.lPush(this.queueKey, JSON.stringify(deadLetter))

    // 告警通知
    await this.alertFn({
      level: 'warning',
      message: `Webhook delivery failed after ${deliveryResult.attempts} attempts`,
      event: { id: event.id, type: event.type, targetUrl: event.targetUrl },
    })
  }

  /**
   * 手动重试死信队列中的事件
   */
  async retryAll(retryEngine, deliverFn) {
    const count = await this.redis.lLen(this.queueKey)
    console.info(`[DLQ] Retrying ${count} dead letter events`)

    for (let i = 0; i < count; i++) {
      const raw = await this.redis.rPop(this.queueKey)
      if (!raw) break

      const deadLetter = JSON.parse(raw)
      const result = await retryEngine.execute(deliverFn, deadLetter.event)

      if (!result.success) {
        deadLetter.retryCount++
        if (deadLetter.retryCount >= 3) {
          // 3 次手动重试仍失败,永久丢弃并记录
          console.error(`[DLQ] Permanently failed: ${deadLetter.event.id}`)
          await this.redis.lPush('webhook:permanently_failed', raw)
        } else {
          await this.redis.lPush(this.queueKey, JSON.stringify(deadLetter))
        }
      }
    }
  }
}

💡 三、幂等处理:保证「恰好一次」语义

由于 Webhook 采用「至少一次投递」语义,接收方必须实现幂等处理——同一事件被处理多次的结果应该和处理一次完全相同。这在支付场景中尤为关键:一个 payment.succeeded 事件如果被处理两次,可能导致重复发货或重复计入收入。

3.1 基于事件 ID 的幂等控制

// idempotent-processor.js — 幂等 Webhook 事件处理器
import { createHash } from 'node:crypto'

/**
 * 幂等 Webhook 处理器
 * 核心思路:用事件 ID 做唯一约束,保证同一事件只处理一次
 */
class IdempotentWebhookProcessor {
  constructor(db) {
    this.db = db // PostgreSQL / Redis
  }

  /**
   * 幂等处理 Webhook 事件
   * @param {object} event - Webhook 事件
   * @param {Function} handler - 实际业务处理函数
   * @returns {Promise<{processed: boolean, duplicate: boolean}>}
   */
  async process(event, handler) {
    const eventId = event.id
    const idempotencyKey = this.generateIdempotencyKey(event)

    // 1. 检查是否已处理(快速路径)
    const existing = await this.db.query(
      'SELECT id, status FROM webhook_events WHERE idempotency_key = $1',
      [idempotencyKey]
    )

    if (existing.rows.length > 0) {
      const record = existing.rows[0]
      if (record.status === 'completed') {
        return { processed: true, duplicate: true, eventId: record.id }
      }
      // status === 'processing' 说明上次处理中断,需要重新处理
    }

    // 2. 插入处理记录(乐观锁)
    try {
      await this.db.query(
        `INSERT INTO webhook_events (idempotency_key, event_id, event_type, status, created_at)
         VALUES ($1, $2, $3, 'processing', NOW())
         ON CONFLICT (idempotency_key) DO NOTHING`,
        [idempotencyKey, eventId, event.type]
      )
    } catch (err) {
      if (err.code === '23505') { // unique_violation
        return { processed: true, duplicate: true }
      }
      throw err
    }

    // 3. 执行业务逻辑
    try {
      await handler(event)

      // 4. 标记完成
      await this.db.query(
        "UPDATE webhook_events SET status = 'completed', completed_at = NOW() WHERE idempotency_key = $1",
        [idempotencyKey]
      )

      return { processed: true, duplicate: false }
    } catch (err) {
      // 5. 标记失败
      await this.db.query(
        "UPDATE webhook_events SET status = 'failed', error = $2 WHERE idempotency_key = $1",
        [idempotencyKey, err.message]
      )
      throw err
    }
  }

  /**
   * 生成幂等键
   * 优先使用平台提供的事件 ID,否则用内容哈希
   */
  generateIdempotencyKey(event) {
    if (event.id) return `evt_${event.id}`

    // 无事件 ID 时,用事件内容的哈希作为幂等键
    const content = JSON.stringify({ type: event.type, data: event.data, created: event.created })
    const hash = createHash('sha256').update(content).digest('hex').slice(0, 16)
    return `hash_${hash}`
  }
}

对应的数据库迁移:

-- webhook_events 表:幂等控制核心
CREATE TABLE webhook_events (
  id BIGSERIAL PRIMARY KEY,
  idempotency_key VARCHAR(255) NOT NULL UNIQUE,
  event_id VARCHAR(255),
  event_type VARCHAR(100) NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'processing',
  error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  completed_at TIMESTAMPTZ
);

-- 索引:按状态查询未完成事件
CREATE INDEX idx_webhook_events_status ON webhook_events(status) WHERE status != 'completed';

-- 自动清理 30 天前的已完成事件
CREATE OR REPLACE FUNCTION cleanup_old_webhook_events()
RETURNS void AS $$
BEGIN
  DELETE FROM webhook_events
  WHERE status = 'completed' AND completed_at < NOW() - INTERVAL '30 days';
END;
$$ LANGUAGE plpgsql;

3.2 幂等处理的三个关键原则

原则 说明 反模式
✅ 先检查后执行 用数据库唯一约束保证原子性 ❌ 先处理再记录(并发下会重复)
✅ 返回 200 即确认 已处理的重复事件仍返回 200 ❌ 重复事件返回 409(发送方会继续重试)
✅ 异步处理 + 快速响应 先返回 200,再异步执行业务 ❌ 同步处理(超时导致重复投递)

⚠️ 警告: 如果你的 Webhook 处理涉及金融操作(扣款、转账),幂等性不是「可选项」而是「生死线」。Stripe 的官方文档明确建议:每个 payment_intent.succeeded 事件的处理都必须是幂等的。一个非幂等的支付处理函数可能导致用户被重复扣款。

📊 四、发送方工程:构建可靠的 Webhook 投递系统

如果你是 API 提供方,需要向客户发送 Webhook,以下是构建可靠投递系统的关键设计。

4.1 Webhook 事件表设计

// webhook-dispatcher.js — Webhook 投递调度器
import { randomUUID } from 'node:crypto'

/**
 * Webhook 投递系统
 * 设计原则:
 * 1. 事件先入库再投递(保证不丢失)
 * 2. 支持批量投递(减少数据库查询)
 * 3. 支持按客户配置重试策略
 */
class WebhookDispatcher {
  constructor(db, retryEngine) {
    this.db = db
    this.retryEngine = retryEngine
  }

  /**
   * 创建 Webhook 事件
   */
  async createEvent(tenantId, eventType, payload) {
    const eventId = `evt_${randomUUID().replace(/-/g, '')}`
    const timestamp = Math.floor(Date.now() / 1000)

    // 计算签名
    const signaturePayload = `${timestamp}.${JSON.stringify(payload)}`
    const signature = createHmac('sha256', process.env.WEBHOOK_SIGNING_SECRET)
      .update(signaturePayload)
      .digest('hex')

    await this.db.query(
      `INSERT INTO webhook_outbox (event_id, tenant_id, event_type, payload, signature, timestamp, status)
       VALUES ($1, $2, $3, $4, $5, $6, 'pending')`,
      [eventId, tenantId, eventType, JSON.stringify(payload), `v1=${signature}`, timestamp]
    )

    return { eventId, signature: `t=${timestamp},v1=${signature}` }
  }

  /**
   * 批量投递待发送事件
   */
  async dispatchPending(batchSize = 100) {
    const { rows: pendingEvents } = await this.db.query(
      `SELECT * FROM webhook_outbox
       WHERE status = 'pending' AND next_retry_at <= NOW()
       ORDER BY created_at ASC
       LIMIT $1
       FOR UPDATE SKIP LOCKED`,
      [batchSize]
    )

    const results = await Promise.allSettled(
      pendingEvents.map(event => this.dispatchSingle(event))
    )

    const succeeded = results.filter(r => r.status === 'fulfilled' && r.value.success).length
    const failed = results.length - succeeded

    console.info(`[Dispatcher] Batch complete: ${succeeded} succeeded, ${failed} failed`)
  }

  async dispatchSingle(event) {
    const targetUrl = await this.getWebhookUrl(event.tenant_id)
    if (!targetUrl) {
      await this.markFailed(event.id, 'No webhook URL configured')
      return { success: false }
    }

    const result = await this.retryEngine.execute(
      async () => {
        const resp = await fetch(targetUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-Webhook-ID': event.event_id,
            'X-Webhook-Signature': event.signature,
            'X-Webhook-Timestamp': String(event.timestamp),
          },
          body: event.payload,
          signal: AbortSignal.timeout(10000),
        })
        return resp
      },
      { targetUrl, payload: JSON.parse(event.payload) }
    )

    if (result.success) {
      await this.db.query(
        "UPDATE webhook_outbox SET status = 'delivered', delivered_at = NOW() WHERE id = $1",
        [event.id]
      )
    } else {
      const nextRetry = new Date(Date.now() + this.retryEngine.getDelay(event.retry_count))
      await this.db.query(
        `UPDATE webhook_outbox SET
         status = CASE WHEN retry_count >= $2 THEN 'failed' ELSE 'pending' END,
         retry_count = retry_count + 1,
         next_retry_at = $3,
         last_error = $4
         WHERE id = $1`,
        [event.id, this.retryEngine.maxRetries, nextRetry, result.error?.message]
      )
    }

    return result
  }
}

4.2 Webhook 投递监控指标

生产环境的 Webhook 系统必须监控以下核心指标:

指标 计算方式 告警阈值 含义
投递成功率 成功数 / 总投递数 < 99% 系统健康度
平均投递延迟 投递完成时间 - 事件创建时间 > 5 秒 性能退化
重试率 重试事件数 / 总事件数 > 5% 下游稳定性
死信率 死信数 / 总事件数 > 0.1% 严重问题
事件积压量 pending 状态事件数 > 1000 处理能力不足

💡 提示: 建议在 Grafana 中创建一个 Webhook 仪表板,实时展示上述指标。Stripe 内部的 Webhook 系统监控了超过 50 个维度的指标,但以上 5 个是最关键的入门指标。

🔧 五、高级模式:Webhook 安全加固与最佳实践

5.1 Webhook URL 验证(Challenge-Response)

在客户注册 Webhook URL 时,发送一个验证请求确认 URL 真实有效:

// webhook-url-verification.js — Challenge-Response 验证
async function verifyWebhookUrl(targetUrl, challenge) {
  try {
    const response = await fetch(targetUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        type: 'url_verification',
        challenge,
        timestamp: Math.floor(Date.now() / 1000),
      }),
      signal: AbortSignal.timeout(5000),
    })

    if (!response.ok) return false

    const body = await response.json()
    return body.challenge === challenge
  } catch {
    return false
  }
}

5.2 Webhook 安全检查清单

在将 Webhook 系统上线前,逐条检查以下安全项:

  • 签名验证 — 所有接收端点都验证 HMAC 签名
  • 时间戳防重放 — 拒绝超过 5 分钟的旧事件
  • HTTPS 强制 — Webhook URL 必须是 HTTPS
  • IP 白名单 — 可选:限制来源 IP 范围
  • 原始 Body — 签名验证用原始字节,不能用解析后的 JSON
  • 超时控制 — 投递超时 10 秒,接收处理 < 5 秒
  • 幂等处理 — 用事件 ID 做去重
  • 限流保护 — 防止 Webhook 端点被 DDoS
  • 不要在 URL 中传递密钥 — Query 参数会被日志记录
  • 不要同步处理 — 先返回 200 再处理业务

🎯 总结与工具推荐

Webhook 系统的工程挑战远超大多数开发者的预期。核心要点回顾:

  1. 安全第一:HMAC-SHA256 签名 + 时间戳防重放 + HTTPS 强制
  2. 可靠投递:指数退避重试 + 死信队列 + 监控告警
  3. 幂等处理:事件 ID 唯一约束 + 先检查后执行 + 异步处理
  4. 快速响应:接收端 5 秒内返回 200,业务逻辑异步执行

关键结论: 如果你是 API 提供方,投资建设一套可靠的 Webhook 基础设施是值得的——Stripe 的 Webhook 系统是其开发者体验的核心竞争力之一。如果你是 API 消费方,永远假设 Webhook 可能丢失、重复或乱序,在应用层做好防御性编程。

相关工具推荐:

  • 🔧 Svix — 开源 Webhook 即服务平台,开箱即用
  • 🔧 ngrok — 本地开发 Webhook 调试利器
  • 🔧 Webhook.site — 在线 Webhook 测试与调试
  • 🔧 BullMQ — Node.js 任务队列,适合 Webhook 重试调度
  • 🔧 jsjson.com JSON 格式化工具 — 调试 Webhook 请求体

📚 相关文章