测试一个 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_id 和 sg_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
- 在 SendGrid 用户界面, open 设置 > 邮件设置.
- 在 Webhook 设置下, open Event Webhooks 并选择 创建新的网络钩子.
- 启用它,添加 PortPreview URL 作为发布 URL,并仅选择您的应用程序需要的操作。
- 在安全功能下,启用 Signed Event Webhook.
- 保存 webhook,重新open 其设置,复制生成的公共验证密钥,并将其存储为
SENDGRID_WEBHOOK_PUBLIC_KEY. - 使用 Test Your Integration,然后发送真实消息来练习重要的事件类型。
SendGrid的电流 设置指南 请注意,测试发送示例事件而不是来自真实邮件发送的数据。测试前保存 signature 验证:密钥对是在验证时生成的 Signed Event Webhook 配置已保存。
如何 SendGrid的签名 webhook 验证有效
Signed Event Webhook 用途 ECDSA. SendGrid 保留私钥并向您显示相应的公共验证密钥。每次交付包括 X-Twilio-Email-Event-Webhook-Signature 和 X-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 和有效负载字节。
生产就绪验收测试
- 发送签名的测试请求并确认 2xx 响应。
- 更改一个有效负载字节并确认 403 且没有数据库写入。
- 重播相同的有效请求并确认没有重复的作业或业务操作。
- 发送一个 JSON 对象而不是数组并确认受控 400。
- 短暂停止数据库,确认 5xx,恢复它,并验证是否重试 accepted 一次。
- 发送真实的电子邮件并确认所选的交付和参与活动遵循相同的路径。
一旦这些检查通过,就可以将端点移至生产环境,而无需更改验证和幂等性逻辑。有关更深入的加密故障模式,请阅读 网络钩子 signature 验证指南.
