Um travamento no meio do corpo escapa de um manipulador comum de timeout do requests

Última atualização em August 17, 2026
Um travamento no meio do corpo escapa de um manipulador comum de timeout do requests
Resumo com IA
Encontrando bugs em código existente com requests? Revise premissas sobre redirects, handlers que capturam apenas requests.exceptions.Timeout mas esperam cobrir um travamento no meio do corpo, e consumidores de .text recebendo respostas sem charset. Migrando mecanicamente para httpx? Troque os namespaces de exceção para httpx.TimeoutException ou para as classes mais específicas por fase, decida se vai habilitar follow_redirects e teste novamente as premissas de decodificação. O handler atual do requests já deixa passar o caso de meio do corpo demonstrado; a migração não cria esse bug específico. Escrevendo algo novo que busca muitos URLs? O httpx é candidato quando você precisa de AsyncClient e de timeouts separados para connect, read, write e pool.

Escreva a proteção que todo mundo escreve:

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

Aponta isso para um servidor que manda na hora a linha de status e os headers e depois trava antes do corpo. A leitura expira. A proteção não dispara. A exceção que aparece é ConnectionError, e ConnectionError não é subclass de Timeout.

O mesmo travamento com httpx gera ReadTimeout, que é um TimeoutException, e aí a proteção equivalente captura.

Fui investigar em que pontos o httpx se diferencia do requests de um jeito que quebra scrapers, imaginando que escreveria sobre async. No fim, async foi a parte menos interessante da lista.

O que testei e como

Oito verificações em um servidor de testes local, porque a resposta de um cliente sobre o que fez não é prova do que de fato aconteceu. O servidor conta conexões TCP — incrementadas uma vez por socket aceito, antes de qualquer linha de requisição ser interpretada — e caminhos realmente acessados. Reaproveitamento de conexão e acompanhamento de redirect são, os dois, afirmações sobre a rede, e é na rede que elas são validadas.

httpx 0.28.1 com o extra http2, requests 2.34.2, Python 3.14.2, macOS arm64. Os dois rodando em um virtualenv novo, para que nenhum herdasse vestígios do outro. Saída bruta: httpx-probes.json.

Seis previsões foram colocadas no harness antes da primeira execução e ficaram lá depois. Três acertaram, duas estavam erradas, e uma estava certa sobre o caso que eu tinha em mente, mas errou o caso que importava. prediction-scorecard.json traz a conta.

Os padrões que mudam sem você perceber

Measured results chart: Defaults that differ between clients

Comportamentorequests 2.34.2httpx 0.28.1
Segue redirects por padrãosimnão
Chamada no nível do módulo reaproveita conexõesnãonão
Travamento antes dos headersReadTimeoutReadTimeout
Travamento no meio do corpoConnectionErrorReadTimeout
Sem charset declaradoISO-8859-1utf-8
HTTP/2não disponívelopcional, funciona
Timeouts separados para connect/read/write/poolnãosim

Redirects, reaproveitamento de socket, resultados de exceção, decodificação e negociação de protocolo foram observados em testes. A forma da API de timeout e a ausência de uma flag HTTP/2 no requests são observações sobre capacidade da API. httpx-probes.json.

Três dessas linhas vão mudar o comportamento do teu código no dia da migração, sem aviso.

Redirects: desligados por padrão, e o servidor prova isso

Uma cadeia de redirect com quatro saltos até /ok:

ClienteO servidor viuStatus retornado
requests5 requests200
httpx1 request302
httpx, follow_redirects=True5 requests200

O cinco é quatro saltos mais o destino. Minha previsão dizia quatro, que foi uma conta que eu não conferi; a direção era a afirmação principal, e o número foi corrigido aqui em vez de ser escondido no texto.

Esse é um comportamento documentado do httpx e faz sentido do ponto de vista de design — um redirect é algo que o chamador pode querer saber. Ainda assim, é a forma mais provável de uma migração quebrar sem levantar exceção nenhuma. Teu código recebe um 302, response.text vem vazio, o parser não encontra linhas, e teus logs dizem 200 OK… exceto que eles dizem 302, e ninguém estava olhando o status code porque com requests nunca houve necessidade de olhar.

O achado sobre timeout, que foi justamente o que eu inverti

Eu previ que o httpx daria nome à fase que falhou e que o requests misturaria tudo em uma classe só. É o contrário.

Referência oficial: Documentação de timeout do Requests.

System diagram: Where the Timeout Lands

Referência oficial: Documentação de timeout do HTTPX.

Travamentorequestshttpx
Antes da linha de statusReadTimeoutReadTimeout
No meio do corpo, depois de enviar headersConnectionErrorReadTimeout

O httpx atribui o mesmo nome correto aos dois casos. O requests separa os casos — e faz essa divisão justamente na fronteira que o código de retry usa.

A consequência não vem de uma inferência sobre a hierarquia de classes. Eu rodei a proteção:

Travamentoexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
Antes da linha de statuscapturacaptura
No meio do corpoescapa como ConnectionErrorcaptura

