所有文章
如何在 localhost 测试 SendGrid Event Webhook
SendGridemail webhookssignature verificationlocalhost

如何在 localhost 测试 SendGrid Event Webhook

测试一个 SendGrid Event Webhook 在本地主机上,在本地运行您的处理程序,公开其端口 npx portpreview PORT,输入结果 HTTPS 端点为 SendGrid的 Post URL,并使用 Signed Event Webhook 在处理其事件之前使用公钥。

什么是 SendGrid Event Webhook 发送

这 Event Webhook 报告之后发生的事情 SendGrid 接受一条消息。可交付性事件包括 processed, delivered, deferred, bounce, 和 dropped。订婚活动包括 open, click、垃圾邮件报告和订阅更改。确切的字段因事件类型而异,因此主要路由 event 并将可选字段视为可选。

请求正文是 JSON 大批,不一定是一个对象。 SendGrid 可以将多个事件放在一个事件中 POST。一个处理程序假设 req.body.event 会默默地错过这批。官方 Event Webhook 参考 记录事件名称和字段,包括 sg_event_idsg_message_id.

使用事件作为事实,而不是命令。例如,一个 delivered 事件可以更新消息状态,而 click 可以附加参与记录。避免制作 click 处理程序会覆盖稍后的取消订阅状态,只是因为请求未按顺序到达。

1. 创建本地端点

这 Express 示例故意仅将原始主体解析器应用于 SendGrid 路线。签名验证取决于确切的字节 SendGrid 签署;解析和重新序列化 JSON 可以更改这些字节。

import express from 'express';
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';

const app = express();
const verifier = new EventWebhook();
const publicKey = verifier.convertPublicKeyToECDSA(
  process.env.SENDGRID_WEBHOOK_PUBLIC_KEY,
);

app.post(
  '/webhooks/sendgrid',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.get(EventWebhookHeader.SIGNATURE());
    const timestamp = req.get(EventWebhookHeader.TIMESTAMP());

    if (!signature || !timestamp || !verifier.verifySignature(
      publicKey,
      req.body,
      signature,
      timestamp,
    )) {
      return res.status(403).send('invalid signature');
    }

    let events;
    try {
      events = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.status(400).send('invalid JSON');
    }
    if (!Array.isArray(events)) {
      return res.status(400).send('expected an event array');
    }

    await enqueueNewEvents(events);
    return res.sendStatus(204);
  },
);

app.use(express.json());
app.listen(3000);

安装官方助手 npm install @sendgrid/eventwebhook。挂载全局 express.json() 在此路线之后,或明确排除此路径。同样的规则适用于 Next.js, Fastify, NestJS、无服务器函数,以及 API gateways:将原始主体保留为字符串或字节缓冲区,直到验证成功。官方 SendGrid 节点存储库有一个匹配的 签署 Event Webhook 例子.

2.给予 SendGrid 一个 HTTPS 网址

保持应用程序运行,然后 open 第二个终端:

npx portpreview 3000

PortPreview 打印公共 HTTPS 起源。如果是的话 https://example.portpreview.dev,完整的帖子 URL 为:

https://example.portpreview.dev/webhooks/sendgrid

该路径必须与路线完全匹配。测试时保持隧道进程处于活动状态。隧道转发流量;它不会取代您的本地服务器,因此连接失败通常意味着应用程序已停止、侦听另一个端口或以隧道无法到达的方式绑定。

3. 配置 Event Webhook 在 SendGrid

  1. 在 SendGrid 用户界面, open 设置 > 邮件设置.
  2. 在 Webhook 设置下, open Event Webhooks 并选择 创建新的网络钩子.
  3. 启用它,添加 PortPreview URL 作为发布 URL,并仅选择您的应用程序需要的操作。
  4. 在安全功能下,启用 Signed Event Webhook.
  5. 保存 webhook,重新open 其设置,复制生成的公共验证密钥,并将其存储为 SENDGRID_WEBHOOK_PUBLIC_KEY.
  6. 使用 Test Your Integration,然后发送真实消息来练习重要的事件类型。

SendGrid的电流 设置指南 请注意,测试发送示例事件而不是来自真实邮件发送的数据。测试前保存 signature 验证:密钥对是在验证时生成的 Signed Event Webhook 配置已保存。

如何 SendGrid的签名 webhook 验证有效

Signed Event Webhook 用途 ECDSA. SendGrid 保留私钥并向您显示相应的公共验证密钥。每次交付包括 X-Twilio-Email-Event-Webhook-SignatureX-Twilio-Email-Event-Webhook-Timestamp。验证涵盖 timestamp 与原始有效负载字节和 SHA-256 哈希;这 signature 是 Base64- 编码。官方助手处理公钥转换, signature 解码、散列和 ECDSA 确认。

这是非对称验证:显示的值是公钥,而不是 HMAC 秘密。不要让负载穿过 JSON.stringify()、修剪空格、附加换行符或一次验证一个数组元素。首先验证完整的请求字节,然后解析数组。看 SendGrid的 安全功能文档 对于算法和标题。

有效的 signature 确定签名字节来自持有者 SendGrid的私钥并且没有被更改。它不会使事件处理幂等、授权任意操作或证明事件是新的。这些是单独的控件。

使批处理幂等

SendGrid 重试失败 POSTs,网络可能会失去成功的响应。因此,重复交付是正常的。使用每个事件的 sg_event_id 作为主重复数据删除键,具有唯一的数据库约束。如果您的产品结合了多种 SendGrid 帐户或环境,按提供商和帐户或环境命名的密钥。

