Uno stallo a metà corpo aggira un comune gestore di timeout di requests

Ultimo aggiornamento il August 17, 2026
Uno stallo a metà corpo aggira un comune gestore di timeout di requests
Riepilogo AI
Stai cercando bug nel codice esistente su requests? Controlla le assunzioni sui redirect, i gestori che catturano solo requests.exceptions.Timeout ma pensano di coprire anche uno stallo a metà body, e i consumer di .text che ricevono risposte senza charset. Stai migrando in modo meccanico verso httpx? Cambia i namespace delle eccezioni in httpx.TimeoutException o nelle classi di fase più specifiche, decidi se abilitare follow_redirects e ritesta le assunzioni sul decoding. Il gestore requests già oggi si perde il caso dimostrato di stallo a metà body; la migrazione non introduce quel bug specifico. Stai scrivendo qualcosa di nuovo che deve scaricare molti URL? httpx è un candidato quando ti servono AsyncClient e timeout separati per connect, read, write e pool.

Scrivi il guardiano che scrivono tutti:

try:
    r = requests.get(url, timeout=0.5)
except requests.exceptions.Timeout:
    retry()

Mandalo verso un server che spedisce subito status line e header e poi si pianta prima del body. La lettura va in timeout. Il guardiano non parte. L’eccezione che esce è ConnectionError, e ConnectionError non è una sottoclasse di Timeout.

Con lo stesso blocco, usando httpx, salta fuori ReadTimeout, che è un TimeoutException, e il guardiano corrispondente lo prende al volo.

Sono andato a vedere in cosa httpx differisce da requests nei punti che mandano in crisi gli scraper, pensando di scrivere dell’async. Alla fine, l’async è risultato l’aspetto meno interessante di tutta la lista.

Cosa ho testato e come

Otto verifiche su un server fixture locale, perché il fatto che un client racconti cosa ha fatto non prova affatto quello che ha davvero fatto. Il server conta le connessioni TCP — incrementate una sola volta per ogni socket accettato, prima ancora di parsare la request line — e i path effettivamente richiesti. Il riuso delle connessioni e il follow dei redirect sono entrambi affermazioni sul traffico reale, e il traffico reale è il punto in cui si verificano davvero.

httpx 0.28.1 con l’extra http2, requests 2.34.2, Python 3.14.2, macOS arm64. Entrambi in un virtualenv nuovo di zecca, così nessuno si porta dietro tracce dell’altro. Output grezzo: httpx-probes.json.

Sei ipotesi sono entrate nel harness prima della prima esecuzione e ci sono rimaste anche dopo. Tre si sono rivelate giuste, due sbagliate, una giusta sul caso che avevo immaginato e sbagliata sul caso che contava davvero. prediction-scorecard.json contiene il conto.

I default che ti cambiano sotto i piedi

Grafico dei risultati misurati: default diversi tra i client

Comportamentorequests 2.34.2httpx 0.28.1
Segue i redirect di defaultno
La chiamata a livello di modulo riusa le connessioninono
Stallo prima degli headerReadTimeoutReadTimeout
Stallo a metà bodyConnectionErrorReadTimeout
Nessun charset dichiaratoISO-8859-1utf-8
HTTP/2non disponibileopzionale, funziona
Timeout separati per connect/read/write/poolno

Redirect, riuso dei socket, esiti delle eccezioni, decoding e negoziazione del protocollo sono stati osservati nelle verifiche. La forma dell’API dei timeout e l’assenza di un flag HTTP/2 in requests sono osservazioni sulle capacità dell’API. httpx-probes.json.

Tre di queste righe cambieranno in silenzio il comportamento del tuo codice il giorno in cui farai la migrazione.

Redirect: disattivati di default, e il server lo dimostra

Una catena di redirect in quattro salti che finisce su /ok:

ClientIl server ha vistoStatus restituito
requests5 request200
httpx1 request302
httpx, follow_redirects=True5 request200

Il cinque è dato da quattro salti più la destinazione finale. La mia previsione diceva quattro: era un conto che non ho verificato; la direzione era quella giusta e il conteggio viene corretto qui, invece di essere lasciato sottointeso nel testo.

