Een mid-body-storing glipt door een veelgebruikte requests-timeout-handler

Laatst bijgewerkt op August 17, 2026
Een mid-body-storing glipt door een veelgebruikte requests-timeout-handler
AI-samenvatting
Bugs zoeken in bestaande requests-code? Controleer aannames over redirects, handlers die alleen requests.exceptions.Timeout opvangen maar denken ook een mid-body-storing te dekken, en .text-gebruikers die responses zonder charset krijgen. Mechanisch migreren naar httpx? Pas exception-namespaces aan naar httpx.TimeoutException of de fijnere faseklassen, beslis of je followredirects wilt inschakelen, en test aannames over decodering opnieuw. De bestaande requests-handler mist het getoonde mid-body-geval al; migratie creëert die specifieke bug niet. Iets nieuws schrijven dat veel URL's ophaalt? httpx is een kandidaat als je AsyncClient nodig hebt en aparte connect-, read-, write- en pool-timeouts.

Gebruik de guard die iedereen schrijft:

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

Richt die op een server die meteen de statusregel en headers terugstuurt en daarna vastloopt vóór de body. Dan loopt de read time-out af. De guard wordt niet getriggerd. De uitzondering die terugkomt is ConnectionError, en ConnectionError is geen subclass van Timeout.

Bij dezelfde storing geeft httpx ReadTimeout, en dat is wél een TimeoutException. Die wordt door de equivalente guard dus netjes opgevangen.

Ik ging op zoek naar de verschillen tussen httpx en requests op punten die scrapers kunnen breken, met het idee dat ik vooral over async zou schrijven. Uiteindelijk bleek async het minst interessante onderwerp op de lijst.

Wat ik testte en hoe

Acht probes op een lokale testserver, omdat wat een client over zichzelf zegt geen bewijs is van wat hij echt heeft gedaan. De server telt TCP-verbindingen — telkens één keer omhoog bij een geaccepteerde socket, nog vóór er een requestregel wordt geparsed — en daadwerkelijk opgehaalde paden. Hergebruik van verbindingen en het volgen van redirects zijn allebei netwerkclaims, en juist daar kun je ze controleren.

httpx 0.28.1 met de http2-extra, requests 2.34.2, Python 3.14.2, macOS arm64. Allebei draaiden ze in één verse virtualenv, zodat geen van beide iets van de ander erft. Ruwe output: httpx-probes.json.

Zes voorspellingen gingen vóór de eerste run het harness in en bleven daarna staan. Drie klopten, twee niet, één zat goed op het geval dat ik had bedacht maar miste het geval dat er echt toe deed. prediction-scorecard.json bevat de afrekening.

De defaults die onder je voeten verschuiven

Measured results chart: Defaults that differ between clients

Gedragrequests 2.34.2httpx 0.28.1
Volgt redirects standaardjanee
Module-level aanroep hergebruikt verbindingenneenee
Vastlopen vóór headersReadTimeoutReadTimeout
Vastlopen midden in de bodyConnectionErrorReadTimeout
Geen charset gedeclareerdISO-8859-1utf-8
HTTP/2niet beschikbaaropt-in, werkt
Gescheiden connect/read/write/pool timeoutsneeja

Redirects, sockethergebruik, uitzonderingsuitkomsten, decodering en protocolnegotiatie zijn geobserveerd in probes. De vorm van de timeout-API en het ontbreken van een HTTP/2-vlag in requests zijn observaties over de API-mogelijkheden. httpx-probes.json.

Drie van deze regels veranderen stilletjes wat je code doet op de dag dat je overstapt.

Redirects: standaard uit, en de server bewijst het

Een redirectketen van vier stappen die eindigt op /ok:

ClientServer zagGeretourneerde status
requests5 requests200
httpx1 request302
httpx, follow_redirects=True5 requests200

Die vijf is vier stappen plus de bestemming. Mijn voorspelling zei vier, wat rekenwerk was dat ik niet had gecontroleerd; de richting was de claim en het aantal is hier gecorrigeerd in plaats van stilletjes in de tekst.