timeout-retry-guard.json. requests.exceptions.ConnectionError não é subclass de requests.exceptions.Timeout; httpx.ReadTimeout é subclass de httpx.TimeoutException.

A mensagem da exceção no requests diz Read timed out. dentro de um ConnectionError. A biblioteca sabe o que aconteceu. Ela só não passa isso para o sistema de tipos, e é o sistema de tipos que o teu except consulta.

O caso medido é específico: os headers chegam, e então o progresso do corpo para tempo suficiente para exceder o timeout de leitura. Uma resposta que continua entregando chunks dentro da janela do timeout, incluindo um stream intencional, pode se comportar de forma diferente e não foi testada aqui.

Pool de conexões: a API do cliente é toda a diferença

Dez GETs, quatro formas, sockets contados do lado do servidor:

ComoSockets abertos
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

Igual, e vale registrar isso porque é a crença popular mais comum sobre essa dupla — que o httpx faz pool e o requests não. Nenhum deles faz pool no nível do módulo. Ambos reaproveitam conexões por meio do objeto cliente. Se hoje tu chamas requests.get() dentro de um loop, trocar por httpx.get() num loop não muda nada no churn de sockets.

System diagram: Pooling Lives in the Client

HTTP/2 é explícito e exige o extra

Contra um endpoint público com HTTP/2 registrado no artefato:

Referência oficial: RFC 9113: HTTP/2.

ClienteNegociado
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, não existe flag

Tu precisa do extra httpx[http2]. Eu supus que um pip install httpx simples deixaria você com um cliente que negociaria HTTP/1.1 silenciosamente, e fui conferir antes de escrever isso:

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

Ele lança no momento de construir o Client, antes de qualquer requisição, e a mensagem já aponta a correção. Essa é a versão boa dessa falha, e eu tinha imaginado o contrário (http2-extra-missing.json).

Essa verificação comprova a negociação bem-sucedida do protocolo nesse endpoint. Ela não comprova ganho de velocidade em scraping; nenhum workload correspondente em HTTP/1.1 foi testado.

Throughput sequencial versus concorrente no fixture

Vinte requisições para um endpoint que dorme 0,3 s:

ModoTempo totalSockets
Sync, um Client6.138 s1
Async, um AsyncClient0.357 s20

A execução concorrente terminou em 0,357 segundos, contra 6,138 segundos na execução sequencial. Ela também abriu vinte conexões enquanto o cliente síncrono reutilizou uma, então o experimento muda o modelo de execução e a concorrência efetiva, em vez de isolar a velocidade da biblioteca.

Essa é a leitura honesta do número. É uma medição de concorrência contra um endpoint propositalmente lento, não uma medição do httpx. Qualquer cliente com um async funcional cai na mesma faixa, e contra um endpoint rápido a diferença desaparece.

O caso de charset que eu não pensei em prever

Eu previ que uma resposta cujo header mente — charset=iso-8859-1 em bytes utf-8 — produziria o mesmo mojibake em ambos. Produz. Ambos retornam Café Ubersetzung â naïve résumé quando o original diz Café Ubersetzung — naïve résumé.

O caso que eu não previ é o que realmente importa:

Respostarequests decodificahttpx decodifica
charset=utf-8, bytes utf-8corretocorreto
charset=iso-8859-1, bytes utf-8mojibakemojibake
sem charset algummojibakecorreto

O requests cai para ISO-8859-1 quando o header não informa nada, enquanto o httpx usa utf-8 por padrão. No fixture sem charset, os clientes produziram textos decodificados diferentes via .text; consumidores que usam response.content mantêm os mesmos bytes originais.

Memória, já que é barato medir

RSS máximo, /usr/bin/time -l, um processo novo por célula:

Célularequestshttpx
Só importação36.0 MiB30.6 MiB
Importação + um GET35.8 MiB40.7 MiB

Esses são snapshots de um único processo, e o valor do requests com um GET ficando ligeiramente abaixo do valor só de importação expõe o ruído da execução. Eles não sustentam conclusão direcional sobre memória; seriam necessárias amostras repetidas e intervalos.

O que isso significa na hora de escolher

Encontrando bugs em código existente com requests? Revise premissas sobre redirects, handlers que capturam só requests.exceptions.Timeout mas esperam cobrir um travamento no meio do corpo, e consumidores de .text recebendo respostas sem charset.

Migrando mecanicamente para httpx? Troque os namespaces de exceção para httpx.TimeoutException ou para as classes mais específicas por fase, decida se vai habilitar follow_redirects, e teste de novo as premissas de decodificação. O handler atual do requests já deixa passar o caso de meio do corpo demonstrado; a migração não cria esse bug específico.

Escrevendo algo novo que busca muitos URLs? O httpx é candidato quando tu precisa de AsyncClient e de timeouts separados para connect, read, write e pool. Essas fases dizem onde a espera aconteceu — estabelecimento da conexão, progresso do corpo da resposta, upload da requisição ou aquisição do pool local —, não por que um host remoto se comportou daquele jeito.

Escrevendo algo pequeno e síncrono? requests está ótimo e está em todo lugar. O motivo para mudar não é velocidade.

