webhook-receiver-hardener

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Webhook Receiver Hardener

Webhook Receiver 加固指南

Build secure, reliable webhook endpoints that handle failures gracefully.
构建能够优雅处理故障的安全、可靠Webhook端点。

Core Security

核心安全特性

Signature Verification: HMAC validation before processing Deduplication: Track processed webhook IDs Idempotency: Safe to process same webhook multiple times Retries: Handle provider retry attempts Rate Limiting: Prevent abuse
签名验证:处理前进行HMAC验证 重复数据删除:追踪已处理的Webhook ID 幂等性:可安全重复处理同一Webhook 重试处理:应对服务提供商的重试尝试 速率限制:防止滥用

Signature Verification

签名验证

typescript
import crypto from "crypto";

export const verifyWebhookSignature = (
  payload: string,
  signature: string,
  secret: string
): boolean => {
  const hmac = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac));
};

// Stripe example
router.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["stripe-signature"];

    try {
      const event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
      await processStripeEvent(event);
      res.json({ received: true });
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);
typescript
import crypto from "crypto";

export const verifyWebhookSignature = (
  payload: string,
  signature: string,
  secret: string
): boolean => {
  const hmac = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac));
};

// Stripe示例
router.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["stripe-signature"];

    try {
      const event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
      await processStripeEvent(event);
      res.json({ received: true });
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);

Deduplication

重复数据删除

typescript
// Redis-based dedupe
const WEBHOOK_TTL = 60 * 60 * 24; // 24 hours

export const isDuplicate = async (webhookId: string): Promise<boolean> => {
  const key = `webhook:${webhookId}`;
  const exists = await redis.exists(key);

  if (exists) return true;

  await redis.setex(key, WEBHOOK_TTL, "1");
  return false;
};

// Usage
if (await isDuplicate(webhook.id)) {
  return res.status(200).json({ received: true }); // Already processed
}
typescript
// 基于Redis的去重方案
const WEBHOOK_TTL = 60 * 60 * 24; // 24小时

export const isDuplicate = async (webhookId: string): Promise<boolean> => {
  const key = `webhook:${webhookId}`;
  const exists = await redis.exists(key);

  if (exists) return true;

  await redis.setex(key, WEBHOOK_TTL, "1");
  return false;
};

// 使用示例
if (await isDuplicate(webhook.id)) {
  return res.status(200).json({ received: true }); // 已处理过
}

Idempotent Processing

幂等处理

typescript
export const processWebhook = async (webhook: Webhook) => {
  // Use database transaction with unique constraint
  try {
    await db.transaction(async (trx) => {
      // Insert webhook record (unique constraint on webhook_id)
      await trx("processed_webhooks").insert({
        webhook_id: webhook.id,
        processed_at: new Date(),
      });

      // Do actual processing
      await performWebhookAction(webhook, trx);
    });
  } catch (err) {
    if (err.code === "23505") {
      // Unique violation
      console.log("Webhook already processed");
      return; // Idempotent - already processed
    }
    throw err;
  }
};
typescript
export const processWebhook = async (webhook: Webhook) => {
  // 使用带唯一约束的数据库事务
  try {
    await db.transaction(async (trx) => {
      // 插入Webhook记录(webhook_id字段有唯一约束)
      await trx("processed_webhooks").insert({
        webhook_id: webhook.id,
        processed_at: new Date(),
      });

      // 执行实际处理逻辑
      await performWebhookAction(webhook, trx);
    });
  } catch (err) {
    if (err.code === "23505") {
      // 唯一约束冲突
      console.log("Webhook已处理");
      return; // 幂等操作 - 已处理完成
    }
    throw err;
  }
};

Retry Handling

重试处理

typescript
// Acknowledge immediately, process async
router.post("/webhooks/provider", async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, req.headers["signature"])) {
    return res.status(401).send("Invalid signature");
  }

  // Return 200 immediately
  res.status(200).json({ received: true });

  // Process async
  processWebhookAsync(req.body).catch((err) => {
    console.error("Webhook processing failed:", err);
    // Will be retried by provider
  });
});

