requests : timeout, exceptions et statut ok / degraded / down
Mettre un timeout à chaque appel, reconnaître Timeout, ConnectionError, HTTPError et leur parent RequestException, ranger les except dans le bon ordre et classer un serveur en ok, degraded ou down.
Mis à jour le
À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.
Pourquoi on voit ça
Un outil de surveillance tourne quand ça va mal. S’il plante sur le premier serveur injoignable, ou s’il reste bloqué devant un serveur muet, il ne sert à rien : les serveurs suivants ne sont jamais vérifiés.
Dans cette leçon, tu apprends à borner l’attente (timeout=), à reconnaître les exceptions de requests, à les attraper dans le bon ordre, et à résumer l’état d’un serveur en un mot : ok, degraded ou down.
Toujours mettre timeout=
Par défaut, requests n’a aucune limite d’attente. Si un serveur accepte la connexion puis ne répond jamais, cette ligne ne se termine pas :
r = requests.get("http://web-3.local/health")
Le programme ne plante pas : il est gelé. Pour un outil qui doit parcourir tout un parc, c’est pire qu’une erreur.
timeout= fixe le délai d’attente, en secondes. Passé ce délai, requests abandonne et lève l’exception requests.Timeout.
r = requests.get("http://web-3.local/health", timeout=3)
requests.exceptions.ReadTimeout: HTTPConnectionPool(host='web-3.local', port=80): Read timed out. (read timeout=3)
Le traceback affiche ReadTimeout : c’est une fille de Timeout, donc except requests.Timeout l’attrape.
Règle simple : chaque appel à requests porte un timeout=. Il n’y a pas d’exception à cette règle.
Les exceptions de requests
Trois exceptions à connaître, et leur parent commun.
| Exception | Quand | État du serveur |
|---|---|---|
requests.Timeout |
pas de réponse dans le délai fixé par timeout= |
injoignable (ou beaucoup trop lent) |
requests.ConnectionError |
impossible d’établir la connexion : nom inconnu, port fermé, machine éteinte | injoignable |
requests.HTTPError |
levée par r.raise_for_status() sur un code 4xx ou 5xx |
en panne |
requests.RequestException |
le parent des trois | — |
Leur arbre généalogique :
Deux choses à remarquer :
TimeoutetConnectionErrorsont levées parrequests.getlui-même : il n’y a pas de réponse, donc pas der.HTTPErrorn’est levée que parr.raise_for_status(). Sans cet appel, un500passe inaperçu.
Comme dans le chapitre sur les exceptions, un except attrape son type et tous ses enfants. except requests.RequestException attrape donc les trois.
Contexte, pas à apprendre.
requests.ConnectionErrorn’est pas leConnectionErrorlivré avec Python : ce sont deux classes différentes qui portent le même nom. Écris toujours le préfixerequests..
L’ordre des except : du plus précis au plus général
Python essaie les except de haut en bas, s’arrête au premier qui correspond, et n’en exécute qu’un seul. Le parent doit donc être placé en dernier.
import requests
try:
r = requests.get("http://web-3.local/health", timeout=3)
r.raise_for_status()
print("sain")
except requests.Timeout:
print("trop lent")
except requests.ConnectionError:
print("injoignable")
except requests.HTTPError:
print("en panne :", r.status_code)
except requests.RequestException:
print("autre erreur requests")
Si tu mets RequestException en premier, il attrape tout, et les branches suivantes ne servent jamais :
try:
raise requests.Timeout()
except requests.RequestException:
print("erreur requests")
except requests.Timeout:
print("trop lent")
erreur requests
Python ne signale pas cette erreur d’ordre. Le programme tourne, mais avec le mauvais message.
Timeout, ConnectionError et HTTPError sont trois sœurs : l’une n’attrape pas les autres. Si tu n’attrapes que Timeout et qu’une ConnectionError arrive, le programme s’arrête sur un traceback.
Classer un serveur : ok, degraded ou down
L’outil résume chaque serveur en un mot.
| Statut | Sens |
|---|---|
ok |
il répond 200, et vite |
degraded |
il répond 200, mais trop lentement |
down |
il répond par une erreur, ou il ne répond pas |
degraded (dégradé) est un état intermédiaire : le service fonctionne encore, mais sa lenteur annonce souvent une panne. On le repère avec r.elapsed.total_seconds() et un seuil.
import requests
SEUIL = 1.0
def check(url):
try:
r = requests.get(url, timeout=3)
r.raise_for_status()
except requests.RequestException:
return "down"
if r.elapsed.total_seconds() > SEUIL:
return "degraded"
return "ok"
Lis-la dans l’ordre :
- serveur injoignable, trop lent pour le timeout, ou code d’erreur : une fille de
RequestExceptionest levée, la fonction renvoiedown; - sinon, une réponse correcte est arrivée : on regarde le temps qu’elle a mis ;
- au-dessus du seuil,
degraded; sinonok.
Ne confonds pas les deux délais. Ici le seuil vaut 1 seconde et le timeout 3 secondes :
Temps de réponse (code 200) |
Ce qui se passe | Statut |
|---|---|---|
| 0,2 s | réponse rapide | ok |
| 2 s | réponse arrivée, mais au-dessus du seuil | degraded |
| 10 s | Timeout levé au bout de 3 s, aucune réponse |
down |
Et comme check ne lève jamais d’exception, la boucle sur le parc va jusqu’au bout :
for url in ["http://web-1.local/health", "http://web-2.local/health"]:
print(url, check(url))
http://web-1.local/health ok
http://web-2.local/health down
À retenir
- Toujours
timeout=: sans lui,requestspeut attendre sans fin. Timeout: pas de réponse dans le délai.ConnectionError: connexion impossible.HTTPError: code4xxou5xx, levée seulement parraise_for_status().- Les trois sont filles de
requests.RequestException. - Ordre des
except: du plus précis au plus général. Le parent en dernier, sinon il attrape tout. ok: répond bien et vite.degraded: répond bien, mais au-dessus du seuil.down: erreur ou pas de réponse.
Entraînement
12 questions, dont 4 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.
Ta note
Cartes · 7
Réviser les cartes ici
Pourquoi