Seja qual for a tua escolha, use o objeto cliente em vez da função no nível do módulo. Essa é a única mudança nesta lista que é ganho direto nas duas bibliotecas.

Onde uma API gerenciada entra nisso

Tudo acima é a camada de fetch, e a camada de fetch é a parte fácil. Nada disso renderiza JavaScript, nada disso lida com challenge anti-bot, e nada disso transforma HTML nas linhas que tu queria.

Nota do autor: Thunderbit é nossa opção gerenciada para renderização e extração a partir de uma URL. Ela não foi testada neste harness de cliente HTTP. Considere essa categoria só quando o problema que tu quer remover for aquisição da página ou extração estruturada — e não a semântica do cliente HTTP.

Se tu está buscando páginas comuns e fazendo o parsing por conta própria, ambos os clientes continuam valendo. Um serviço gerenciado é uma decisão separada de build versus buy, não um argumento para escolher entre essas bibliotecas.

Para o panorama mais amplo, nosso resumo das melhores APIs de web scraping cobre as opções hospedadas, e o pilar de scrapers open source cobre as auto-hospedadas.

Experimente Thunderbit para Extração de Dados da Web

Veredito

Para uma nova camada de fetch em Python que precise de concorrência async, timeouts específicos por fase e fallback para UTF-8, httpx é minha escolha padrão dentro das restrições testadas aqui. requests continua sendo uma opção viável para código síncrono maduro, quando o risco de migração pesa mais do que esses benefícios. Proxies, políticas de retry, fingerprint TLS, streaming, uploads e variação real de rede não foram testados, então isso não é um ranking universal de clientes de scraping.

O motivo para ter cuidado é o padrão de redirects, e ele é um risco real justamente por ser uma boa decisão de design. Explícito é melhor que implícito — até o dia em que a coisa implícita era parte estrutural do código que tu já publicou.

A scorecard pré-registrada terminou com três previsões corretas, duas incorretas e uma previsão incompleta. A correção útil foi a classe de exceção no meio do corpo; o resto da decisão deve vir do comportamento observado, e não da narrativa da scorecard.

Experimente Thunderbit para Extração de Dados da Web Get Started Free

Perguntas frequentes

O httpx realmente não segue redirects? Não por padrão. O servidor contou uma requisição para uma cadeia de quatro saltos, e a resposta voltou como 302. Passe follow_redirects=True em cada chamada, ou configure isso uma vez no Client. Isso é documentado e intencional; ainda assim, é a coisa com maior chance de quebrar silenciosamente numa migração, porque a falha é um parse vazio, não uma exceção.

except requests.exceptions.Timeout realmente não é suficiente? Não para um servidor que trava depois de enviar headers. Nesse caso, a exceção é ConnectionError, que não é subclass de Timeout, então a proteção não pega — demonstrado diretamente, não inferido. Capture requests.exceptions.RequestException se quiser cobrir ambos, aceitando que também estará capturando coisas que não são timeouts.

O httpx é mais rápido que o requests? Não de forma relevante, quando se faz uma requisição por vez — não é para isso que ele serve. O 17,2× deste teste é uma medição de vinte requisições concorrentes contra um endpoint de 0,3 s, ou seja, uma medição de concorrência. Se teu workload é sequencial, espere nenhum ganho de velocidade e escolha com base nos padrões.

Preciso do extra http2? Só se tu quiser HTTP/2 — e, se definir http2=True sem ele, o httpx lança ImportError ao construir o Client, com uma mensagem dizendo para instalar httpx[http2]. Nada de downgrade silencioso para se preocupar. Eu esperava um e fui conferir em vez de escrever no papel.

O que não foi testado aqui? Comportamento de proxy, que importa muito para scraping e precisa de um harness próprio. Retries — o httpx não traz lógica de retry e o requests a obtém do urllib3, então uma comparação justa é, na prática, uma comparação entre duas bibliotecas de retry. Fingerprinting TLS, que é o eixo que sistemas anti-bot realmente observam e que nenhuma das duas bibliotecas resolve. Streaming e uploads de arquivos. E tudo aqui foi feito em uma máquina, uma versão de Python e localhost em seis das oito verificações — números de latência de um servidor fixture medem o design, não a tua rede.

Ke
Ke
CTO na Thunderbit | Cientista de Dados Sênior e Especialista em ML Com quase uma década de experiência em machine learning e data science, Ke Shen é ex-aluno da Columbia University e foi Cientista de Dados Sênior na Walmart Labs. Com profunda experiência, reconhecida pelos pares, em Python, R, Java e Estatística, ele compartilha insights testados em batalha sobre como levar algoritmos complexos de IA da teoria à arquitetura pronta para produção.
Sumário
Thunderbit · Agente de dados web com IA

Extraia dados de qualquer página em 1 clique

Aprovado por mais de 250.000 usuários
plano gratuito disponível
Da página web para a planilha
Descreva o que você precisa — o Agente de IA da Thunderbit extrai e exporta para Excel, Google Sheets, Airtable ou Notion. Comece grátis.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week