Gérer les erreurs et réessayer
Les exceptions du SDK
Quand l'API répond par une erreur, google-genai lève une exception de google.genai.errors :
| Exception | Codes HTTP | Exemples |
|---|---|---|
errors.ClientError | 4xx : votre requête pose problème | 400 clé invalide, 404 modèle inconnu, 429 quota |
errors.ServerError | 5xx : le service a un souci | 500 erreur interne, 503 surcharge, 504 délai dépassé |
errors.APIError | classe mère des deux | attribut e.code (le code HTTP) |
flowchart TD
A["Appel"] --> E{"Exception ?"}
E -->|"non"| OK["Résultat"]
E -->|"code 429, 500, 502, 503, 504"| T{"Essais restants ?"}
T -->|"oui"| W["Attendre 5 s, 10 s, 20 s..."] --> A
T -->|"non"| R["Relancer l'exception"]
E -->|"autre code (400, 403, 404)"| R
Seul 429 parmi les 4xx est temporaire : le quota se reconstitue avec le temps. Tous les autres 4xx demandent une correction (clé, nom de modèle, format de requête).
La fonction de réessai
def avec_reessais(appel, essais=4, attente=5):
"""Réessaie sur 429 (quota) et 5xx (serveur), avec attente exponentielle."""
for essai in range(essais):
try:
return appel()
except errors.APIError as e:
if e.code not in (429, 500, 502, 503, 504) or essai == essais - 1:
raise
pause = attente * 2 ** essai
print(f"[erreur {e.code}, nouvel essai dans {pause} s]", file=sys.stderr)
time.sleep(pause)On lui passe une fonction sans argument (lambda: client.models.count_tokens(...)) : la même logique protège tous les appels du CLI. L'attente exponentielle (5, 10, 20 s) laisse au service le temps de récupérer, au lieu de le marteler.
Le délai maximal
Sans délai, une requête qui ne répond jamais bloque le programme indéfiniment. Le client du lab fixe une limite :
def make_client() -> genai.Client:
"""Le même code sert les deux modes : le SDK lit les variables d'environnement.
Le délai (en millisecondes) évite qu'une requête bloquée fige le programme."""
return genai.Client(http_options=types.HttpOptions(timeout=90_000))Quelles erreurs avec_reessais réessaie-t-elle ?
429 et 503 sont temporaires. 404 (modèle inconnu) et 400 (clé ou requête invalide) se reproduiraient à l'identique : il faut corriger, pas attendre.
Pourquoi l'attente double-t-elle à chaque essai ?
Réessayer immédiatement après un 429 ou un 503 a toutes les chances d'échouer encore et ajoute de la charge. Doubler l'attente espace les tentatives.
Résultat obtenu
Les tests sans réseau simulent les trois situations (extrait de pytest -v, 2026-10-15) :
test_reunion.py::test_reessais_sur_503_puis_succes PASSED
test_reunion.py::test_pas_de_reessai_sur_cle_invalide PASSED
test_reunion.py::test_abandon_apres_n_essais PASSEDEt deux vraies erreurs provoquées volontairement en mode clé d'API, pour reconnaître les messages :
400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'API key not valid. Please pass a valid API key.', ...}}
404 NOT_FOUND. {'error': {'code': 404, 'message': 'models/gemini-modele-inexistant is not found for API version v1beta, ...'}}Exercice
Écrivez le test qui vérifie que avec_reessais attend bien 5 puis 10 secondes avant de réussir au troisième essai, sans vraiment attendre.
Voir la solution
def test_attentes_exponentielles(monkeypatch):
pauses = []
monkeypatch.setattr(reunion.time, "sleep", pauses.append) # enregistre au lieu d'attendre
panne = errors.ServerError(503, {"error": {"message": "x", "status": "UNAVAILABLE"}})
client = FakeClient(panne, panne, NS(total_tokens=1))
assert reunion.tokens(client, "x") == 1
assert pauses == [5, 10]monkeypatch.setattr remplace time.sleep par pauses.append le temps du test : chaque « attente » ajoute sa durée à la liste. On vérifie ainsi le calcul sans perdre 15 secondes.
Je sais faire
- Distinguer
ClientErroretServerErroret liree.code - Réessayer seulement les erreurs temporaires, avec attente exponentielle et nombre d'essais maximal
- Fixer un délai maximal avec
HttpOptions(timeout=...) - Tester une logique de réessai sans réseau ni attente réelle