Dit is gedocumenteerd gedrag van httpx en een verdedigbare keuze — een redirect is iets waar de aanroeper misschien iets van wil weten. Het is ook de meest waarschijnlijke manier waarop een migratie stukloopt zonder dat er een uitzondering wordt gegooid. Je code krijgt een 302, response.text is leeg, je parser vindt geen rijen, en je logs zeggen 200 OK… behalve dat ze 302 zeggen, en niemand lette op de statuscode omdat er met requests nooit iets was om op te letten.

De timeoutbevinding, en dit is de ene waar ik het mis had

Ik voorspelde dat httpx de mislukte fase zou benoemen en dat requests beide gevallen in één klasse zou laten opgaan. Het is precies andersom.

Officiële verwijzing: Requests timeout-documentatie.

System diagram: Where the Timeout Lands

Officiële verwijzing: HTTPX timeout-documentatie.

Storingrequestshttpx
Vóór de statusregelReadTimeoutReadTimeout
Midden in de body, nadat headers al zijn verzondenConnectionErrorReadTimeout

httpx geeft voor beide gevallen dezelfde, correcte naam. requests splitst ze — precies over de grens waar retry-code op is geschreven.

Het gevolg hoef je niet uit de class hierarchy af te leiden. Ik heb de guard gewoon uitgevoerd:

Storingexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
Vóór de statusregelvangt afvangt af
Midden in de bodyglipt weg als ConnectionErrorvangt af

timeout-retry-guard.json. requests.exceptions.ConnectionError is geen subclass van requests.exceptions.Timeout; httpx.ReadTimeout is wel een subclass van httpx.TimeoutException.

De foutmelding van requests zegt Read timed out. binnen een ConnectionError. De bibliotheek weet dus wat er gebeurde. Alleen geeft hij het niet door aan het type-systeem, en juist dat type-systeem kijkt je except-clausule aan.

Het gemeten geval is specifiek: headers komen binnen, daarna stopt de voortgang van de body lang genoeg om de read timeout te overschrijden. Een response die binnen het timeoutvenster steeds chunks blijft leveren, bijvoorbeeld een bewuste stream, kan anders reageren en is hier niet getest.

Connection pooling: de client-API maakt het verschil

Tien GET-requests, vier manieren, sockets geteld aan serverzijde:

ManierGeopende sockets
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

Identiek, en het is goed om dat expliciet te zeggen, omdat dit de meest gehoorde folk knowledge over deze twee libraries is — dat httpx pooled en requests niet. Geen van beide pooled op module-niveau. Beide poolen via hun client-object. Als je vandaag requests.get() in een loop gebruikt, verandert overstappen naar httpx.get() in een loop niets aan je socket churn.

System diagram: Pooling Lives in the Client

HTTP/2 is expliciet en vereist de extra

Tegen één publiek HTTP/2-endpoint dat in het artefact is vastgelegd:

Officiële verwijzing: RFC 9113: HTTP/2.

ClientOnderhandeld protocol
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, er bestaat geen vlag

Je hebt de httpx[http2]-extra nodig. Ik ging ervan uit dat een gewone pip install httpx je een client zou geven die stilletjes 1.1 onderhandelt, en heb dat vóór het schrijven even gecontroleerd:

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

Hij gooit een fout bij het aanmaken van de Client, nog vóór één enkele request, en de melding noemt meteen de oplossing. Dat is de goede versie van deze fout, en ik had het andersom verwacht (http2-extra-missing.json).

Deze probe laat zien dat protocolnegotiatie op dat endpoint werkt. Ze bewijst geen voordeel in scraping-snelheid; er is geen vergelijkbare HTTP/1.1-workload getest.

Sequentiële versus gelijktijdige doorvoer op de testserver

Twintig requests naar een endpoint dat 0,3 s slaapt:

ModusTotale tijdSockets
Sync, één Client6.138 s1
Async, één AsyncClient0.357 s20

De gelijktijdige run was klaar in 0,357 seconden tegenover 6,138 seconden voor de sequentiële run. Hij opende ook twintig verbindingen terwijl de synchrone client er één hergebruikte, dus dit experiment verandert het uitvoeringsmodel en de effectieve paralleliteit in plaats van dat het puur library-snelheid isoleert.

