实用 webhook 例子:安全实施指南

实用Web钩子示例:安全实施指南

Find a complete web hook example with code for Node.js, Python, and Go. Learn to securely verify signatures, prevent replay attacks, and debug your endpoints.

马丁·多纳迪厄

马丁·多纳迪厄

内容营销人员

实用 Web 钩子示例:安全实施指南

您有一个需要在发生其他地方事件时反应的服务。支付清算。客户记录发生变化。一个仓库接收推送。您可以每分钟轮询一个 API 并浪费循环询问“有新内容吗?”,或者您可以让源系统在事件发生时调用您。

大多数 Web 钩子示例文章在这里停止。它们显示一个路由,打印 JSON 体,返回, 200并认为完成了。这种版本在有人发送伪造请求,重放有效请求,或者您的处理程序因为框架在签名验证之前解析了体而中断时,仍然有效。

本指南采用您将在生产中使用的路径。示例足够小以便复制,但它们包括重要部分:原始体处理,HMAC 验证,时间戳检查,快速确认和实用调试。

目录

什么是Webhook?为什么要使用它们?

您的账单提供商在 02:13 将发票标记为已付款。如果您的应用程序在 02:14 学习到这一点,客户立即获得访问权限。如果您的应用程序在下一个轮询周期学习到这一点,他们会等待,支持团队会收到一个票据,而您的日志会填满可避免的噪音。Webhooks 解决了这个时间问题,通过发送 HTTP 回调来通知事件发生。

在实际场景中,一个 webhook 是一个事件驱动的 POST 请求,从一个系统传递到另一个系统。提供商检测到一个变化,例如 invoice.paid, order.createdpush,并将事件数据发送到您控制的 URL。这样可以移除轮询创建的“是否有新内容?”的循环,并减少了大量的浪费请求。

This pattern shows up in real systems because it maps cleanly to business events. Stripe posts payment outcomes. GitHub posts repository activity. Shopify posts order updates. The shape is simple, but production behavior is not. A webhook that updates money, access, or inventory deserves the same care as any public API endpoint, especially once retries, duplicates, and untrusted traffic enter the picture.

一个有用的模型是帮助

一个有用的方式是将 webhook 流程框架为四个部分一起工作:

  • 源系统. 检测事件的服务。
  • 目的端点. 接收它的 HTTP 路由。
  • 事件.发生的名称变更,如 invoice.paidpush.
  • 载荷.包含code详细信息的请求体。

服务提供商发送有关已经发生的事情的信息。您的任务是验证发送者、确认请求是最新的,并在确认后应用更改。最后一部分比许多基本教程承认的更重要。在生产环境中,重复交付是正常行为,而不是边缘案例。

实用规则: 使用Webhook进行事件驱动的更新。使用轮询进行预定读取、补充或不提供出站事件的提供商。

构建更广泛的 工作流自动化和数据集成的团队,Webhook通常是保持系统在同步状态而不产生不必要的请求流量的事件层。如果您正在工作于集成密集的服务,Capgo的 后端开发文章 是有用的背景,因为核心问题会出现在重试、队列、可观察性和故障处理等方面。

在生产环境中什么有效,什么无效

通常设计得很乏味的设置是那些能坚持住的。只订阅你需要的事件。根据提供商或事件家族将端点范围限制在特定范围内。存储事件 ID 以避免重复交付导致的副作用。验证并排队请求后立即返回一个快速的 2xx 响应,然后异步执行更慢的业务逻辑。

易碎的版本很容易识别。一个通用的端点处理所有事情。签名检查在早期测试中被跳过并且从未回来。处理程序直接写入关键表格之前检查事件是否合法或过期。这个在演示中有效,但在重试风暴、提供商故障或攻击者重放旧请求时就会失败。

这个权衡定义了本指南的其余部分。一个 webhook 接收器的“hello world”版本很小。生产就绪的版本从一开始就添加了签名验证、重放防御、重复处理和调试钩子。

Webhook HTTP 请求的解剖学

在写 code 之前,帮助看看请求作为原始 HTTP 而不是框架对象。一个典型的 webhook 就是一个 HTTP POST 请求到公共端点,带有头部和 JSON 体。

一个简单的原始请求

POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678

{
  "event": "order.created",
  "id": "evt_123",
  "data": {
    "order_id": "ord_456",
    "status": "created"
  }
}

