Tous les articles
Un serveur NestJS avec rawBody activé vérifiant les signatures webhook depuis req.rawBody via un guard, nest start exposé par un tunnel local.
NestJSNode.jswebhook debugginglocal testing

Webhooks NestJS : rawBody, guards de signature et pièges ValidationPipe

Les handlers webhook NestJS échouent de façon subtile. Un ValidationPipe global supprime des champs avant le guard, et le parsing JSON par défaut détruit les octets signés par le provider. Le test local doit prouver rawBody: true, des guards de signature et une validation ciblée avant de coller une URL tunnel dans Stripe ou GitHub.

Activer le corps brut au bootstrap

Passez rawBody: true à la création de l'application Nest pour qu'Express conserve un buffer intact :

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { rawBody: true });
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  await app.listen(3000);
}

Sans ce flag, req.rawBody est undefined et chaque HMAC échoue même si le réseau est correct.

@Body() vs req.rawBody pour les signatures

Ne vérifiez jamais les signatures avec @Body() — Nest a déjà parsé l'objet. Lisez le buffer du middleware raw-body :

@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 };
}

Parsez le JSON seulement après validation du guard. Ordre : guard d'abord, désérialisation ensuite.

Guard de signature avant les effets de ValidationPipe

Encapsulez la vérification provider dans un guard qui lit les en-têtes et compare les digests :

@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);
  }
}

Appliquez le guard avec @UseGuards uniquement sur la route webhook.

Éviter le piège ValidationPipe global

Un ValidationPipe global avec transform: true peut coercer les types avant le contrôleur. Les routes webhook doivent utiliser rawBody + JSON.parse manuel après vérification.

Test local avec nest start + PortPreview

  1. Démarrez : npm run start:dev ou nest start --watch sur le port 3000.
  2. Exposez : npx portpreview 3000.
  3. Enregistrez l'URL tunnel + chemin webhook.
  4. Déclenchez des livraisons test.
  5. Rejouez les event ID en double — voir patterns retry.

Pièges courants

ValidationPipe avant la signature

Si la validation passe en premier, Nest peut transformer le body et laisser rawBody vide. Les guards doivent s'exécuter sur le buffer brut.

Ordre du middleware JSON global

Le middleware json() Express avant raw-body consomme le stream. Gardez rawBody: true à la création et évitez les parsers dupliqués.

Handlers lents et retries

Renvoyez 200 vite après vérification, mettez le travail lourd en queue et dédupliquez par event ID.

Pour aller plus loin

Pour les bases, lisez les bases du tunneling localhost et le débogage webhook en local. Pour la cryptographie, consultez le guide de vérification de signature. Pour les handlers sans doublons, voyez les patterns retry et idempotence. commencez PortPreview gratuitement.

Questions fréquentes

Pourquoi req.rawBody est undefined dans mon webhook NestJS ?
Passez rawBody: true à NestFactory.create. Sans cela, Express n'attache pas le buffer brut.
Puis-je vérifier les signatures avec @Body() ?
Non. @Body() renvoie un objet parsé différent des octets signés. Utilisez req.rawBody avec rawBody: true.
Comment tester les webhooks NestJS en local ?
Lancez nest start sur 3000, exposez avec npx portpreview, enregistrez l'URL tunnel et déclenchez des événements test.