Dat is de eerlijke lezing van het getal. Het is een meting van concurrency tegen een bewust traag endpoint, geen meting van httpx zelf. Elke client met een werkend async-verhaal komt ongeveer in dezelfde buurt, en tegen een snel endpoint verdwijnt het verschil.

Het charset-geval dat ik niet dacht te voorspellen

Ik voorspelde dat een response waarvan de header liegt — charset=iso-8859-1 op utf-8-bytes — in beide gevallen dezelfde mojibake zou geven. Dat klopt. Beide geven Café Ubersetzung â naïve résumé terug terwijl de bron Café Ubersetzung — naïve résumé zegt.

Het geval dat ik níet voorspelde, is het geval dat ertoe doet:

Responserequests decoderthttpx decodert
charset=utf-8, utf-8-bytescorrectcorrect
charset=iso-8859-1, utf-8-bytesmojibakemojibake
geen charsetmojibakecorrect

requests valt terug op ISO-8859-1 wanneer de header niets zegt, terwijl httpx standaard utf-8 gebruikt. In de test zonder charset leverden de clients daarom verschillende gedecodeerde tekst op via .text; gebruikers van response.content behouden gewoon dezelfde originele bytes.

Geheugen, omdat dat goedkoop te meten is

Peak RSS, /usr/bin/time -l, per cel één vers proces:

Celrequestshttpx
Alleen import36.0 MiB30.6 MiB
Import plus één GET35.8 MiB40.7 MiB

Dit zijn snapshots van één proces, en het feit dat requests’ één-GET-waarde iets onder de import-only-waarde ligt, laat vooral ruis in de run zien. Hieruit kun je geen harde conclusie over geheugen trekken; daarvoor heb je herhaalde samples en spreiding nodig.

Wat dit betekent bij de keuze

Bugs zoeken in bestaande requests-code? Controleer aannames over redirects, handlers die alleen requests.exceptions.Timeout afvangen maar denken ook een mid-body-storing te dekken, en .text-consumenten die responses zonder charset krijgen.

Mechanisch migreren naar httpx? Pas exception-namespaces aan naar httpx.TimeoutException of de fijnere faseklassen, bepaal of je follow_redirects wilt aanzetten, en test aannames over decodering opnieuw. De bestaande requests-handler mist het getoonde mid-body-geval al; migratie creëert die specifieke bug niet.

Iets nieuws schrijven dat veel URL's ophaalt? httpx is een kandidaat als je AsyncClient nodig hebt en aparte connect-, read-, write- en pool-timeouts. Die fasen laten zien waar het wachten zat — tijdens het opzetten van de verbinding, het binnenhalen van de response-body, het uploaden van de request of het krijgen van een vrije pool-slot — niet waarom een remote host zich zo gedroeg.

Iets kleins en synchroon schrijven? requests is prima en overal aanwezig. De reden om over te stappen is niet snelheid.

Wat je ook kiest, gebruik het client-object in plaats van de module-level functie. Dat is de ene verandering op deze lijst die in beide libraries een duidelijke winst oplevert.

Waar een managed API past

Alles hierboven zit op de fetch-laag, en dat is het makkelijke deel. Niets hiervan rendert JavaScript, niets handelt een anti-bot challenge af, en niets zet HTML om in de rijen die je wilde hebben.

Auteur opmerking: Thunderbit is onze managed optie voor URL-in rendering en extractie. Die is niet getest in dit HTTP-client-harness. Zie dat soort oplossing alleen als het ophalen van pagina’s of gestructureerde extractie — niet de semantiek van de HTTP-client — het probleem is dat je wilt oplossen.

Als je gewoon pagina’s ophaalt en zelf parseert, blijven beide clients relevant. Een managed service is een aparte build-versus-buy-beslissing, geen bewijs voor een keuze tussen deze libraries.

Voor het bredere veld behandelt onze overzichtspagina voor webscraping-API's de gehoste opties en de open-source scraper-pagina de zelfgehoste.

Probeer Thunderbit voor webdata-extractie

Eindoordeel