重要部分很明显:

  • 方法在实践中,webhook 交付通常是 POST 请求。
  • Content-Type大多数现代提供商发送 JSON。
  • User-Agent. 对于调试有所帮助,但从来不够以信任为依据。
  • Signature header. 携带了提供商的真实性校验。
  • Timestamp header. 用于拒绝过时或重放的请求。

为什么体积很重要

您的code通常不关心每个字段。它关心事件类型、事件标识符和业务对象。 data. 这就是为什么好的处理程序只解析它们需要的内容并将其余内容记录以便调试。

OpenAPI现在直接模拟了这个模式。OpenAPI 3.1.0添加了第一类webhook支持,顶级 webhooks 对象,其中每个webhook都像Path Item一样描述,但由提供商触发。Canonical示例使用一个 newPet webhook具有 post 在操作、一个 JSON 请求体和一个 200 响应来表示接收,正如 OpenAPI webhook 示例所示 如果您正在为自己的接收器或提供者合同编写文档,强大的示例比抽象的模式文本更有用。 我喜欢使用参考文献,如.

SheetMergy 的 __CAPGO_KEEP_0__ 文档示例 SheetMergy’s API doc examples 一个 webhook 在传输层上很简单。 大多数故障来自于对标头、请求体编码或签名规则的不匹配假设。

如何安全地验证 webhook 签名

一个签名的 webhook 答了一个问题:这个载荷来自于知道共享密钥的人吗?

这与问是否请求是最新的或您已经处理过它是不同的。 签名验证是第一道门槛,而不是最后一道门槛。

一个图表,展示了验证 webhook 签名的六步流程,以确保请求的真实性和安全性。

验证流程

确保请求的真实性和安全性

The usual HMAC flow looks like this:

  1. 从提供者的头部读取签名。
  2. 读取 原始请求正文 完全按照原始接收的方式读取。
  3. 从安全配置中加载您的 webhook 密钥。
  4. 使用相同的算法重新计算预期的 HMAC 值。
  5. 使用安全的比较方法比较接收到的签名和计算的签名。
  6. 如果它们不匹配,则拒绝请求。

那一步就是很多好实现都失败的地方。如果您的框架首先解析 JSON,重新格式化空格或更改编码详细信息,然后再进行散列,计算的签名就不会与提供者的匹配。

在真实的code中要注意什么

这些是我经常看到的错误:

  • 在解析 JSON 后进行哈希. 不要 JSON.stringify(req.body) 和期望它匹配。
  • 使用正常的字符串等式. 使用安全的时间比较
  • 硬编码机密. 将它们保存在环境变量或密钥管理器中
  • 依赖头部. 如果您没有验证签名头部,则它是无意义的

为了团队在服务之间加强机密处理,Capgo关于 API应用商店合规性中的密钥安全 是相关的,因为同样的纪律也适用于 webhook 接收器。

A generic verification example

const crypto = require('crypto');

