Corso Python / Richieste HTTP e API: risposte JSON ed errori

Un’API HTTP riceve una richiesta e restituisce uno stato e dati di risposta. Un client Python deve controllare separatamente esito della richiesta, attese di rete, dimensione della risposta e struttura del JSON: uno stato riuscito non rende automaticamente validi gli importi ricevuti.

Separare protocollo, indirizzo e formato dei dati

HTTP è il protocollo di richiesta e risposta; URL è l’indirizzo della risorsa; JSON è uno dei possibili formati del corpo. API significa interfaccia di programmazione: l’interfaccia di un servizio specifica anche metodi, campi, errori e condizioni di utilizzo, oltre al semplice indirizzo.

Per una risorsa esterna segui le indicazioni del servizio su HTTPS, autenticazione e limiti di richiesta. Qui usiamo invece un server locale sotto il tuo controllo per rendere riproducibili i dati e gli errori. Non è una configurazione di un server di produzione.

La documentazione ufficiale di urllib.request descrive apertura delle risorse, risposta e attese. La distinzione tra parsing e controllo dei dati è spiegata anche nella guida al JSON.

Preparare una risposta e un server locale

Crea una cartella di esercizio nuova contenente soltanto file dimostrativi. Salva expenses.json, in UTF-8 senza BOM, con questo contenuto completo:

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

Nel primo terminale entra in quella cartella, seleziona l’interprete del progetto e avvia:

python -m http.server 8000 --bind 127.0.0.1

Lascia aperto questo terminale mentre esegui il client nell’altro. Il server risponde soltanto sulla rete locale interna al computer, all’indirizzo 127.0.0.1. Se la porta 8000 è già occupata, scegli una porta libera e cambia lo stesso numero nell’URL passato al client; non interrompere un altro servizio solo per liberarla.

Il server offre i file della cartella in cui lo avvii, quindi non partire da una cartella con credenziali o documenti personali. La documentazione di http.server ne descrive l’uso per prove e i limiti rispetto a una distribuzione di produzione.

Eseguire il client completo e validare la risposta

Salva il programma completo come fetch_expenses.py, in UTF-8, nella cartella dell’esercizio. Usa lo stesso interprete del progetto; il client richiede solo la libreria standard.

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)

Nel secondo terminale, dalla cartella del client, esegui:

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

L’output normale deve essere records: 2 e poi total: 11500. Al termine delle prove torna nel terminale del server e fermalo con Ctrl+C. Su macOS o Ubuntu fuori dall’ambiente virtuale usa python3 se è il comando dell’interprete selezionato.

Il programma riceve l’URL come primo argomento: ometterlo provoca un errore di accesso agli argomenti, perché questa versione didattica non include ancora un’interfaccia argparse. Usa il comando completo indicato. Con un URL HTTP, urlopen esegue qui una lettura della risorsa; il blocco with chiude la risposta quando esci.

La lettura richiede al massimo 1.000.001 byte, uno in più del limite scelto. Se supera 1.000.000 byte, il client interrompe prima del parsing. Altrimenti decodifica UTF-8 e interpreta JSON; poi richiede una lista di dizionari con amount esattamente intero e non negativo, escludendo i booleani.

La validazione riguarda questi campi e non l’intera identità dei record. Non controlla unicità di id né contenuto di name e accetta una lista vuota con totale zero. Due record con lo stesso id vengono contati entrambi. Se il contratto richiede unicità o altri campi obbligatori, aggiungi regole specifiche prima di usare i dati.

Distinguere errori di rete, HTTP e dati

HTTPError indica una risposta HTTP d’errore, come un file inesistente. URLError può rappresentare un problema di connessione o risoluzione del nome. Una risposta ricevuta può invece fallire per stato inatteso, limite di dimensione, UTF-8 non valido, JSON malformato o record non accettato. Il programma non sostituisce queste condizioni con una lista che sembra riuscita.

timeout=5 limita attese di operazioni bloccanti di rete; non promette una durata totale del lavoro inferiore a cinque secondi. Inoltre il gruppo di eccezioni gestito non copre ogni possibile guasto del protocollo: per esempio una chiusura remota prima della risposta può emergere come RemoteDisconnected con traceback. Per un client operativo definisci i casi di errore da trattare sulla base delle interfacce effettive, senza fingere che qualsiasi errore sia già gestito dall’esempio.

  1. Con il server attivo chiedi un nome file inesistente nell’URL: deve apparire request failed: HTTPError e non il totale.
  2. Ripristina l’URL e cambia nel JSON soltanto amount da numero a stringa, per esempio "3500". La richiesta riesce ma i dati devono essere rifiutati con invalid record.
  3. Ripristina il JSON originale e controlla di nuovo i due record e il totale 11500.
  4. Ferma il server con Ctrl+C e ripeti la richiesta: verifica il fallimento della connessione, distinto dalla risposta valida.

I messaggi di errore vengono scritti da SystemExit sul canale di errore standard e il processo termina con stato non zero. Se vuoi automatizzarne l’uso, controlla anche lo stato di uscita, come descritto nella lezione su logging e diagnosi, invece di cercare soltanto una parola nell’output.

Per i tentativi successivi stabilisci numero massimo, attese e condizioni di ripetizione. Considera l’idempotenza, cioè se ripetere la stessa richiesta può produrre ulteriori effetti: una lettura dimostrativa e una scrittura su un servizio non richiedono necessariamente la stessa politica. Conserva i token di autenticazione secondo le regole del servizio, senza inserirli in codice pubblico, URL o messaggi diagnostici.