NestJS webhook 处理器会在细节上失败。全局 ValidationPipe 会在 guard 运行前剥离字段,默认 JSON 解析会破坏提供商签名的原始字节。在把隧道 URL 粘贴到 Stripe 或 GitHub 之前,本地测试应验证 rawBody: true、签名 guard 和路由级校验。
在 bootstrap 启用 raw body
创建 Nest 应用时传入 rawBody: true,让 Express 保留未改动的 buffer:
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(3000);
}
没有此标志,req.rawBody 为 undefined,每次 HMAC 都会失败。
签名用 @Body() 还是 req.rawBody
不要用 @Body() 验证签名 — Nest 已解析对象。读取 raw-body 中间件的 buffer:
@Post('stripe')
@UseGuards(StripeSignatureGuard)
handleStripe(@Req() req: RawBodyRequest) {
const payload = req.rawBody;
const event = JSON.parse(payload.toString('utf8'));
this.events.process(event);
return { received: true };
}
guard 确认后再 parse JSON。顺序:guard 优先,反序列化其次。
在 ValidationPipe 生效前使用签名 guard
将提供商验证封装在 guard 中,读取请求头并恒定时间比较 digest:
@Injectable()
export class StripeSignatureGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest>();
const sig = req.headers['stripe-signature'] as string;
return verify(req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
}
}
仅在 webhook 路由上用 @UseGuards 应用 guard。
避免全局 ValidationPipe 陷阱
带 transform: true 的全局 ValidationPipe 可能在控制器前强制类型。webhook 路由需要 rawBody + 验证后手动 JSON.parse。
用 nest start + PortPreview 本地测试
- 启动:
npm run start:dev或nest start --watch端口 3000。 - 暴露:
npx portpreview 3000。 - 在控制台注册隧道 URL + webhook 路径。
- 触发测试投递。
- 重放重复 event ID — 见 重试模式。
常见陷阱
签名前的 ValidationPipe
若校验先运行,Nest 可能变换 body 导致 rawBody 为空。guard 必须在 raw buffer 上执行。
全局 JSON 中间件顺序
raw-body 之前的 Express json() 会消耗 stream。创建 factory 时保持 rawBody: true。
慢 handler 与重试
验证后快速返回 200,重活入队,按 event ID 去重。
延伸阅读
基础阅读localhost 隧道基础和本地 webhook 调试。签名机制见签名验证指南。防重复处理见重试与幂等性模式。免费开始使用 PortPreview。