function verifySignature(rawBody, receivedSignature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(receivedSignature, 'utf8');
  const b = Buffer.from(expected, 'utf8');

  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

This is intentionally generic. Real providers often prefix signatures, combine timestamps into the signed content, or encode the digest differently. The rule stays the same. Follow the provider’s exact signing format, and always verify against the raw payload.

防止重放攻击

A signed webhook can still be dangerous if it arrives hours later and your handler treats it as new. That happens more often than teams expect. Proxies log traffic, request payloads leak into the wrong place, or a provider retries after a network failure and your endpoint processes the same event twice.

防止重放攻击的五项关键安全措施

Signature verification answers one question: did the sender create this payload with the shared secret? Replay protection answers a different one: should this request still be accepted right now? Production receivers need both.

最小的检查

A practical replay defense starts with a signed timestamp. The provider includes a timestamp in headers or in the signed message, and your receiver rejects requests that fall outside a small tolerance window.

该流程应如下所示:

  • 从提供者定义的位置读取时间戳不要猜测头部名称。
  • 将其解析为整数或RFC格式的日期基于提供者的规范。
  • 与您的服务器时间进行比较.
  • 拒绝过时或过期的请求.
  • 在签名方案中验证时间戳 当提供者支持时。

这点很重要。如果时间戳不在签名中,攻击者可以将新时间戳插入并重放原始主体。通常我会检查提供者的签名格式以确定是否可以信任时间戳逻辑。

选择容忍度窗口的方法

五分钟是常见的默认值。它足够短以缩小攻击窗口,但足够长以承受小时钟漂移和正常网络延迟。

这里存在权衡。30秒的窗口听起来更安全,但在实际系统中会更频繁地出现问题,尤其是在重试、排队或区域延迟的情况下。30分钟的窗口更容易操作,但如果签名请求被泄露,攻击者将有更多时间。从几分钟开始,同步您的服务器以使用NTP,然后只在提供者的交付模式支持时才进行调整。

重放防御不仅仅是时间戳检查

时间戳验证可以阻止过时的请求,但不能阻止同一时间戳内的重复处理。如果同一签名事件在有效窗口内被两次传递,应用程序仍然需要识别它。

使用第二层:

  • 记录事件 ID 或交付 ID 在一个短暂的存储中,如 Redis。
  • 将处理程序视为幂等的 以便重复交付不会创建重复订单、电子邮件或计费操作。
  • 记录被拒绝的陈旧请求 以原因代码记录,但永远不要记录机密或完整的敏感负载。
  • 返回快速响应 在验证和队列繁重的工作之后。

已经习惯考虑过期时间窗口和撤销的团队会认识到这个模式。Capgo关于 Capacitor应用中的令牌撤销模式指南 涵盖了相同的运营理念。一个凭据或请求在有效时不应永远被信任。

已签名且陈旧仍然是不安全的。

在 Node.js 中构建一个 Webhook 接收器

使用 Node 和 Express 是快速部署一个严肃的接收器的最快方法,但有一个陷阱比其他任何一个更重要。您需要访问原始的请求体,而不是 Express 将其转换为对象之前。

一台笔记本电脑放在木桌上,显示 Node.js 接收器 code 在 VS Code 编辑器环境中。

一个生产环境的 Express 示例

const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Capture raw body for signature verification
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

function safeEqual(a, b) {
  const aBuf = Buffer.from(a, 'utf8');
  const bBuf = Buffer.from(b, 'utf8');
  if (aBuf.length !== bBuf.length) return false;
  return crypto.timingSafeEqual(aBuf, bBuf);
}

function verifySignature(rawBody, secret, receivedSignature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return safeEqual(expected, receivedSignature);
}

function isFresh(timestampHeader, toleranceSeconds = 300) {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= toleranceSeconds;
}

app.post('/webhooks/example', async (req, res) => {
  const signature = req.get('x-webhook-signature');
  const timestamp = req.get('x-webhook-timestamp');

  if (!WEBHOOK_SECRET) {
    return res.status(500).send('Webhook secret is not configured');
  }

  if (!signature || !timestamp) {
    return res.status(400).send('Missing required security headers');
  }

  if (!isFresh(timestamp)) {
    return res.status(401).send('Stale webhook');
  }

  const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge quickly
  res.status(200).send('OK');

  // Process after acknowledgement
  try {
    const event = req.body;
    console.log('Accepted event:', event.event, event.id);
    // enqueueJob(event)
  } catch (err) {
    console.error('Post-ack processing failed:', err);
  }
});

app.listen(PORT, () => {
  console.log(`Webhook receiver listening on ${PORT}`);
});

为什么这个结构能够持续

这里有一些选择是故意的:

  • 原始请求体捕获发生在中间件中. 这样就可以保留原始的字节用于哈希。
  • 时间戳在业务逻辑之前被检查. 没有必要为过期流量做工作。
  • 该路由返回 200 快速. 长时间的工作应该放在队列或后台任务中。
  • Post-ack 处理是隔离的. 即使下游逻辑失败,接收路径也保持小。

Secrets 是很多 webhook 实现的弱点。不要将它们放在源代码中,paste 不要将它们粘贴到测试用例中,也不要在日志中回显它们。如果您需要更广泛的过程来处理旋转和 CI 处理,Capgo 的指南 在 CI/CD pipeline 中管理 secrets 的 很好地处理了运营方面。

如果您想看到移动的部件,一个短的教程会很有帮助:

我会改变的东西

对于一个真正的提供商集成,我会在持久存储中添加事件 ID 去重,在请求 ID 的结构化日志中添加,并在确认路径后面添加一个队列。另外,我会避免使用单个通用端点,如果多个提供商使用不同的签名格式。分离的处理程序更容易理解,也更难破坏。

在 Python 中构建一个 Webhook 接收器

Flask 是一个适合清洁 web hook 示例的好选择,因为请求处理是明确的,Python 的标准库已经为您提供了 HMAC 所需的内容。

主要要记住的是与 Node 一样,验证原始请求字节,而不是解析的 JSON 字典。

A Flask example with signature and timestamp checks

import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")

def is_fresh(timestamp_header, tolerance_seconds=300):
    try:
        timestamp = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    now = int(time.time())
    return abs(now - timestamp) <= tolerance_seconds

def verify_signature(raw_body, secret, received_signature):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received_signature)

