Passer au contenu
DedicatedPHP Contact

Webhooks dans le désordre en PHP sans corrompre l’état

Concevez une intégration PHP résistante aux événements dupliqués, tardifs et concurrents grâce à la validation, l’audit et l’idempotence.

Diagramme éditorial d’événements webhook dupliqués et tardifs traités par une application PHP avec contrôle d’état

Les webhooks dans le désordre en PHP constituent un problème de cohérence, et pas seulement de connectivité. Un fournisseur peut renvoyer une livraison parce qu’il n’a pas reçu de réponse valide, une file d’attente peut retarder un message ou deux événements d’une même entité peuvent emprunter des chemins distincts. Si l’application suppose que chaque événement arrive une seule fois et dans l’ordre, une confirmation ancienne peut écraser une annulation ultérieure, ou une répétition peut exécuter deux fois une opération irréversible.

La règle de départ est simple : un webhook est une notification indiquant que quelque chose a peut-être changé dans un autre système. Ce n’est pas, à lui seul, une instruction fiable pour modifier l’état local sans vérifications. La conception doit conserver les éléments reçus, décider quels événements sont admissibles et appliquer les modifications de manière idempotente et ordonnée selon les règles du domaine.

Séparer réception, validation et application au domaine

Séparer réception, validation et application au domaine — guía visual de DedicatedPHP

L’endpoint HTTP doit faire peu de choses et les faire de manière prévisible. Sa responsabilité consiste à recevoir la requête, à la vérifier, à persister un enregistrement immuable et à répondre dans le délai attendu par l’émetteur. Le travail qui modifie des commandes, abonnements, stocks ou toute autre entité métier devrait intervenir ensuite, généralement par le biais d’un processus asynchrone.

Séparer les phases évite qu’une indisponibilité transitoire d’une API interne transforme une livraison valide en une nouvelle tentative ambiguë. Cela permet également de reprendre le traitement sans demander au fournisseur de renvoyer d’anciens événements.

  1. Réception : capturer les en-têtes, le corps non transformé, l’instant de réception et l’origine identifiée.
  2. Validation d’entrée : vérifier la signature, le format, la taille, le type de contenu et les champs minimaux.
  3. Persistance : stocker l’événement et son état initial dans une transaction courte.
  4. Mise en file d’attente : signaler qu’un travail est en attente, sans dépendre de son traitement dans la réponse HTTP.
  5. Application : un worker interprète l’événement, obtient l’état nécessaire et exécute une transition métier contrôlée.

Il est important de distinguer une livraison d’un événement. Une même livraison peut être répétée, et certains fournisseurs attribuent un identifiant différent à chaque tentative de livraison. S’il existe un identifiant d’événement stable, celui-ci constitue généralement la meilleure base de déduplication. Dans le cas contraire, il faudra définir une clé comprenant l’origine, l’entité externe, le type et une version ou un horodatage dont la signification est connue.

Que consigner pour pouvoir auditer et retraiter

Une table d’événements ne doit pas stocker uniquement le JSON interprété. Conservez le corps original, car le normaliser avant son stockage peut supprimer des informations nécessaires pour vérifier une signature, enquêter sur un incident ou adapter un parser ultérieur.

Au minimum, l’enregistrement devrait contenir :

  • L’origine ou le fournisseur et l’environnement d’intégration.
  • L’identifiant externe de l’événement et, s’il existe, l’identifiant de livraison.
  • Le type d’événement, l’identifiant d’entité externe et la version, séquence ou date effective.
  • Les en-têtes pertinents et la charge utile originale protégée contre les modifications.
  • L’instant de réception local et, séparément, l’horodatage déclaré par l’émetteur.
  • L’empreinte cryptographique de la charge utile pour le diagnostic et la déduplication auxiliaire.
  • L’état de traitement : reçu, validé, en attente, appliqué, ignoré, en échec ou en revue.
  • Le nombre de tentatives, l’erreur résumée, l’instant de la dernière tentative et la référence à l’entité locale affectée.

Une contrainte unique sur (origine, external_event_id) résout les répétitions lorsque le fournisseur propose un ID stable. Insérez d’abord et traitez le conflit comme une livraison déjà connue, et non comme une erreur métier. La réponse peut rester satisfaisante afin d’arrêter les nouvelles tentatives.

Mais dédupliquer le message ne suffit pas à garantir l’idempotence. Par exemple, deux événements distincts peuvent exprimer la même confirmation et tenter tous deux de créer une écriture comptable. L’opération métier doit disposer de sa propre protection : une clé d’idempotence, une contrainte unique sur l’effet ou une transition qui vérifie si le résultat existe déjà.

Valider l’authenticité et limiter la surface d’entrée

N’acceptez pas un webhook parce qu’il provient d’une adresse IP attendue ou parce qu’il inclut un champ qui semble secret. Lorsque le fournisseur le permet, validez une signature calculée sur le corps brut et un horodatage. La comparaison doit être effectuée en temps constant et la fenêtre temporelle doit limiter les tentatives de rejeu, en tenant compte du décalage raisonnable des horloges.

Avant de persister, imposez des limites opérationnelles : taille maximale du corps, durée de lecture, formats acceptés et schéma minimal. Un JSON valide n’est pas nécessairement un événement valide. Rejetez les types inconnus s’il n’existe pas de politique explicite permettant de les archiver sans appliquer d’effets.

Les secrets de signature nécessitent une rotation. Lors d’un changement, il peut être nécessaire d’accepter une ancienne clé et une nouvelle pendant une période délimitée, en consignant laquelle a validé la livraison. N’incluez pas de corps complets, de tokens ni de données personnelles inutiles dans les logs de l’application. Le journal d’audit doit disposer de contrôles d’accès et d’une politique de rétention adaptée à la sensibilité des données.