async function enqueueNewEvents(events) {
  for (const event of events) {
    await db.transaction(async (tx) => {
      const inserted = await tx.webhookReceipts.insertIfAbsent({
        provider: 'sendgrid',
        eventId: event.sg_event_id,
        receivedAt: new Date(),
      });
      if (!inserted) return;

      await tx.jobs.enqueue({
        type: 'process-sendgrid-event',
        payload: event,
      });
    });
  }
}

收据插入和持久队列应一起提交。批次持久后才返回2xx accepted。如果一个事件在其他事件提交后失败,则非 2xx 响应可能会导致整个请求返回;重复数据删除让下一次尝试跳过已经发生的事件 accepted 并安全继续。不要使用内存中的 Set 在生产中,因为重新启动会擦除它并且多个实例不会共享它。更广泛的 webhook 重试和幂等性指南 覆盖耐用的图案。

选择状态代码之前了解重试

根据 SendGrid的 Event Webhook 文档,2xx 响应标志着 POST 成功的。非 2xx 响应会导致在事件发生后最多 24 小时内以越来越长的间隔进行重试;这是每个新失败事件的滚动窗口。这种行为意味着永久 signature 失败还可能会产生重复尝试,而对于从未存储的事件返回 2xx 则会丢失该事件。

  • 2xx: 整批产品均经过验证且持久 accepted,或者每个事件都是已知的。
  • 4xx: 格式错误或未经身份验证的输入。仅记录安全诊断;预计 SendGrid的一般非 2xx 重试行为。
  • 5xx: 应重试的暂时性数据库、队列或应用程序故障。

保持请求路径简短:验证、验证外部形状、原子地重复数据删除和排队,然后响应。执行电子邮件分析更新, CRM 同步和工作人员通知。

本地故障排除 SendGrid 网络钩子

这 signature 总是无效的

最常见的原因是 JSON 中间件在验证之前消耗主体。确认验证者收到原件 Buffer,包括任何前导或尾随空格。然后检查公钥是否属于这个确切的 Event Webhook 配置并且两者 Twilio 标头到达应用程序时保持不变。更改环境后重新启动本地进程。

测试集成成功,但真实事件未出现

验证 Webhook 是否已启用以及是否选择了所需的操作。打开需要 open 跟踪,以及 click要求 click 追踪。另请记住,测试请求包含示例;使用实际发送来验证类似生产的字段和排序。

端点返回404或502

对于 404,将配置的路径与 /webhooks/sendgrid。对于网关错误,请确保本地应用程序正在传递到的同一端口上运行 PortPreview。如果请求到达但返回 500,请检查本地日志并暂时将处理程序减少到验证和持久捕获。

事件重复或无序

这是传输系统的现实,而不是隧道重复流量的证据。重复数据删除方式 sg_event_id,尽可能使状态转换单调,并将事件时间与接收时间分开存储。使用 本地 webhook 调试工作流程 隔离传输、身份验证和业务逻辑故障。

本地和生产使用的安全检查表

  • 使用 HTTPS 并验证每一个 signature 在解析或记录事件详细信息之前。
  • 将公共验证密钥保留在配置中,以便在 Webhook 密钥更改时可以干净地更新它。
  • 接受 POST only,限制请求大小,验证解析的值是一个数组,并仅允许您处理的事件名称。
  • 请勿将 PII 放入 SendGrid 类别或独特的论点; SendGrid的参考文献明确警告这些字段已存储且不被视为 PII。
  • 不要通过同一临时源公开管理会话、调试控制台或不相关的本地路由。
  • 不记录收件人地址、有效负载、 signatures,或环境值,除非必要且经过适当编辑。
  • 将临时隧道 URL 替换为稳定的生产 URL HTTPS 测试后端点,并禁用过时的 webhook 配置。

SendGrid 还可以使用 OAuth 2.0 为了 Event Webhook 安全,无论是单独的还是并排的 signatures。如果您的部署需要承载 -token 生命周期控制,遵循官方安全指南,而不是发明一个 token 交换。签名验证仍然很有价值,因为它绑定了确切的 timestamp 和有效负载字节。

生产就绪验收测试

  1. 发送签名的测试请求并确认 2xx 响应。
  2. 更改一个有效负载字节并确认 403 且没有数据库写入。
  3. 重播相同的有效请求并确认没有重复的作业或业务操作。
  4. 发送一个 JSON 对象而不是数组并确认受控 400。
  5. 短暂停止数据库,确认 5xx,恢复它,并验证是否重试 accepted 一次。
  6. 发送真实的电子邮件并确认所选的交付和参与活动遵循相同的路径。

一旦这些检查通过,就可以将端点移至生产环境,而无需更改验证和幂等性逻辑。有关更深入的加密故障模式,请阅读 网络钩子 signature 验证指南.

常见问题

能 SendGrid 发送 Event Webhook到本地主机?
不直接。本地运行处理程序,启动 `npx portpreview PORT`,并配置生成的public HTTPS URL 加上您的 webhook 路径为 SendGrid的帖子网址。
我如何验证 SendGrid 签署 Event Webhook?
阅读 X-Twilio-Email-Event-Webhook-Signature 和 X-Twilio-Email-Event-Webhook-Timestamp headers,保留完整的原始请求主体,并使用公钥验证它们 SendGrid的官方 Event Webhook 帮手。
为什么会 SendGrid webhook 验证失败后 JSON 解析?
这 ECDSA signature 涵盖了 timestamp 加上确切的原始有效负载字节。解析和重新序列化 JSON 可以更改空格或格式,因此必须在之前针对原始缓冲区或字符串进行验证 JSON 解析。
做 SendGrid 重试失败 Event Webhook是?
是的。 SendGrid 文档增加了每次事件后长达 24 小时内非 2xx 响应的重试间隔。仅在批次经过验证且持久后才返回 2xx accepted,并使用去重 sg_event_id.