@app.route("/webhooks/example", methods=["POST"])
def webhook():
    if not WEBHOOK_SECRET:
        return "Webhook secret is not configured", 500

    signature = request.headers.get("X-Webhook-Signature")
    timestamp = request.headers.get("X-Webhook-Timestamp")

    if not signature or not timestamp:
        return "Missing required security headers", 400

    if not is_fresh(timestamp):
        return "Stale webhook", 401

    raw_body = request.get_data()

    if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
        return "Invalid signature", 401

    payload = request.get_json(silent=True) or {}

    # Acknowledge receipt
    response = jsonify({"status": "ok"})

    # In production, queue payload here instead of heavy sync work
    print("Accepted event:", payload.get("event"), payload.get("id"))

    return response, 200

if __name__ == "__main__":
    app.run(port=5000, debug=True)

Flask相关细节

request.get_data() 是这里的关键调用。它给你请求体的原始字节。如果你直接跳到 request.json,你已经越过了签名不匹配的混淆界限。

一些实现注意事项:

  • 使用 hmac.compare_digest 而不是平等。
  • 当客户端缺失头部时,视其为客户端失败 并拒绝请求。
  • 使用 silent=True 进行JSON解析 如果你想控制错误处理而不是让Flask抛出异常。
  • 保持路由薄弱. 如果载荷触发任何昂贵的操作,则在队列中排队。

不要通过放松安全检查来调试签名不匹配。通过打印您计算的字节和提供者期望的格式来调试它们。

团队通常会卡在哪里

常见的失败路径是使用手工构建的 JSON 体验,然后切换到真实的提供商并发现签名不再匹配。通常这意味着其中一个三件事情:提供商签署了带有时间戳的封velope,签名以您假设的方式编码不同,或者中间件在验证之前修改了体。

当发生这种情况时,停止随机更改加密code。捕获原始头和原始体, reproduce 在一个小的隔离脚本中 hash,然后将其放回 Flask 路由中。

在 Go 中构建 Webhook 接收器

Go 是一个很好的选择来接收 webhook,因为标准库足够。您不需要框架来获得一个小而可靠的处理程序,而且code很容易保持诚实。

需要小心的是体处理。 r.Body 是一个流。读取它一次,hash 得到的字节,然后从同样的字节中反序列化。

标准库示例

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type WebhookPayload struct {
	Event string          `json:"event"`
	ID    string          `json:"id"`
	Data  json.RawMessage `json:"data"`
}

func isFresh(timestampHeader string, toleranceSeconds int64) bool {
	ts, err := strconv.ParseInt(timestampHeader, 10, 64)
	if err != nil {
		return false
	}

	now := time.Now().Unix()
	diff := now - ts
	if diff < 0 {
		diff = -diff
	}

	return diff <= toleranceSeconds
}

func verifySignature(rawBody []byte, secret string, received string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))

	if len(expected) != len(received) {
		return false
	}

	return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}

func webhookHandler(secret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		signature := r.Header.Get("X-Webhook-Signature")
		timestamp := r.Header.Get("X-Webhook-Timestamp")

		if signature == "" || timestamp == "" {
			http.Error(w, "missing required security headers", http.StatusBadRequest)
			return
		}

		if !isFresh(timestamp, 300) {
			http.Error(w, "stale webhook", http.StatusUnauthorized)
			return
		}

		rawBody, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "failed to read body", http.StatusBadRequest)
			return
		}

		if !verifySignature(rawBody, secret, signature) {
			http.Error(w, "invalid signature", http.StatusUnauthorized)
			return
		}

		var payload WebhookPayload
		if err := json.Unmarshal(rawBody, &payload); err != nil {
			http.Error(w, "invalid json", http.StatusBadRequest)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))

		log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
	}
}