Voor een nieuwe Python-fetch-laag die async concurrency, fase-specifieke timeouts en een UTF-8-fallback nodig heeft, is httpx onder de hier geteste voorwaarden mijn standaardkeuze. Requests blijft prima bruikbaar voor volwassen synchrone code waarbij migratierisico zwaarder weegt dan die voordelen. Proxies, retry-beleid, TLS-fingerprinting, streaming, uploads en realistische netwerkvariatie zijn niet getest, dus dit is geen universele ranglijst van scraping-clients.

De reden om voorzichtig te zijn is de redirect-default, en dat is juist een reëel risico omdat het een goede ontwerpkeuze is. Expliciet is beter dan impliciet, totdat het impliciete ding dragend bleek te zijn in code die je al hebt uitgebracht.

De vooraf geregistreerde scorecard eindigde met drie juiste voorspellingen, twee onjuiste en één onvolledige voorspelling. De nuttige correctie was de exception class bij mid-body-stalls; de rest van de beslissing moet komen uit het geobserveerde gedrag, niet uit het verhaal van de scorecard.

Probeer Thunderbit voor webdata-extractie Get Started Free

Veelgestelde vragen

Volgt httpx echt geen redirects? Niet standaard, nee. De server telde één request voor een keten van vier hops, en de response kwam terug als een 302. Geef per call follow_redirects=True mee, of stel het één keer in op de Client. Dit is gedocumenteerd en bewust zo ontworpen; toch is het nog steeds het meest waarschijnlijke punt waarop een migratie stilletjes misgaat, omdat het falen een lege parse is in plaats van een uitzondering.

Is except requests.exceptions.Timeout echt niet genoeg? Niet voor een server die vastloopt nadat de headers zijn verzonden. In dat geval krijg je ConnectionError, en dat is geen subclass van Timeout, dus de guard mist het — rechtstreeks aangetoond, niet afgeleid. Vang requests.exceptions.RequestException als je beide wilt meenemen, en accepteer dat je daarmee ook dingen opvangt die geen timeouts zijn.

Is httpx sneller dan requests? Niet echt, als je één request tegelijk doet — daarvoor is het niet bedoeld. De 17,2× in deze test is een meting van twintig gelijktijdige requests tegen een endpoint van 0,3 s, dus van concurrency. Als je workload sequentieel is, verwacht dan geen snelheidswinst en kies op basis van de defaults.

Heb ik de http2-extra nodig? Alleen als je HTTP/2 wilt — en als je http2=True instelt zonder die extra, gooit httpx een ImportError bij het construeren van de Client, met een bericht dat je vertelt httpx[http2] te installeren. Geen stille downgrade om je zorgen over te maken. Ik verwachtte die wel en heb het daarom eerst gecontroleerd in plaats van het zomaar op te schrijven.

Wat is hier niet getest? Proxygedrag, en dat is erg belangrijk voor scraping en verdient een eigen harness. Retries — httpx levert geen retry-logica en requests krijgt die van urllib3, dus een eerlijke vergelijking is eigenlijk een vergelijking van twee retry-bibliotheken. TLS-fingerprinting, de as waar anti-botsystemen echt naar kijken en waar geen van beide libraries iets aan doet. Streaming en file uploads. En alles hier draaide op één machine, één Python-versie en in zes van de acht probes op localhost — latencycijfers van een testserver meten het ontwerp, niet je netwerk.

Ke
Ke
CTO bij Thunderbit | Senior Data Scientist & ML-expert Met bijna tien jaar ervaring in machine learning en data science is Ke Shen alumnus van Columbia University en voormalig Senior Data Scientist bij Walmart Labs. Met diepgaande, door vakgenoten erkende expertise in Python, R, Java en statistiek deelt hij praktijkgerichte inzichten over hoe je complexe AI-algoritmen van theorie naar productieklare architectuur brengt.
Inhoudsopgave
Thunderbit · AI-webdata-agent

Gegevens extraheren van elke pagina in 1 klik

Vertrouwd door meer dan 250.000 gebruikers
gratis abonnement beschikbaar
Van webpagina naar spreadsheet
Beschrijf wat je nodig hebt — Thunderbit's AI Agent scrapt het en exporteert het naar Excel, Google Sheets, Airtable of Notion. Gratis om te starten.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week