NestJS の webhook handler は微妙に失敗します。グローバル ValidationPipe が guard 前にフィールドを削除し、デフォルト JSON パースが署名対象のバイトを壊します。Stripe や GitHub にトンネル URL を貼る前に 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 は既にオブジェクトを parse 済み。raw-body middleware の 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 確認後にのみ JSON を parse。順序:guard → デシリアライズ。
ValidationPipe より前に signature guard
プロバイダー検証を guard にカプセル化し、ヘッダー読み取りと定数時間比較:
@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);
}
}
guard は webhook ルートのみ @UseGuards で適用。
グローバル 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 middleware の順序
raw-body 前の Express json() は stream を消費。rawBody: true を factory 作成時に維持。
遅い handler とリトライ
検証後すぐ 200、重い処理はキュー、event ID で重複排除。
さらに学ぶ
基礎はlocalhost トンネリングとローカル webhook デバッグ。署名の仕組みは署名検証ガイド。重複安全なハンドラーはリトライと冪等性のパターン。PortPreview を無料で始める。