func main() {
	secret := os.Getenv("WEBHOOK_SECRET")
	if secret == "" {
		log.Fatal("WEBHOOK_SECRET is not set")
	}

	http.HandleFunc("/webhooks/example", webhookHandler(secret))

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

为什么 Go 在这里感觉坚实

一些显著的好处是:

  • 处理器是明确的. 没有隐藏的中间件魔法.
  • 边缘的输入帮助. 头部解析、时间戳转换和 JSON 解码都失败了.
  • 标准的加密包就足够了. 基本的 HMAC 验证不需要额外的依赖.

操作说明

如果 webhook 流量增长,Go 的并发模型给你足够的空间来分散后台工作而不改变 HTTP 入口点。即使这样,也要保持接收器狭窄。接受、验证、确认,然后转移.

最强大的 Go webhook 处理器我见过的都保持着平淡。它们不混合运输验证与业务逻辑,并且在响应返回之前不进行数据库密集型工作.

必备调试技巧

一个 webhook 错误通常表现为支持消息,而不是堆栈跟踪。提供者说他们已经传递了事件。你的端点说什么都没有到达应用程序,或者在一个看起来在第一眼有效的请求上,签名验证失败。到那时,调试就是重建精确的 HTTP 交换,字节为字节,证明它在哪里破裂了.

A list of five essential tools and techniques for debugging webhooks in a software development environment.

A practical debugging toolkit

从wire格式开始。

如果签名校验失败,请捕获原始请求正文(包括接收时的原始字节)以及用于验证的头部信息。实际上,bug往往是枯燥的。框架在散列之前解析了JSON,代理更改了编码,测试重放忽略了原始时间戳头部。仅仅记录解析的对象是不够的。您需要原始字节和验证输入。

These tools help isolate the problem fast:

  • 原始请求捕获. 在调查期间,请记录头部、内容类型、内容长度和未修改的正文。
  • 请求检查端点. 服务如 webhook.site 帮助确认发送者传输的内容。
  • 本地隧道. ngrok 和类似的工具让您可以在不通知提供商的情况下测试本地接收器。
  • 手动重播. 使用相同的请求体和头部在 curl 或 Postman 中重播请求。 这是确认是否是 code 或服务端 payload 的最快方法。
  • 服务端提供日志. 发送者控制台通常包含响应代码、重试历史和请求标识符,您可以将其与您的日志匹配。

模式很重要。 从外向内工作。 首先确认服务端发送了您期望的内容。 然后确认您的服务器接收到了相同的字节。 然后确认您的 code 使用相同的密钥和时间戳规则对字节进行了相同的哈希。

有用的日志

好的 webhook 日志应该在一个搜索中回答三个问题:

问题有用的日志字段
请求是否到达?route, method, received_at
為什麼被拒絕了?__CAPGO_KEEP_0__
是否可以稍後進行相關聯?__CAPGO_KEEP_0__, __CAPGO_KEEP_1__

第四個字段在實際系統中會有所幫助。添加一個本地的 request_id 由您的接收器生成,以便您可以通過您的應用程式、佇列和工作器日誌跟蹤請求。

對存儲的選擇性。永遠不要記錄機密資訊。避免將包含客戶資料、存取令牌或帳單細節的完整生產負載進行擷取。如果您包含客戶資料、存取令牌或帳單細節的完整生產負載,請記錄元資料加上短的身體雜湊。這樣仍然讓您可以比較重試和驗證兩個交付是否相同。

用原始輸入重現失敗

這是基本教程跳過的部分。如果您無法重新播放失敗的請求,則您是在猜測。

儲存失敗的Webhook為:

  • 原始身體位元組
  • 所有與簽名相關的標頭
  • 请求时间戳
  • 内容类型
  • 服务端请求ID

然后将其重放到一个测试环境中。如果重放通过,比较传输过程中发生了什么变化。常见的原因包括中间件对请求体进行标准化、字符编码不匹配以及负载均衡器将或重写请求头。还有一些团队会将美化后的请求体从仪表盘视图中复制到实际请求体中,这也会导致HMAC验证失败。仅仅是空格的差异就足以导致HMAC验证失败。

为了更广泛的发布和移动传输调试,相同的调试习惯在Capgo的指南中 tools for debugging OTA updates in Capacitor不同传输方式,相同的教训。捕获请求路径的实际值之前不要改变应用程序code。

如果签名验证失败,请检查原始字节、用于验证的精确请求头和时间戳值之前不要接触加密code。

生产就绪的Webhook检查表

通常在测试环境中,Webhook处理程序看起来很好,直到第一个重试风暴、错误的请求体或签名不匹配的事件发生在凌晨2点。生产环境的标准更高。接收端需要拒绝伪造的请求、接受合法的重试请求,并为操作员提供足够的信号来调试失败而不暴露敏感数据。

安全性和正确性检查

  • 验证每个请求签名. URL 地址泄露。测试 URL 在聊天中被分享。签名验证是告诉您发送者知道共享密钥的控制器。
  • Reject old requests. 有效签名可以在旧载荷上重放。强制使用与提供商重试模型匹配的时间戳容差。
  • Hash raw body,而不是解析的 JSON. 中间件可以重新排序键,规范空格或更改编码。验证必须在接收到的原始字节上运行。
  • Keep signing secrets out of code. 环境变量是基础。 如果您定期轮换凭据或跨多个环境运行,则使用密钥管理器是一个更好的选择。
  • Fail closed on auth errors. 如果签名头缺失、格式错误或使用了意外的方案,拒绝请求并记录原因。

Reliability checks

  • Acknowledge fast. 提供商通常将任何 2xx 视为成功,因此验证请求、持久化所需的内容并将慢速工作转移到队列或工作人员中。
  • 保持处理器幂等性. 事件可能会多次到达。使用事件 ID、传递 ID 或稳定提供商标识符来隔离关键副作用。
  • 返回可预测的错误代码. 对于输入不正确的数据使用 400 ,对于验证失败使用 401 ,并且仅在系统出现问题时使用。这样可以使提供商重试行为更容易理解。 403 在解析之前设置限制 5xx . 在请求大小、内容类型和标头数量方面限制 Cap 请求。这可以防止 webhook 端点变成一个通用的 ingestion hole。
  • 保持契约狭窄. 只接受您支持的字段和事件类型。松散的解析在一开始可能会感到方便,但在提供商 __CAPGO_KEEP_0__ 变化时会变得很昂贵。
  • Make handlers idempotent. Accept only the fields and event types you support. Loose parsing feels convenient at first and becomes expensive during provider API changes.

__CAPGO_KEEP_0__

好 webhook 操作看起来很无聊。团队可以快速回答三个问题:我们是否接收到了它?我们是否验证了它?下游处理是否成功?

使用这个标准:

  • 单独跟踪接收、验证和处理结果.
  • 记录请求 ID、事件 ID、签名状态和时间偏差.
  • 衡量队列延迟、处理器延迟和重试次数.
  • 保留一个安全的重放路径,用于 staging 或重发工作流.
  • 在模式变化时警报例如,签名失败的激增或重复交付。

Capgo 是更广泛的运营点的有用例子。它包括了关于发布交付和可观察性的工具更新工作流中,以及其生态系统的一部分也触及了 webhook 相关流程。这个教训是实用的。交付系统需要从接收到完成的可见性。

如果团队覆盖了上述检查项,webhook 接收器通常在生产环境中是良好的。如果有任何项缺失,那么这个缺口通常在事故发生时才会显现,而不是在演示时。

关于 Webhook 常见问题

What status code should I return?

返回一个 200 当您接受了 webhook 时,返回 2xx 状态码。如果验证失败,请返回与失败相匹配的客户端或认证错误,例如 400 对于输入不正确的数据 401

对于无效的认证数据。保持该逻辑一致,以便于提供商仪表板更容易解释。

是否应该同步处理 webhook?

通常不应该。验证它,确认它,然后将实际工作推送到队列或后台工作线程中。这保持了传递路径的速度,并减少了由于下游处理速度慢而导致的重复重试。

如何处理重试?

假设它们会发生。将无害性构建到处理器中,以便接收相同事件不会导致副作用重复。事件 ID 或提供商传递 ID 是通常用于实现无害性的锚点。

如果事件到达顺序不正确呢?

How do I deal with webhook version changes?

故意将处理逻辑版本化。将供应商特定的解析分离出来,避免在代码库中散布载荷假设,并在推出对新格式的支持之前,使用真实捕获的样本添加测试。


如果您的团队部署 Capacitor 或 Electron 应用程序, Capgo 值得一提的是,它为团队提供了一个控制的方式来交付签名的 Web 更新、观察发布行为和在等待应用商店审查之前恢复从事故障。这种方式与坚实的 Webhook 设计背后的工程直觉相符:验证输入、保持发布路径可观察并快速恢复。

实时更新 Capacitor 应用

当 web 层 bug 活跃时,通过 Capgo 将修复推送给用户,而不是等待几天的 app store 审核。用户在后台接收更新,而原生变化保持在正常的审查路径中。

立即开始

博客最新文章

Capgo 给您创建真正专业的移动应用所需的最佳见解。