Cours de Python / Lire une API HTTP et valider sa réponse JSON

Une API HTTP reçoit une requête et renvoie un statut ainsi que des données. Un client Python doit vérifier séparément la réponse HTTP, les limites d’attente, la taille du contenu et la structure du JSON décodé.

Séparer protocole, adresse et données

Une API, ou application programming interface, définit une manière d’interagir avec un programme. HTTP signifie Hypertext Transfer Protocol et organise les requêtes et réponses ; l’URL identifie ici la ressource demandée. JSON est un format de données possible pour le corps de la réponse. Un statut 200 ne garantit donc pas que le contenu est utilisable par votre application.

Nous utilisons un serveur local dont vous contrôlez le fichier pour reproduire les réussites et les erreurs. Ce serveur de fichiers sert uniquement à l’exercice, pas à déployer une application en production. La documentation de http.server décrit sa portée.

Le client utilisera urllib.request de la bibliothèque standard, sans paquet supplémentaire. Il enverra une requête de lecture et interprétera une liste de dépenses. Les règles de lecture et validation du JSON s’appliquent encore après la communication réseau.

Préparer la réponse locale et lancer le serveur

Créez un dossier d’exercice ne contenant aucune donnée confidentielle. Enregistrez-y expenses.json en UTF-8, avec exactement ce contenu :

[{"id":"e1","name":"coffee","amount":3500},{"id":"e2","name":"lunch","amount":8000}]

Ouvrez un premier terminal dans ce dossier et sélectionnez l’environnement virtuel du projet. Lancez :

python -m http.server 8000 --bind 127.0.0.1

Le terminal reste occupé par le serveur et affiche son adresse d’écoute. L’option --bind limite l’écoute à 127.0.0.1, l’adresse de boucle locale du même ordinateur. Le serveur peut servir les fichiers du dossier : gardez ce périmètre limité aux données d’essai. Si le port 8000 est déjà utilisé, choisissez par exemple 8001 dans la commande et dans l’URL du client.

Conservez ce terminal ouvert. Dans un deuxième terminal, placez-vous dans le même dossier et utilisez le même environnement Python. Sous macOS ou Ubuntu hors environnement virtuel, remplacez python par python3 dans les commandes si c’est votre nom d’interpréteur.

Exécuter le client complet et lire les contrôles

Enregistrez le programme suivant en UTF-8 dans fetch_expenses.py :

import json
import sys
from urllib.request import urlopen
from urllib.error import HTTPError, URLError

url = sys.argv[1]
try:
    with urlopen(url, timeout=5) as response:
        if response.status != 200:
            raise ValueError("unexpected status")
        raw = response.read(1_000_001)
        if len(raw) > 1_000_000:
            raise ValueError("response too large")
        data = json.loads(raw.decode("utf-8"))
except (HTTPError, URLError, TimeoutError, UnicodeError, ValueError) as error:
    raise SystemExit(f"request failed: {type(error).__name__}")
if not isinstance(data, list):
    raise SystemExit("expected a list")
total = 0
for row in data:
    if not isinstance(row, dict) or type(row.get("amount")) is not int or row["amount"] < 0:
        raise SystemExit("invalid record")
    total += row["amount"]
print("records:", len(data))
print("total:", total)

Dans le deuxième terminal, fournissez obligatoirement l’URL en argument :

python fetch_expenses.py http://127.0.0.1:8000/expenses.json

La sortie normale comporte deux lignes : records: 2 puis total: 11500. Le processus se termine avec le code zéro. Si vous avez choisi un autre port, utilisez ce même numéro dans l’URL. Sans argument URL, ce petit script n’affiche pas une aide personnalisée : il échoue en accédant à sys.argv[1].

Le bloc with ferme la réponse après lecture. Le client exige le statut 200, lit au plus 1 000 001 octets et rejette le contenu si sa taille dépasse 1 000 000 octets. Le dernier octet sert à détecter le dépassement ; le programme ne doit pas accepter silencieusement une réponse tronquée comme si elle était complète.

Le décodage UTF-8 et l’analyse JSON peuvent aussi échouer. Une fois le JSON chargé, le programme exige une liste puis, pour chaque ligne, un dictionnaire dont amount est exactement un int positif ou nul. Une chaîne "3500", un booléen True ou un montant négatif sont refusés. Le total n’est affiché qu’après validation de toutes les lignes.

Les champs id et name de l’échantillon illustrent une dépense, mais le client ne les vérifie pas et ne détecte pas les doublons. La validation porte ici uniquement sur la structure et les montants nécessaires à la somme. La référence de urlopen précise le comportement de la réponse et du paramètre timeout.

Distinguer les erreurs et restaurer le résultat normal

  1. Dans la commande client, demandez missing.json à la place de expenses.json. Le serveur répond avec une erreur HTTP ; le client se termine en échec avec request failed: HTTPError. Rétablissez le chemin.
  2. Dans expenses.json, remplacez seulement 3500 par "3500", avec guillemets. Le réseau et le JSON fonctionnent, mais le client signale invalid record. Rétablissez le nombre.
  3. Essayez temporairement un objet JSON vide {} à la place de la liste : le message devient expected a list. Restaurez ensuite le fichier complet.
  4. Relancez avec les données initiales et vérifiez records: 2 et total: 11500. Arrêtez ensuite le serveur dans le premier terminal avec Ctrl+C, puis relancez le client : la connexion doit échouer au lieu de produire un total normal.

HTTPError indique ici une réponse HTTP d’erreur. URLError peut représenter un échec de connexion ou de résolution de nom ; HTTPError est elle-même une sous-classe de URLError. Une réponse reçue peut néanmoins échouer lors du décodage, du contrôle de taille ou de la validation. La documentation des exceptions urllib explique cette hiérarchie.

Un timeout de cinq secondes limite certaines attentes réseau bloquantes ; il ne garantit pas une durée totale maximale de cinq secondes pour tout le traitement. Des erreurs de protocole ou de lecture autres que celles listées dans except peuvent aussi remonter directement : ce client pédagogique ne normalise pas tous les échecs possibles. En cas de problème, conservez sa nature plutôt que de substituer une liste vide donnant l’apparence d’un succès.

Pour un service externe, respectez son URL HTTPS, son authentification, ses quotas et le schéma documenté. Une politique de reprise doit limiter le nombre de tentatives et les délais et tenir compte de l’idempotence, c’est-à-dire de l’effet d’une répétition de la même opération. Ne répétez pas une requête d’écriture sans comprendre le risque de doublon. Les jetons d’authentification n’ont pas leur place dans le code d’exemple, une URL partagée ou les journaux.