Guzzle 8.0 est là — les annonces de release sont tombées autour du 21 juillet 2026 — et la fonctionnalité que tout le monde met en avant, c'est HTTP/3. Très bien. Ça grignotera un peu de latence de handshake sur les intégrations bavardes et ça rendra bien dans un changelog. Mais le changement qui va vraiment modifier ta semaine d'astreinte est plus terne : les échecs sont désormais interrogeables. Tu peux demander ce qui est mort et à quel moment, au lieu de recevoir un unique type d'exception qui signifie « il s'est passé quelque chose sur Internet ». C'est cette mise à jour-là qui mérite qu'on lui consacre un sprint. Et voici quand même ma thèse : ça ne te sauvera pas, parce que le difficile, dans un appel HTTP raté, n'a jamais été le diagnostic. C'était de décider quoi faire ensuite, et cette décision n'a jamais habité la librairie cliente.
Prends l'exemple qui revient sans arrêt dans ces discussions, parce que c'est celui qui coûte de l'argent : ton service fait un POST vers un prestataire de paiement pour créer une commande, et l'appel part en timeout. Deux mondes sont compatibles avec ce timeout. Dans le premier, la connexion TCP n'a jamais été établie et rien ne s'est produit en face. Dans le second, le prestataire a reçu ta requête, écrit la commande, débité la carte, et la réponse est morte sur le chemin du retour. L'ancien code attrapait un RequestException bien large, haussait les épaules et rejouait. Dans le premier monde, c'est correct. Dans le second, tu viens de facturer quelqu'un deux fois, et ton support l'apprendra avant ton dashboard.
Guzzle 8 aide réellement ici, et je ne veux pas le brader. Savoir qu'une connexion a été refusée avant qu'un seul octet du corps de la requête ne sorte, c'est catégoriquement différent de savoir que tu as tout envoyé et que la lecture a expiré à la 29e seconde. Le premier cas est rejouable sans ambiguïté. Le second est un pile ou face que tu ne devrais jamais jouer. Pouvoir distinguer les deux dans un bloc catch — plutôt qu'en grepant des codes d'erreur curl dans une chaîne, ce que la plupart d'entre nous faisions — transforme une supposition en décision. C'est concret, et c'est pour ça que si on me forçait à choisir, je prendrais les diagnostics plutôt que HTTP/3.
Le contre-argument honnête, c'est que je sous-estime le travail sur le transport. HTTP/3 n'est pas cosmétique pour tout le monde. Si tu envoies en éventail des centaines de petits appels vers un fournisseur d'IA ou une API de cartographie depuis un worker sur un réseau qui perd des paquets, le head-of-line blocking est une taxe mesurable, et le supprimer se voit à des workers de queue qui terminent leur lot au lieu de rester plantés avec un pool de connexions saturé. Idem pour les points plus discrets de la release : ne plus laisser fuiter des identifiants dans les logs et les redirections, et ne plus appliquer en silence un correctif de compatibilité que tu n'as jamais demandé. Ce sont les valeurs par défaut qui évitent l'incident que tu n'aurais jamais relié à ton client HTTP. Je ne les traite pas de décoration.
Mais regarde ce qu'une hiérarchie d'exceptions plus riche te donne vraiment. Elle te dit la forme de l'échec. Elle ne peut pas te dire si l'endpoint distant est idempotent, parce qu'elle n'a aucune idée de ce que fait cet endpoint. Un timeout de lecture sur GET /v1/orders/42 et un timeout de lecture sur POST /v1/charges, c'est le même événement au niveau transport et deux événements complètement différents pour ton métier. Si ta politique de retry vit dans un middleware qui ne voit que le transport, elle finira par se tromper sur l'un des deux. Le seul correctif durable est au niveau du protocole entre toi et ton prestataire : des clés d'idempotence générées par l'appelant, une ligne d'outbox écrite avant que la requête ne parte, et un job de réconciliation qui demande au prestataire ce qu'il pense qu'il s'est passé, au lieu de deviner.
Donc la mise à jour que je recommanderais vraiment tient en deux parties, et la seconde n'est pas dans le composer.json. D'abord, passe à Guzzle 8 et va supprimer tes handlers fourre-tout — chaque endroit où tu as écrit catch (RequestException $e) puis rejoué trois fois avec un sleep est maintenant un endroit où tu peux faire un choix défendable. Ensuite, et c'est la partie qui prend du temps, trie tes appels sortants en deux listes : ceux qu'on peut envoyer deux fois sans risque, et les autres. La seconde liste est plus courte que tu ne le crois, et chaque entrée a besoin d'une clé d'idempotence ou d'un chemin de réconciliation. Fais ce travail et la mise à jour de la librairie devient un multiplicateur. Saute-le et tu auras acheté de bien meilleurs logs décrivant le même double débit.
Ce sur quoi je continue d'hésiter, c'est l'endroit où cette décision de retry devrait physiquement vivre. Je l'ai construite en middleware client, où elle est propre, réutilisable et complètement aveugle au sens de la requête. Je l'ai aussi construite dans la classe de service, à côté de la logique métier, où elle est fine, correcte et copiée-collée à onze endroits. Ni l'un ni l'autre ne m'a paru juste. Toi, où tu traces la ligne : tes clés d'idempotence sont frappées dans la couche HTTP ou dans le domaine, et comment tu empêches les deux de diverger ?
Commentaires
Pas encore de commentaire — écris le premier.
Lance la discussion
Pas de compte ni de mot de passe — saisis simplement ton adresse e-mail et nous t’envoyons un lien de connexion à usage unique. Première visite ? Tout se met en place automatiquement.
Ton évaluation sera appliquée automatiquement après ta connexion.
Vérifie ta boîte mail
Nous avons envoyé un lien de connexion à …. Ouvre-le sur cet appareil — cet onglet te connectera automatiquement.
En attente de ton clic …
·