重试是常态
提供方会因为网络失败和进程重启而重试 Webhook。假设恰好一次投递的处理器迟早会重复扣款、重复发送或重复创建。提供方的事件 id 就是幂等键。把它与唯一约束一起存储,并让业务更新和去重插入在同一事务中提交。
CREATE TABLE processed_events (
provider text NOT NULL,
event_id text NOT NULL,
processed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (provider, event_id)
);
如果插入因唯一冲突失败,说明事件已处理过。应返回成功而不是错误,让提供方停止重试。如果业务更新失败,要同时回滚更新和插入,使后续重试可以再次尝试。
先验签再解析
签名验证必须使用原始请求体。框架先解析 JSON 再序列化会改变空白和键顺序,导致 HMAC 不再匹配。只在 Webhook 路由上挂载原始体中间件,验签后再解析。使用常量时间比较,并拒绝时间戳超出小窗口的请求以限制重放。
快速确认,安全处理
提供方超时很快。在请求内只做最小的持久化工作,把其余工作入队,然后返回 2xx。如果确认后队列发布可能失败,就用与去重记录同一事务的 outbox 行。这样事件要么被完整接受,要么可以在不重复业务效果的情况下重试。
测试重试
发送两次相同的已签名负载,断言只发生一次状态变更且两次都返回 2xx。发送签名错误的负载,断言 401 且没有写入。如果提供方不保证顺序,还要测试乱序事件,验证业务逻辑会拒绝或缓冲过期更新。