Questo è il comportamento documentato di httpx, ed è una scelta progettuale difendibile — un redirect è qualcosa di cui chi chiama potrebbe voler essere consapevole. È anche il modo più probabile in cui una migrazione si rompe senza generare errori. Il tuo codice riceve un 302, response.text è vuoto, il parser non trova righe e i log dicono 200 OK… anzi no, dicono 302, e nessuno stava controllando lo status code perché con requests non c’era mai stato nulla da controllare.

Il risultato sui timeout, quello che avevo interpretato al contrario

Avevo previsto che httpx avrebbe nominato la fase fallita e che requests avrebbe fuso tutto in un’unica classe. È l’opposto.

Riferimento ufficiale: documentazione sui timeout di Requests.

Schema di sistema: dove si ferma il timeout

Riferimento ufficiale: documentazione sui timeout di HTTPX.

Stallorequestshttpx
Prima della status lineReadTimeoutReadTimeout
A metà body, dopo l’invio degli headerConnectionErrorReadTimeout

httpx assegna lo stesso nome corretto a entrambi i casi. requests li distingue — e li separa proprio lungo il confine su cui è costruito il codice di retry.

La conseguenza non è un’inferenza ricavata dalla gerarchia delle classi. Ho eseguito davvero il guardiano:

Stalloexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
Prima della status lineintercettaintercetta
A metà bodysfugge come ConnectionErrorintercetta

timeout-retry-guard.json. requests.exceptions.ConnectionError non è una sottoclasse di requests.exceptions.Timeout; httpx.ReadTimeout è una sottoclasse di httpx.TimeoutException.

Il messaggio dell’eccezione di requests dice Read timed out. dentro un ConnectionError. La libreria sa cosa è successo. Semplicemente non lo comunica al type system, e il type system è quello che il tuo except consulta.

Il caso misurato è specifico: gli header arrivano, poi il body smette di avanzare abbastanza a lungo da superare il timeout di lettura. Una risposta che continua a mandare chunk entro la finestra del timeout, incluso uno stream intenzionale, può comportarsi in modo diverso e qui non è stata testata.

Pooling delle connessioni: tutta la differenza sta nell’API del client

Dieci GET, quattro modi diversi, socket contati lato server:

ModalitàSocket aperti
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

Identico, e vale la pena dirlo perché è l’idea più diffusa su questa coppia — che httpx fa pooling e requests no. Nessuno dei due fa pooling a livello di modulo. Entrambi lo fanno attraverso l’oggetto client. Se oggi stai chiamando requests.get() dentro un ciclo, passare a httpx.get() dentro un ciclo non cambia nulla rispetto al churn dei socket.

Schema di sistema: il pooling vive nel client

HTTP/2 è esplicito e richiede l’extra

Contro un endpoint pubblico HTTP/2 registrato nell’artifact:

Riferimento ufficiale: RFC 9113: HTTP/2.

ClientProtocollo negoziato
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, non esiste un flag

Serve l’extra httpx[http2]. Avevo supposto che un semplice pip install httpx ti lasciasse un client che negozia in silenzio HTTP/1.1, e sono andato a controllare prima di scriverlo:

ImportError: Using http2=True, but the 'h2' package is not installed.
Make sure to install httpx using `pip install httpx[http2]`.

L’errore viene sollevato alla costruzione del Client, prima ancora di fare una richiesta, e il messaggio indica anche la correzione. Questa è la versione buona del fallimento, e io l’avevo interpretata al contrario (http2-extra-missing.json).

Questa verifica dimostra una negoziazione riuscita del protocollo su quell’endpoint. Non dimostra alcun vantaggio di velocità nello scraping; non è stato testato un carico HTTP/1.1 equivalente.

Throughput sequenziale contro concorrente sul fixture

Venti richieste verso un endpoint che dorme per 0,3 s:

ModalitàTempo totaleSocket
Sincrona, un solo Client6.138 s1
Async, un solo AsyncClient0.357 s20

L’esecuzione concorrente si è chiusa in 0,357 secondi contro i 6,138 della sequenziale. Ha anche aperto venti connessioni mentre il client sincrono ne ha riutilizzata una sola, quindi l’esperimento cambia il modello di esecuzione e la concorrenza effettiva, invece di isolare la velocità della libreria.