Décider de l’ordre logique, ne pas se fier à l’ordre réseau

L’heure de réception ne définit pas ce qui s’est produit en premier. De même, une date incluse dans le payload n’est pas toujours suffisante : elle peut être approximative, correspondre à la création de l’événement plutôt qu’à la transition, ou être affectée par des horloges non synchronisées. Le meilleur signal est une version monotone ou un numéro de séquence par entité fourni par le système source.

Lorsqu’une version existe, stockez la dernière version appliquée dans l’entité locale. Un worker ne peut appliquer un événement que si sa version est supérieure à celle stockée ; une version égale indique une répétition, et une version inférieure est un événement tardif. En cas de lacunes dans la séquence, n’inventez pas l’état intermédiaire : marquez l’entité pour rapprochement ou interrogez l’API source, si cette API est le référentiel.

S’il n’existe ni séquence ni version, les règles doivent relever du domaine. Une machine à états explicite est plus sûre que l’affectation directe d’un texte reçu. Par exemple, une entité annulée pourrait empêcher un retour à l’état confirmée sauf via une transition documentée et autorisée. Le modèle doit définir quoi faire pour chaque combinaison entre l’état actuel et l’événement entrant.

if ($eventVersion <= $entity->lastExternalVersion) {
    markIgnored($event, 'version_pas_plus_recente');
    return;
}

applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;

Le code illustre le critère ; il ne remplace ni la transaction ni les règles de transition. Pour les événements sans version, une comparaison de dates n’est acceptable que si le contrat de l’émetteur garantit leur sémantique et leur précision.

Traiter les événements tardifs selon le coût d’une erreur

Tous les événements retardés ne méritent pas la même réponse. Le choix entre ignorer, consigner, recalculer ou compenser dépend de la capacité de l’événement à modifier une obligation réelle et de la source de vérité.

  • Ignorer : approprié pour une ancienne version dont l’effet est déjà inclus dans un état ultérieur vérifiable.
  • Consigner et alerter : utile si la séquence est incohérente ou si des informations manquent pour décider sans intervention.
  • Recalculer : interroger l’état actuel dans le système externe et mettre à jour le miroir local lorsque la source externe prévaut.
  • Compenser : créer une action corrective traçable lorsqu’un effet antérieur a déjà produit des conséquences et ne peut pas être supprimé de manière sûre.

Considérez le cas hypothétique d’une opération externe. Une confirmation avec la version 12 arrive, puis une annulation avec la version 13 et, plus tard, la confirmation 12 est à nouveau tentée. Avec un contrôle de version, la répétition ne réactive pas l’opération. Si l’annulation arrive en premier et que le système sait que la version 12 manque, il peut appliquer l’annulation si la machine à états le permet ou demander un rapprochement avant de produire un effet sensible.

Concurrence interne, files d’attente et verrous par entité

Le traitement asynchrone améliore la réactivité, mais introduit des conditions de concurrence internes : deux workers peuvent lire le même état avant que l’un d’eux n’écrive. La déduplication de l’événement n’évite pas cette situation.

Pour les entités sensibles, sérialisez par clé d’entité externe ou locale. Cela peut être obtenu avec des partitions de file d’attente basées sur cette clé, un verrou distribué avec une expiration conçue avec soin ou un verrou de ligne au sein d’une transaction courte. Une autre option est le contrôle optimiste : ne mettre à jour que si la version stockée reste celle attendue et réessayer lors de la détection d’un conflit.

Évitez de maintenir une transaction ouverte lorsque vous appelez des services distants. Réservez ou lisez d’abord l’état de manière cohérente ; effectuez ensuite l’appel avec une clé idempotente lorsque cela est possible ; enfin, consignez le résultat. Si le processus échoue entre ces étapes, une nouvelle tentative doit pouvoir distinguer une opération en attente d’une opération déjà terminée.

Exploitation, observabilité et tests avant la publication

Exploitation, observabilité et tests avant la publication — guía visual de DedicatedPHP

Un tableau de bord opérationnel doit indiquer combien d’événements restent en attente, échouent de manière répétée, sont ignorés en raison de leur ancienneté, sont rejetés pour signature et présentent des lacunes de séquence. Mesurez également l’ancienneté de la file d’attente et le délai entre la réception et l’application. Ces signaux permettent de détecter une intégration dégradée avant que le décalage ne devienne un problème métier.

Conservez des mécanismes de retraitement qui partent de l’événement original et d’une version explicite du parser ou du gestionnaire. Retraiter ne signifie pas exécuter aveuglément : limitez la portée, consignez qui l’a demandé et maintenez actives les mêmes garanties d’idempotence.

Liste de vérification

  • Envoyer le même événement plusieurs fois, y compris de manière concurrente.
  • Livrer une annulation avant la confirmation associée.
  • Retarder un ancien événement jusqu’après un autre doté d’une version supérieure.
  • Introduire des lacunes, des types inconnus, des charges utiles tronquées et des signatures invalides.
  • Simuler l’arrêt du worker après la création d’un effet externe et avant le marquage de l’événement comme appliqué.
  • Vérifier que deux workers sur la même entité ne produisent pas une transition impossible.
  • Vérifier que le retraitement conserve l’audit et ne duplique pas les effets.

L’intégration robuste ne cherche pas à forcer le réseau à livrer dans l’ordre. Elle conçoit une frontière fiable : elle conserve chaque entrée vérifiable, applique des règles métier idempotentes, utilise un ordre logique lorsqu’il existe et effectue un rapprochement lorsqu’elle ne peut pas connaître l’état avec certitude.

Vous souhaitez appliquer ces idées à votre projet ?Parlons de votre plateforme PHP.
Afficher les services associés