درس 5 — Fiabiliser et sécuriser

Erreurs, timeouts et réessais

5 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Erreurs, timeouts et réessais

Trois pannes, trois traitements

flowchart TB
  E["Quelque chose échoue"] --> Q{"Où ?"}
  Q -- "API RH répond 4xx" --> R1["Renvoyer {erreur, code_http} au modèle"]
  Q -- "API RH muette" --> R2["Timeout httpx, puis {erreur} au modèle"]
  Q -- "Modèle 429 / 503" --> R3["Réessai avec attente croissante"]
  R1 --> M["Le modèle explique à l'utilisateur"]
  R2 --> M
  R3 --> G["generate_content relancé (4 essais max)"]

Le principe pour les outils : ne jamais lever d'exception vers la boucle. Une exception casse la conversation ; un dictionnaire {"erreur": ...} est un résultat comme un autre que le modèle sait reformuler (« vous n'avez pas accès à ce solde »).

Côté API métier : un seul point d'appel HTTP

Tous les outils passent par _appel, qui transforme chaque panne en dictionnaire :

files/outils_rh.py (extrait)
# Timeout explicite : sans lui, un appel bloqué fige tout l'assistant.
http = httpx.Client(base_url=API_URL, timeout=httpx.Timeout(5.0, connect=2.0))

def _appel(utilisateur: str, methode: str, chemin: str, **kw) -> dict:
    try:
        r = http.request(methode, chemin, headers={"X-Utilisateur": utilisateur}, **kw)
    except httpx.TimeoutException:
        return {"erreur": "L'API RH n'a pas répondu à temps, réessayez plus tard."}
    except httpx.HTTPError as e:
        return {"erreur": f"API RH injoignable ({type(e).__name__})."}
    if r.status_code >= 400:
        try:
            detail = r.json().get("detail")
        except ValueError:
            detail = r.text[:200]
        return {"erreur": detail, "code_http": r.status_code}
    return r.json()
CodeSignification dans l'API RHCe que le modèle doit en faire
403l'utilisateur n'est ni l'employé ni son managerexpliquer le refus, ne pas réessayer
404employé ou demande inconnuproposer de chercher par nom
409solde insuffisant, demande déjà annuléedonner le solde disponible
422données invalides (dates inversées…)reformuler la demande
timeoutl'API n'a pas répondu en 5 sdire de réessayer plus tard

Côté modèle : réessayer intelligemment

Les erreurs 429 (quota dépassé, trop de requêtes) et 503 (service surchargé) sont temporaires. On réessaie avec une attente qui double (backoff exponentiel). Les autres erreurs (400 : requête mal formée, 403 : clé invalide) ne guériront pas : on les remonte tout de suite.

files/assistant.py (extrait)
def generer(client, contents, config, essais=4):
    for i in range(essais):
        try:
            return client.models.generate_content(model=MODEL, contents=contents, config=config)
        except errors.APIError as e:
            if e.code not in (429, 500, 503) or i == essais - 1:
                raise
            time.sleep(2 ** i * 2)  # 2 s, 4 s, 8 s

L'API RH répond 403 à consulter_solde. Que doit renvoyer l'outil à la boucle ?

Quelles erreurs de l'API Gemini justifient un réessai automatique ?

Exercice : tester le timeout sans réseau

Écrivez un test pytest qui vérifie qu'un timeout de l'API devient un message lisible, sans lancer l'API ni attendre 5 secondes.

Voir la solution

httpx.MockTransport remplace le réseau par une fonction ; on lui fait lever un ReadTimeout :

files/test_assistant.py (extrait)
def test_timeout_devient_message(monkeypatch):
    def lent(request):
        raise httpx.ReadTimeout("trop lent", request=request)
    monkeypatch.setattr(outils_rh, "http", httpx.Client(base_url="http://x",
                                                        transport=httpx.MockTransport(lent)))
    assert "pas répondu à temps" in outils_rh.consulter_solde("E001", "E001")["erreur"]

Le test s'exécute en quelques millisecondes et ne dépend d'aucun service.

Résultat obtenu

python e2e.py, scénario 5, exécuté le 2026-10-15 avec gemini-3.5-flash. E004 (comptabilité) demande le solde d'une employée du marketing : l'API répond 403, le modèle l'explique.

[E004] Quel est le solde de Lucie Moreau ?   (mode AUTO)
  [tour 1] 1 appel(s) : chercher_employe({"nom": "Lucie Moreau"})
  [tour 2] 1 appel(s) : consulter_solde({"employe_id": "E003"})
  → L'accès au solde de Lucie Moreau vous est refusé car vous n'êtes ni l'employée concernée ni son manager.

Et la ligne correspondante du journal d'audit :

{"ts": "2026-10-15T14:35:26+00:00", "utilisateur": "E004", "outil": "consulter_solde", "args": {"employe_id": "E003"}, "statut": "erreur", "duree_ms": 3, "resultat": {"erreur": "Accès refusé : vous n'êtes ni l'employé ni son manager", "code_http": 403}}

Je sais faire

  • Fixer un timeout explicite sur un client HTTP
  • Transformer toute panne d'outil en dictionnaire {"erreur": ...}
  • Réessayer les 429/503 avec un backoff exponentiel, et seulement eux
  • Expliquer pourquoi on ne réessaie pas automatiquement une écriture
التالي