Le WordPress d'aujourd'hui, décodé pour les développeurs

Tests

Tester la résilience d’un webhook Stripe rejoué deux fois par erreur

Trois webhooks Stripe identiques reçus en quatre secondes ont créé trois commandes. Voici comment tester l'idempotence du traitement plutôt que la signature.

Par Clément Hadrot • 4 août 2024 • 4 min de lecture • Aucun commentaire
Tester la résilience d'un webhook Stripe rejoué deux fois par erreur

Trois requêtes identiques, même id d’événement evt_1P..., reçues en quatre secondes sur l’endpoint /wp-json/paiements/v1/stripe-webhook. Stripe garantit la livraison au moins une fois, jamais exactement une fois — c’est écrit noir sur blanc dans sa documentation, mais nous ne l’avions pas pris au sérieux avant qu’un client ne se retrouve avec trois commandes facturées pour un seul paiement.

La vérification de signature, elle, fonctionnait parfaitement : chacune des trois requêtes était authentique, envoyée par Stripe, avec un en-tête Stripe-Signature valide. Le problème n’était pas l’authenticité, mais l’absence de protection contre le traitement redondant du même événement. C’est ce point précis que nous testons désormais systématiquement, sans revenir sur la vérification de signature elle-même, déjà couverte dans une autre suite.

Reproduire le doublon en test

La première étape consiste à capturer un événement réel de type checkout.session.completed (via le CLI Stripe, en mode test) et à le rejouer littéralement à l’identique dans un test PHPUnit, en appelant deux fois de suite le même contrôleur avec le même corps de requête.

public function test_meme_evenement_rejoue_ne_cree_pas_deux_commandes(): void
{
    $payload = file_get_contents(__DIR__ . '/fixtures/checkout-session-completed.json');

    $this->traiterWebhook($payload);
    $this->traiterWebhook($payload);

    $commandes = get_posts(['post_type' => 'commande', 'post_status' => 'any']);
    $this->assertCount(1, $commandes);
}

Ce test à lui seul aurait détecté l’incident avant sa mise en production. Il ne teste rien de la cryptographie, seulement la conséquence métier d’une réception en double.

Où placer le verrou d’idempotence

Deux stratégies s’affrontent. La première consiste à vérifier, avant tout traitement, si l’id de l’événement Stripe a déjà été traité — en le stockant dans une table dédiée ou en meta d’une commande existante. La seconde consiste à rendre l’opération elle-même idempotente, par exemple en cherchant d’abord une commande existante liée à l’id de session Stripe avant d’en créer une nouvelle.

L'essentiel à retenir : Un webhook Stripe peut arriver plusieurs fois pour un même événement ; La vérification de signature ne protège pas contre le doublon ; Un test doit rejouer l'événement et vérifier l'absence d'effet en double

Nous privilégions la première option quand le traitement a des effets de bord difficiles à rendre naturellement idempotents (envoi d’un e-mail, appel à un service tiers de facturation). La seconde suffit quand l’opération se limite à une création en base contrôlable.

Tester les cas limites du verrou

  • Deux requêtes concurrentes arrivant au même instant, avant que la première n’ait fini d’écrire son verrou — un test unitaire seul ne suffit pas ici, il faut simuler la concurrence
  • Un événement dont l’id a été traité puis purgé par une tâche de nettoyage trop agressive
  • Un événement de type différent mais lié à la même session Stripe (par exemple checkout.session.completed suivi de charge.succeeded) : deux événements légitimes distincts qu’il ne faut pas confondre avec un doublon

Pour le premier cas, un test d’intégration qui déclenche deux appels via des processus PHP séparés (avec proc_open) reste la méthode la plus fiable en local, faute de pouvoir simuler une vraie course critique dans un seul processus PHPUnit.

Automatiser le rejeu dans la CI

Nous conservons désormais un dossier de fixtures d’événements Stripe réels (anonymisés) rejoués systématiquement en double dans la CI, pour chaque type d’événement traité par le projet. La commande WP-CLI suivante permet de générer rapidement une fixture à partir du CLI Stripe en local :

stripe trigger checkout.session.completed --add checkout_session:client_reference_id=test-idempotence

En résumé

La signature d’un webhook Stripe prouve son authenticité, pas son unicité. Un test d’idempotence, même simple, coûte quelques lignes et détecte un bug qui, en production, se traduit directement par des remboursements et une perte de confiance du client. Documentation de référence : developer.mozilla.org pour les bonnes pratiques HTTP générales, et la documentation officielle Stripe pour le détail des garanties de livraison.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi