所有文章
启用 rawBody 的 NestJS 服务器通过 guard 从 req.rawBody 验证 webhook 签名,nest start 经本地隧道暴露。
NestJSNode.jswebhook debugginglocal testing

NestJS Webhook:rawBody、签名 guard 与 ValidationPipe 陷阱

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 本地测试

  1. 启动:npm run start:devnest start --watch 端口 3000。
  2. 暴露:npx portpreview 3000
  3. 在控制台注册隧道 URL + webhook 路径。
  4. 触发测试投递。
  5. 重放重复 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

常见问题

为什么 NestJS webhook 中 req.rawBody 是 undefined?
必须向 NestFactory.create 传入 rawBody: true,否则 Express 不会附加 raw buffer。
能在 NestJS 中用 @Body() 验证签名吗?
不能。@Body() 返回已解析对象,与提供商签名的字节不一致。请用 rawBody: true 后的 req.rawBody。
如何在本地测试 NestJS webhook?
在 3000 运行 nest start,用 npx portpreview 暴露,注册隧道 URL 并触发测试事件。