title: "retry-client: retentatives automatiques dans le client HTTP"
labels: ["Epic"]
🎯 Problème
Chaque échec transitoire (429, 529, 5xx, timeout réseau) remonte immédiatement à l'appelant de Typesafe::Client#evaluate : chaque utilisateur du gem doit réécrire à la main la boucle « attendre puis rejouer », et la plupart ne le font pas. Leurs intégrations restent fragiles aux salves de surcharge, faute de résilience zéro-config.
💡 Solution
Le client retente automatiquement les erreurs retentables (429, 529, 5xx) et les erreurs de connexion (Typesafe::ConnectionError), par défaut 2 retries. Le délai suit Retry-After quand le serveur l'impose, sinon un backoff exponentiel 0,5 s → 8 s avec jitter. Toute la politique se règle via un unique Hash retry_options: sur Client.new ; retry_options: { max_retries: 0 } retrouve le comportement actuel.
🧭 Décisions arrêtées
- Retry actif par défaut : 2 retries (3 tentatives), opt-out via
retry_options: { max_retries: 0 } (ADR-0001, qui écarte l'ancien contrat « retry 429/529 manuel »).
- Périmètre : toutes les erreurs retentables (
APIError#retryable? : 429, 529, 5xx) plus les erreurs de connexion.
- Nouvelle
Typesafe::ConnectionError < APIError encapsulant les échecs réseau (connexion refusée, DNS, timeout, reset), marquée retentable.
- Configuration : un unique argument
retry_options: (Hash) sur Client.new, hérité par Typesafe::Jev ; pas d'override par appel.
- Clés de
retry_options: : max_retries / base_delay / max_delay, défauts 2 / 0,5 / 8,0 ; nil = défauts ; Hash normalisé figé sur le client.
- Validation stricte :
ArgumentError sur clé inconnue ou valeur invalide (numériques non négatifs, max_retries entier), à la construction.
- Délai :
Retry-After / Retry-After-Ms honoré (429), sinon backoff exponentiel avec doublement, plafond 8 s et jitter.
- Pas de budget temps global : le pire cas reste borné par
max_retries et le plafond de délai.
- Rejeu du POST à l'identique : sûr, l'évaluation est sans état (quota consommé : coût assumé).
- Retentatives totalement silencieuses en v1 : pas de log, pas de callback.
🧪 Seam de test
- Seam retenu : l'API publique
Typesafe::Client#evaluate / Typesafe::Jev#evaluate, avec WebMock sur l'endpoint et sleep stubbé — le seam existant de spec/typesafe/client_spec.rb, le plus haut et déjà en place.
🚫 Hors périmètre
- Observabilité (callback, logger, métriques) — rejeté pour v1, réintroduisable sans casser l'API.
- Override de la politique par appel dans
evaluate — la signature ne change pas.
- Budget temps global —
max_retries + plafond bornent déjà le pire cas.
- Stratégie de retry pluggable — supplantée par le Hash
retry_options: à clés fixes.
- Retentative des erreurs non retentables (400/401/403/404/422) — jamais, par construction.
📚 Références
- Spec :
docs/plans/retry-client/spec.md
- Affinage :
docs/plans/retry-client/affinage.md
- ADR :
docs/adr/0001-retentatives-automatiques-par-defaut.md
- Glossaire :
LEXIQUE.md
📎 Affinage — session complète
Affinage — Retry des requêtes HTTP dans le client
- Date : 2026-09-20 · Sujet : ajouter une fonctionnalité de retry au client HTTP du gem · Mode : produit
- Sources : .agents/dev-workflow.md, AGENTS.md (contrat API), lib/typesafe/client.rb, lib/typesafe/errors.rb, lib/typesafe/jev.rb
Round 1
Posé : Q1 — Périmètre des erreurs retentées · Q2 — Erreurs réseau · Q3 — Surface de configuration · Q4 — Stratégie de backoff · Q5 — Nombre de retries par défaut
- Décision (user) Q1 : 429 + 529 + 5xx (tout ce que
retryable? considère) — AGENTS.md sera mis à jour en conséquence (le contrat « 429/529 seulement » est supplanté).
- Décision (user) Q2 : oui — encapsuler dans une
Typesafe::ConnectionError < APIError retentable.
- Décision (user) Q3 : options sur
Client.new uniquement (héritées par Jev), pas d'override par appel.
- Décision (user) Q4 : honorer
Retry-After si présent, sinon backoff exponentiel 0,5 s → 8 s (doublement, plafond) + jitter.
- Décision (user) Q5 : 2 retries par défaut (3 tentatives au total), opt-out via configuration.
Réglé jusqu'ici
| Q |
Décision |
| Q1 |
429 + 529 + 5xx |
| Q2 |
ConnectionError, retentable |
| Q3 |
Options sur Client.new uniquement |
| Q4 |
Retry-After sinon expo 0,5 s→8 s + jitter |
| Q5 |
2 retries par défaut |
Round 2
Posé : Q6 — Réglage des délais · Q7 — Observabilité · Q8 — Budget temps global · Q9 — ADR
- Décision (user) Q6 : supplantée par l'amendement du user (voir ci-dessous) — les options de délai vivent dans
retry_options:.
- Décision (user) Q7 : totalement silencieux en v1 — pas de callback ni de log.
- Décision (user) Q8 : pas de budget temps global —
max_retries + plafond de délai bornent déjà le pire cas.
- Décision (user) Q9 : ADR-0001 écrite (
docs/adr/0001-retentatives-automatiques-par-defaut.md, accepté) + AGENTS.md mis à jour.
Amendement (user) — surface de configuration
- Décision (user) : remplacer les kwargs plats (
max_retries:, …) par un unique argument retry_options: (Hash) regroupant toutes les options de retry, sur Client.new, hérité par Jev. Q3 est amendée en conséquence ; les détails (clés exactes, validation des clés inconnues) sont reposés en Round 3.
Réglé jusqu'ici
| Q |
Décision |
| Q1 |
429 + 529 + 5xx |
| Q2 |
ConnectionError, retentable |
| Q3 |
kwargs plats → retry_options: (Hash unique) sur Client.new (amendé) |
| Q4 |
Retry-After sinon expo 0,5 s→8 s + jitter |
| Q5 |
2 retries par défaut |
| Q7 |
Silencieux, pas de hook v1 |
| Q8 |
Pas de budget global |
| Q9 |
ADR-0001 + AGENTS.md mis à jour |
Round 3
Posé : Q10 — Clés de retry_options: · Q11 — Clés inconnues
- Décision (user) Q10 :
{ max_retries:, base_delay:, max_delay: } — clés absentes/nil → valeurs par défaut (2 / 0,5 / 8,0) ; retry_options: nil = tous les défauts ; valeurs validées (numériques positifs, Integer pour max_retries) et hash normalisé figé sur le client.
- Décision (user) Q11 :
ArgumentError sur toute clé inconnue (typo = échec bruyant à la construction du client).
Réglé jusqu'ici
| Q |
Décision |
| Q10 |
max_retries / base_delay / max_delay, défauts 2 / 0,5 / 8,0 |
| Q11 |
ArgumentError sur clé inconnue |
- Session confirmée le 2026-09-20
- Passage advisor proposé (sujets : retry actif par défaut, silence des retries en v1) — refusé par le user, les décisions tiennent telles quelles.
Délibérément non posé
- Formule de jitter exacte (full vs equal jitter, amplitude) — détail d'implémentation, politique figée en v1.
- Liste précise des exceptions réseau enveloppées par ConnectionError — détail d'implémentation pour /cadrage.
title: "retry-client: retentatives automatiques dans le client HTTP"
labels: ["Epic"]
🎯 Problème
Chaque échec transitoire (429, 529, 5xx, timeout réseau) remonte immédiatement à l'appelant de
Typesafe::Client#evaluate: chaque utilisateur du gem doit réécrire à la main la boucle « attendre puis rejouer », et la plupart ne le font pas. Leurs intégrations restent fragiles aux salves de surcharge, faute de résilience zéro-config.💡 Solution
Le client retente automatiquement les erreurs retentables (429, 529, 5xx) et les erreurs de connexion (
Typesafe::ConnectionError), par défaut 2 retries. Le délai suitRetry-Afterquand le serveur l'impose, sinon un backoff exponentiel 0,5 s → 8 s avec jitter. Toute la politique se règle via un unique Hashretry_options:surClient.new;retry_options: { max_retries: 0 }retrouve le comportement actuel.🧭 Décisions arrêtées
retry_options: { max_retries: 0 }(ADR-0001, qui écarte l'ancien contrat « retry 429/529 manuel »).APIError#retryable?: 429, 529, 5xx) plus les erreurs de connexion.Typesafe::ConnectionError < APIErrorencapsulant les échecs réseau (connexion refusée, DNS, timeout, reset), marquée retentable.retry_options:(Hash) surClient.new, hérité parTypesafe::Jev; pas d'override par appel.retry_options::max_retries/base_delay/max_delay, défauts 2 / 0,5 / 8,0 ;nil= défauts ; Hash normalisé figé sur le client.ArgumentErrorsur clé inconnue ou valeur invalide (numériques non négatifs,max_retriesentier), à la construction.Retry-After/Retry-After-Mshonoré (429), sinon backoff exponentiel avec doublement, plafond 8 s et jitter.max_retrieset le plafond de délai.🧪 Seam de test
Typesafe::Client#evaluate/Typesafe::Jev#evaluate, avec WebMock sur l'endpoint etsleepstubbé — le seam existant despec/typesafe/client_spec.rb, le plus haut et déjà en place.🚫 Hors périmètre
evaluate— la signature ne change pas.max_retries+ plafond bornent déjà le pire cas.retry_options:à clés fixes.📚 Références
docs/plans/retry-client/spec.mddocs/plans/retry-client/affinage.mddocs/adr/0001-retentatives-automatiques-par-defaut.mdLEXIQUE.md📎 Affinage — session complète
Affinage — Retry des requêtes HTTP dans le client
Round 1
Posé : Q1 — Périmètre des erreurs retentées · Q2 — Erreurs réseau · Q3 — Surface de configuration · Q4 — Stratégie de backoff · Q5 — Nombre de retries par défaut
retryable?considère) — AGENTS.md sera mis à jour en conséquence (le contrat « 429/529 seulement » est supplanté).Typesafe::ConnectionError < APIErrorretentable.Client.newuniquement (héritées parJev), pas d'override par appel.Retry-Aftersi présent, sinon backoff exponentiel 0,5 s → 8 s (doublement, plafond) + jitter.Réglé jusqu'ici
Client.newuniquementRound 2
Posé : Q6 — Réglage des délais · Q7 — Observabilité · Q8 — Budget temps global · Q9 — ADR
retry_options:.max_retries+ plafond de délai bornent déjà le pire cas.docs/adr/0001-retentatives-automatiques-par-defaut.md, accepté) + AGENTS.md mis à jour.Amendement (user) — surface de configuration
max_retries:, …) par un unique argumentretry_options:(Hash) regroupant toutes les options de retry, surClient.new, hérité parJev. Q3 est amendée en conséquence ; les détails (clés exactes, validation des clés inconnues) sont reposés en Round 3.Réglé jusqu'ici
kwargs plats→retry_options:(Hash unique) surClient.new(amendé)Round 3
Posé : Q10 — Clés de
retry_options:· Q11 — Clés inconnues{ max_retries:, base_delay:, max_delay: }— clés absentes/nil → valeurs par défaut (2 / 0,5 / 8,0) ;retry_options: nil= tous les défauts ; valeurs validées (numériques positifs, Integer pour max_retries) et hash normalisé figé sur le client.ArgumentErrorsur toute clé inconnue (typo = échec bruyant à la construction du client).Réglé jusqu'ici
Délibérément non posé