Tom Daluzeau
← Parcours Python · système
Python · système · leçon 6 / 8Ch. 5 · API REST et requests12 minTerminée

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 :

RequestException attrape les trois Timeout ConnectionError HTTPError ReadTimeout fille de Timeout levées par requests.get aucune réponse, donc pas de r levée par raise_for_status() réponse arrivée, en 4xx ou 5xx
Un parent, trois filles. Un except sur le parent attrape toutes ses filles, et leurs propres filles.

Deux choses à remarquer :

  • Timeout et ConnectionError sont levées par requests.get lui-même : il n’y a pas de réponse, donc pas de r.
  • HTTPError n’est levée que par r.raise_for_status(). Sans cet appel, un 500 passe 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.ConnectionError n’est pas le ConnectionError livré avec Python : ce sont deux classes différentes qui portent le même nom. Écris toujours le préfixe requests..

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.

bon ordre : du précis au général except requests.Timeout except requests.ConnectionError except requests.HTTPError except requests.RequestException Timeout → « trop lent » parent en premier except requests.RequestException except requests.Timeout except requests.ConnectionError except requests.HTTPError Timeout → « erreur requests » Python teste de haut en bas et n'exécute que le premier except qui correspond
À droite, le parent passe devant tout le monde : les branches précises ne servent plus jamais.

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 :

  1. serveur injoignable, trop lent pour le timeout, ou code d’erreur : une fille de RequestException est levée, la fonction renvoie down ;
  2. sinon, une réponse correcte est arrivée : on regarde le temps qu’elle a mis ;
  3. au-dessus du seuil, degraded ; sinon ok.

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
0,2 s 2 s 10 s : rien n'arrive avant 3 s ok degraded down : Timeout 0 s 1 s seuil 3 s timeout=3 la réponse n'est mesurée que si elle arrive avant le timeout
Le seuil sépare ok de degraded ; le timeout sépare une réponse lente d'une absence de réponse.

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, requests peut attendre sans fin.
  • Timeout : pas de réponse dans le délai. ConnectionError : connexion impossible. HTTPError : code 4xx ou 5xx, levée seulement par raise_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.

Cartes · 7

Réviser les cartes ici

Espace : retourner