Questo è il modo corretto di leggere quel numero. È una misura della concorrenza contro un endpoint volutamente lento, non della velocità di httpx. Qualsiasi client con una buona storia async finirà nello stesso ordine di grandezza, e contro un endpoint veloce il divario si riduce parecchio.

Il caso del charset che non avevo pensato di prevedere

Avevo previsto che una risposta il cui header mente — charset=iso-8859-1 su byte utf-8 — avrebbe prodotto lo stesso mojibake in entrambi. Ed è così. Entrambi restituiscono Café Ubersetzung â naïve résumé, mentre la sorgente dice Café Ubersetzung — naïve résumé.

Il caso che non avevo previsto è quello che conta:

Rispostarequests decodificahttpx decodifica
charset=utf-8, byte utf-8correttocorretto
charset=iso-8859-1, byte utf-8mojibakemojibake
nessun charsetmojibakecorretto

requests torna a ISO-8859-1 quando l’header non specifica nulla, mentre httpx usa utf-8 come default. Nel fixture senza charset, i due client hanno quindi prodotto testo decodificato diverso tramite .text; chi usa response.content conserva invece gli stessi byte originali.

Memoria, visto che è economico misurarla

RSS di picco, /usr/bin/time -l, un processo nuovo per ogni cella:

Cellarequestshttpx
Solo import36.0 MiB30.6 MiB
Import + un GET35.8 MiB40.7 MiB

Sono snapshot di un singolo processo, e il valore di requests con un GET leggermente sotto quello del solo import mostra il rumore della misura. Non consentono nessuna conclusione direzionale sulla memoria; servirebbero più campioni e intervalli.

Cosa significa quando devi scegliere

Stai cercando bug nel codice esistente su requests? Controlla le assunzioni sui redirect, i gestori che catturano solo requests.exceptions.Timeout ma credono di coprire anche uno stallo a metà body, e i consumer di .text che ricevono risposte senza charset.

Stai migrando in modo meccanico verso httpx? Cambia i namespace delle eccezioni in httpx.TimeoutException o nelle classi di fase più specifiche, decidi se attivare follow_redirects e ritesta le assunzioni sul decoding. Il gestore requests già oggi si perde il caso dimostrato di stallo a metà body; la migrazione non introduce quel bug specifico.

Stai scrivendo qualcosa di nuovo che deve scaricare molti URL? httpx è un buon candidato quando ti servono AsyncClient e timeout separati per connect, read, write e pool. Quelle fasi ti dicono dove è avvenuta l’attesa — connessione, avanzamento del body della risposta, upload della request o acquisizione dal pool locale — non perché un host remoto si sia comportato così.

Stai scrivendo qualcosa di piccolo e sincrono? requests va benissimo ed è ovunque. Il motivo per spostarsi non è la velocità.

Qualunque sia la scelta, usa l’oggetto client invece della funzione a livello di modulo. È l’unico cambiamento in questa lista che è un vantaggio netto per entrambe le librerie.

Dove si colloca un’API gestita

Tutto quello che precede riguarda il livello di fetch, e il livello di fetch è la parte facile. Nessuno di questi strumenti renderizza JavaScript, nessuno gestisce una challenge anti-bot, e nessuno trasforma l’HTML nelle righe che volevi.

Nota dell’autore: Thunderbit è la nostra opzione gestita per rendering ed estrazione a partire dall’URL. Non è stata testata in questo harness di client HTTP. Considera questa categoria solo quando il problema che vuoi eliminare è l’acquisizione della pagina o l’estrazione strutturata — non la semantica del client HTTP.

Se stai recuperando pagine normali e le analizzi tu stesso, entrambi i client restano opzioni valide. Un servizio gestito è una decisione separata di build versus buy, non una prova per scegliere tra queste librerie.

Per il quadro più ampio, la nostra rassegna delle web scraping API copre le opzioni hosted e il pilastro sugli scraper open source quelle self-hosted.

Prova Thunderbit per l’estrazione di dati dal web

