Ya está fuera Guzzle 8.0 —los artículos sobre la release aparecieron alrededor del 21 de julio de 2026— y la novedad con la que todo el mundo abre es HTTP/3. Vale. Recortará algo de latencia de handshake en integraciones habladoras y quedará bien en un changelog. Pero el cambio que de verdad va a alterar cómo te va la semana de guardia es más aburrido: ahora los fallos son algo que puedes interrogar. Puedes preguntar qué murió y dónde, en lugar de recibir un único tipo de excepción que significa «ha pasado internet». Esa es la mejora que merece que le dediques un sprint. Y aun así mi tesis es esta: no te va a salvar, porque la parte difícil de una llamada HTTP fallida nunca fue el diagnóstico. Fue decidir qué hacer después, y esa decisión jamás ha vivido en la librería cliente.

Coge el ejemplo que sale una y otra vez en estas discusiones, porque es el que de verdad cuesta dinero: tu servicio hace un POST a un proveedor de pagos para crear un pedido y la llamada se va a timeout. Ese timeout es compatible con dos mundos. En uno, la conexión TCP nunca llegó a levantarse y al otro lado no pasó nada. En el otro, el proveedor recibió tu petición, escribió el pedido, cobró la tarjeta y la respuesta se murió en el camino de vuelta. El código de antes capturaba un RequestException genérico, se encogía de hombros y reintentaba. En el primer mundo eso es correcto. En el segundo acabas de cobrarle dos veces a alguien, y tu cola de soporte se entera antes que tu dashboard.

Guzzle 8 ayuda de verdad aquí, y no quiero quitarle mérito. Saber que una conexión fue rechazada antes de que saliera un solo byte del cuerpo de la petición es categóricamente distinto de saber que enviaste todo y que la lectura hizo timeout en el segundo 29. Lo primero se puede reintentar sin ninguna duda. Lo segundo es una moneda al aire que no deberías estar lanzando nunca. Poder distinguir esos dos casos dentro de un catch —en vez de haciendo grep de códigos de error de curl dentro de un string, que es lo que hemos estado haciendo casi todos— convierte una adivinanza en una decisión. Eso es real, y por eso, si alguien me obligara a elegir, me quedaría con los diagnósticos antes que con HTTP/3.

El contraargumento honesto es que estoy infravalorando el trabajo de transporte. HTTP/3 no es cosmético para todo el mundo. Si estás abriendo cientos de llamadas pequeñas en abanico hacia un proveedor de IA o una API de mapas desde un worker en una red con pérdidas, el head-of-line blocking es un impuesto medible, y quitarlo se nota en workers de cola que terminan su lote en vez de quedarse ahí parados con el pool de conexiones lleno. Lo mismo con los puntos más silenciosos de la release: no filtrar credenciales en logs y redirecciones, y no aplicar en silencio algún arreglo de compatibilidad que nunca pediste. Son esa clase de defaults que evitan un incidente que jamás habrías rastreado hasta tu cliente HTTP. No los estoy llamando decoración.

Pero mira lo que te da realmente una jerarquía de excepciones más rica. Te dice la forma del fallo. No puede decirte si el endpoint remoto es idempotente, porque no tiene ni idea de lo que hace ese endpoint. Un timeout de lectura en GET /v1/orders/42 y un timeout de lectura en POST /v1/charges son el mismo evento en la capa de transporte y eventos completamente distintos para tu negocio. Si tu política de reintentos vive en un middleware que solo ve el transporte, tarde o temprano se equivocará con uno de los dos. El único arreglo duradero está en el nivel de protocolo entre tú y tu proveedor: claves de idempotencia generadas por quien llama, una fila de outbox escrita antes de que salga la petición y un job de reconciliación que le pregunte al proveedor qué cree él que pasó, en vez de adivinarlo.

Así que la actualización que de verdad recomendaría tiene dos partes, y la segunda no está en el composer.json. Primero, pásate a Guzzle 8 y ve a borrar tus handlers atrapalotodo: cada sitio donde escribiste catch (RequestException $e) y luego reintentaste tres veces con un sleep es ahora un sitio donde puedes tomar una decisión defendible. Segundo, y esta es la parte que lleva tiempo de verdad, clasifica tus llamadas salientes en dos listas: las que se pueden enviar dos veces sin riesgo y las que no. La segunda lista es más corta de lo que crees, y cada entrada necesita una clave de idempotencia o un camino de reconciliación. Haz ese trabajo y la actualización de la librería se multiplica. Sáltatelo y lo que has comprado son unos logs mucho mejores describiendo el mismo cobro duplicado.

Lo que sigo dándole vueltas es dónde debería vivir físicamente esa decisión de reintento. La he construido como middleware del cliente, donde queda limpia y reutilizable y completamente ciega a lo que significa la petición. También la he construido en la clase de servicio, al lado de la lógica de dominio, donde es lista y correcta y está copiada y pegada en once sitios. Ninguna de las dos me acabó de convencer. ¿Dónde trazas tú esa línea: tus claves de idempotencia se acuñan en la capa HTTP o en el dominio, y cómo evitas que las dos se separen con el tiempo?