// Exponential backoff for provider retries
// Attempt 1: immediate
// Attempt 2: +5 minutes
// Attempt 3: +15 minutes
// Attempt 4: +1 hour
// Attempt 5: +6 hours
typescript
// 立即确认,异步处理
router.post("/webhooks/provider", async (req, res) => {
  // 验证签名
  if (!verifySignature(req.body, req.headers["signature"])) {
    return res.status(401).send("Invalid signature");
  }

  // 立即返回200状态码
  res.status(200).json({ received: true });

  // 异步处理
  processWebhookAsync(req.body).catch((err) => {
    console.error("Webhook处理失败:", err);
    // 服务提供商会自动重试
  });
});

// 服务提供商重试的指数退避策略
// 第1次:立即重试
// 第2次:间隔5分钟
// 第3次:间隔15分钟
// 第4次:间隔1小时
// 第5次:间隔6小时

Error Responses

错误响应

typescript
// Return appropriate status codes
const webhookHandler = async (req, res) => {
  // 400: Malformed payload (won't retry)
  if (!isValidPayload(req.body)) {
    return res.status(400).json({ error: "Invalid payload" });
  }

  // 401: Invalid signature (won't retry)
  if (!verifySignature(req.body, req.headers["signature"])) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // 200: Already processed (idempotent)
  if (await isDuplicate(req.body.id)) {
    return res.status(200).json({ received: true });
  }

  // 500: Processing error (will retry)
  try {
    await processWebhook(req.body);
    return res.status(200).json({ received: true });
  } catch (err) {
    console.error("Processing error:", err);
    return res.status(500).json({ error: "Processing failed" });
  }
};
typescript
// 返回合适的状态码
const webhookHandler = async (req, res) => {
  // 400:无效负载(不会重试)
  if (!isValidPayload(req.body)) {
    return res.status(400).json({ error: "Invalid payload" });
  }

  // 401:无效签名(不会重试)
  if (!verifySignature(req.body, req.headers["signature"])) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // 200:已处理(幂等)
  if (await isDuplicate(req.body.id)) {
    return res.status(200).json({ received: true });
  }

  // 500:处理错误(会重试)
  try {
    await processWebhook(req.body);
    return res.status(200).json({ received: true });
  } catch (err) {
    console.error("处理错误:", err);
    return res.status(500).json({ error: "Processing failed" });
  }
};

Monitoring & Runbook

监控与事件处理手册

markdown
undefined
markdown
undefined

Webhook Incidents

Webhook事件处理

High Error Rate

高错误率

  1. Check provider status page
  2. Review recent code deploys
  3. Check signature secret rotation
  4. Verify database connectivity
  1. 查看服务提供商状态页面
  2. 检查近期代码部署记录
  3. 检查签名密钥轮换情况
  4. 验证数据库连接性

Missing Webhooks

缺失Webhook

  1. Check provider sending (their dashboard)
  2. Verify endpoint is accessible
  3. Check rate limiting rules
  4. Review dedupe cache TTL
  1. 检查服务提供商发送情况(他们的控制台)
  2. 验证端点是否可访问
  3. 检查速率限制规则
  4. 查看去重缓存的TTL设置

Duplicate Processing

重复处理

  1. Check dedupe cache connectivity
  2. Verify unique constraints
  3. Review idempotency logic
undefined
  1. 检查去重缓存连接性
  2. 验证唯一约束配置
  3. 审查幂等逻辑
undefined

Best Practices

最佳实践

  • Verify signature BEFORE any processing
  • Return 200 quickly, process async
  • Dedupe with Redis + database constraints
  • Log all webhook attempts
  • Monitor processing latency
  • Set up alerts for failures
  • Document expected payload schemas
  • 先验证签名,再执行任何处理逻辑
  • 快速返回200状态码,异步处理业务逻辑
  • 使用Redis + 数据库约束实现去重
  • 记录所有Webhook请求尝试
  • 监控处理延迟
  • 设置故障告警
  • 记录预期的负载 schema

Output Checklist

输出检查清单

  • Signature verification
  • Deduplication mechanism
  • Idempotent processing
  • Async processing pattern
  • Proper status codes
  • Error logging
  • Monitoring/alerts
  • Incident runbook
  • 签名验证
  • 去重机制
  • 幂等处理
  • 异步处理模式
  • 正确的状态码
  • 错误日志
  • 监控/告警
  • 事件处理手册