Verdetto

Per un nuovo layer di fetch in Python che richieda concorrenza async, timeout specifici per fase e un fallback a UTF-8, httpx è la mia scelta predefinita entro i vincoli testati qui. Requests resta una scelta valida per codice sincrono maturo, quando il rischio di migrazione pesa più di quei vantaggi. Proxy, policy di retry, fingerprint TLS, streaming, upload e variazioni realistiche di rete non sono stati testati, quindi questo non è un ranking universale dei client per lo scraping.

Il motivo per cui bisogna stare attenti è il default sui redirect, ed è un rischio reale proprio perché è una buona decisione progettuale. Esplicito è meglio di implicito, finché l’implicito non era parte portante di codice che hai già rilasciato.

La scorecard preregistrata si è chiusa con tre previsioni corrette, due sbagliate e una incompleta. La correzione utile è stata la classe di eccezione a metà body; il resto della decisione dovrebbe venire dal comportamento osservato, non dal racconto della scorecard.

Prova Thunderbit per l’estrazione di dati dal web Get Started Free

FAQ

È vero che httpx non segue davvero i redirect? Non di default, no. Il server ha contato una sola richiesta per una catena di quattro salti, e la risposta è tornata come 302. Passa follow_redirects=True a ogni chiamata, oppure impostalo una volta sul Client. È un comportamento documentato e voluto; resta comunque il punto più probabile in cui una migrazione si rompe in silenzio, perché il fallimento è un parse vuoto e non un’eccezione.

Basta davvero except requests.exceptions.Timeout? Non per un server che si blocca dopo aver inviato gli header. In quel caso viene sollevato ConnectionError, che non è una sottoclasse di Timeout, quindi il guardiano non lo intercetta — dimostrato direttamente, non dedotto. Cattura requests.exceptions.RequestException se vuoi coprire entrambi i casi, accettando però di intercettare anche cose che non sono timeout.

httpx è davvero più veloce di requests? Non in modo significativo, quando si parla di una richiesta per volta — non è per questo che è pensato. Il 17,2× di questo test misura venti richieste concorrenti contro un endpoint da 0,3 s, quindi misura la concorrenza. Se il tuo carico è sequenziale, aspettati nessun guadagno di velocità e scegli in base ai default.

Mi serve l’extra http2? Solo se vuoi HTTP/2 — e se imposti http2=True senza averlo, httpx solleva ImportError quando costruisci il Client, con un messaggio che ti dice di installare httpx[http2]. Nessun downgrade silenzioso di cui preoccuparsi. Io mi aspettavo che avvenisse e invece ho controllato prima di scriverlo.

Cosa non è stato testato qui? Il comportamento con i proxy, che conta tantissimo nello scraping e richiede un harness dedicato. I retry — httpx non include logica di retry, mentre requests la eredita da urllib3, quindi un confronto corretto è davvero un confronto tra due librerie di retry. Il fingerprinting TLS, cioè l’asse su cui i sistemi anti-bot guardano davvero e che nessuna delle due librerie affronta. Streaming e upload di file. E inoltre tutto questo è stato eseguito su una sola macchina, una sola versione di Python, e su localhost per sei delle otto verifiche — i numeri di latenza di un fixture server misurano il design, non la tua rete.

Ke
Ke
CTO di Thunderbit | Senior Data Scientist ed esperto di ML Con quasi un decennio di esperienza nel machine learning e nella data science, Ke Shen è un ex studente della Columbia University ed ex Senior Data Scientist presso Walmart Labs. Grazie a una profonda competenza, riconosciuta dai suoi pari, in Python, R, Java e statistica, condivide insight collaudati sul passaggio di algoritmi AI complessi dalla teoria a un'architettura pronta per la produzione.
Indice
Thunderbit · Agente AI per dati web

Estrai dati da qualsiasi pagina in 1 clic

Scelto da oltre 250.000 utenti
piano gratuito disponibile
Dalla pagina web al foglio di calcolo
Descrivi ciò che ti serve — l'agente AI di Thunderbit lo estrae ed esporta in Excel, Google Sheets, Airtable o Notion. Puoi iniziare gratis.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week