Corrigir ProxyError, SSLError & ConnectTimeout em Requests

requests.exceptions.ProxyError é um sintoma, não uma causa. Aqui está a hierarquia de exceções decodificada, cada traceback associado à sua correção real, e uma função de verificação de integridade que classifica falhas e gira em torno de saídas inativas.

requests.exceptions.ProxyError é uma das mensagens de erro menos úteis em Python: ela dispara para um proxy inativo, credenciais erradas, um esquema ruim, uma saída sobrecarregada e um bloqueio de firewall, todos com tracebacks quase idênticos. O truque para corrigi-lo rapidamente é saber que ProxyError não é uma causa raiz — é uma categoria. No código-fonte do Requests, ProxyError, SSLError e ConnectTimeout são subclasses de ConnectionError, e cada um dispara em um estágio específico do ciclo de vida da solicitação. Leia o estágio e você lerá a causa. Este guia decodifica a hierarquia de exceções, associa cada traceback comum à sua correção real e fornece uma função de verificação de integridade que classifica falhas e gira em torno de saídas moribundas automaticamente.

A hierarquia de exceções do requests

Todo erro relacionado a proxy no Requests descende de RequestException. O ramo útil para depuração é ConnectionError, porque as três exceções que você realmente encontra estão todas sob ele:

Como os três primeiros compartilham um pai, um except requests.exceptions.ConnectionError captura todos eles para lógica de repetição — enquanto capturar as subclasses individualmente permite que você registre por que cada um falhou. A configuração limpa que evita a maioria destes é coberta em nosso guia de proxy do Python Requests; este post é sobre o que fazer uma vez que o traceback já está na tela.

Um hábito economiza mais tempo do que qualquer correção única: leia o traceback de baixo para cima. Requests envolve a falha subjacente do urllib3, então os quadros superiores descrevem onde a chamada foi feita e os quadros inferiores descrevem o que deu errado. A linha que você quer é a cláusula Caused by mais interna — ela nomeia a falha concreta (uma conexão recusada, uma incompatibilidade de certificado, um erro de porta analisada) que o ProxyError ou ConnectionError externo está apenas reerguendo. Uma vez que você pode ler essa linha, o resto deste guia é uma tabela de consulta.

O clássico ValueError antes do ProxyError

O traceback de proxy mais pesquisado não é nem mesmo um ProxyError — é ValueError: invalid literal for int() with base 10, lançado profundamente dentro do urllib3. Isso acontece quando você incorpora credenciais no valor do proxy sem um esquema, então o analisador lê o texto após os dois pontos como um número de porta:

# Broken — no scheme, so 'pass@host' is parsed as host:port
proxies = {"https": "user:pass@45.11.22.33:8000"}
# -> ValueError: invalid literal for int() with base 10: 'pass@45.11.22.33'

# Fixed — scheme in front, password URL-encoded if it has @ : or /
from urllib.parse import quote
pw = quote("p@ss:word", safe="")
proxies = {
    "http":  f"http://user:{pw}@gate.quantumproxies.io:PORT",
    "https": f"http://user:{pw}@gate.quantumproxies.io:PORT",
}
Diagrama de fluxo mostrando onde cada exceção do Python requests dispara ao longo do ciclo de vida da solicitação: MissingSchema, ProxyError, SSLError e ReadTimeout
O estágio do ciclo de vida nomeia a causa: um erro de digitação na construção é MissingSchema, uma conexão de proxy falhada é ProxyError, uma falha TLS é SSLError, uma saída lenta é ReadTimeout.

Uma verificação de integridade do proxy que classifica falhas

Em vez de adivinhar, capture cada tipo de exceção e transforme-o em um veredicto em inglês simples. Esta função retorna o IP de saída em caso de sucesso e uma razão rotulada em caso de falha — coloque-a na frente de qualquer raspagem para confirmar que o proxy está ativo antes de gastar requests nele. Funciona contra qualquer gateway autenticado, incluindo proxies residenciais:

import requests

def check_proxy(proxies, url="https://httpbin.org/ip", timeout=(5, 20)):
    try:
        r = requests.get(url, proxies=proxies, timeout=timeout)
        r.raise_for_status()
        return True, r.json().get("origin")
    except requests.exceptions.ProxyError as e:
        return False, f"proxy unreachable or auth rejected: {e}"
    except requests.exceptions.SSLError as e:
        return False, f"TLS failed (https:// in the https key?): {e}"
    except requests.exceptions.ConnectTimeout:
        return False, "proxy did not answer within the connect window"
    except requests.exceptions.ReadTimeout:
        return False, "target too slow after connect (exit quality)"
    except requests.exceptions.RequestException as e:
        return False, f"other request error: {e}"

ok, detail = check_proxy(proxies)
print("OK" if ok else "FAIL", detail)

Quando curl funciona mas Python lança ProxyError

Se as mesmas credenciais têm sucesso no curl mas geram ProxyError em Python, uma variável de ambiente está quase sempre substituindo seu dicionário. Requests lê HTTP_PROXY, HTTPS_PROXY e NO_PROXY do shell, e um valor corporativo obsoleto redireciona silenciosamente cada chamada. Imprima session.proxies para ver o que está realmente sendo usado, depois desative completamente a busca no ambiente:

import requests

session = requests.Session()
session.trust_env = False          # ignore HTTP_PROXY / HTTPS_PROXY from the shell
session.proxies = {
    "http":  "http://USER:PASS@gate.quantumproxies.io:PORT",
    "https": "http://USER:PASS@gate.quantumproxies.io:PORT",
}
print(session.get("https://httpbin.org/ip", timeout=(5, 20)).json())

Um 407 que sobrevive a credenciais corretas aponta para um plano de lista branca de IP chamado de um endereço não registrado — a lista completa de causas está em nosso guia de 407 Proxy Authentication Required.

ProxyError intermitente: gire, não reinicie

O caso frustrante é o código que roda por vinte minutos, então lança ProxyError, então funciona novamente. Isso não é um bug no seu script — é um único IP de saída morrendo ou sendo limitado por taxa no meio da execução. A correção é tentar novamente com rotação: envolva a chamada, capture ConnectionError, e deixe um gateway rotativo lhe entregar um novo IP na próxima tentativa. Através de proxies rotativos cada tentativa viaja por uma saída diferente, então um endereço morto nunca pode falhar uma solicitação duas vezes:

import requests

def get_with_rotation(url, proxies, attempts=4):
    last = None
    for _ in range(attempts):
        try:
            r = requests.get(url, proxies=proxies, timeout=(5, 20))
            if r.status_code not in (429, 500, 502, 503, 504):
                return r
            last = r.status_code
        except requests.exceptions.ConnectionError as e:  # Proxy/SSL/ConnectTimeout
            last = e
    raise RuntimeError(f"failed after {attempts} attempts: {last}")

Se ProxyError persistir em muitos IPs novos, o problema se moveu do seu pool para o alvo: você está sendo bloqueado, não desconectado. Essa é uma luta diferente — veja a lista de verificação anti-ban para ritmo, cabeçalhos e higiene de sessão.

Há mais uma distinção que vale a pena internalizar, porque muda como você responde. Um ProxyError ou ConnectTimeout significa que a solicitação nunca foi concluída, então tentar novamente é seguro mesmo para um POST — nada aconteceu do outro lado. Um ReadTimeout, por outro lado, significa que o alvo recebeu sua solicitação e simplesmente demorou muito para responder; tentar novamente uma gravação não-idempotente pode resultar em envio duplo. Quando você constrói o loop de repetição, trate falhas de estágio de conexão como livremente repetíveis e falhas de estágio de leitura como repetíveis apenas para GET e HEAD. Essa única regra previne o bug sutil onde um proxy instável transforma um checkout em três.

Lista de verificação que associa mensagens de erro comuns de proxy do Python requests às suas correções de uma linha, desde não pode conectar ao proxy até falhas intermitentes
Leia a mensagem, aplique a correção: host ruim, esquema ausente, chave TLS errada, senha não codificada ou uma saída moribunda cada um mapeia para um único passo corretivo.

Perguntas frequentes

O que causa requests.exceptions.ProxyError: não pode conectar ao proxy?

O host ou porta do proxy está errado, o proxy está fora do ar, ou um firewall está bloqueando a conexão antes que qualquer solicitação saia. Verifique o endpoint com curl -x usando as mesmas credenciais; se curl também falhar, o proxy está inacessível, e se curl tiver sucesso, uma variável de ambiente ou um dicionário malformado no seu Python é o culpado.

Por que ProxyError está envolto em HTTPSConnectionPool?

Esse wrapper apenas nomeia o pool de conexões que o urllib3 usou para alcançar o alvo — é ruído ao redor da mensagem real aninhada dentro. Leia a cláusula Caused by mais interna: Cannot connect to proxy significa uma falha de conexão, enquanto uma mensagem 407 ou SSL dentro do mesmo pool aponta para um problema de autenticação ou TLS/esquema em vez disso.

Como faço para parar requests de ler proxies do ambiente?

Defina session.trust_env = False na sua Session, ou passe trust_env=False de forma equivalente, para que Requests ignore HTTP_PROXY e HTTPS_PROXY. Esta é a correção quando um proxy funciona em um shell mas gera ProxyError em outro, ou quando uma variável corporativa sequestra um scraper que você não configurou para usar um proxy.

É seguro tentar novamente ConnectTimeout?

Sim — a documentação do Requests marca ConnectTimeout como seguro para tentar novamente porque a solicitação nunca chegou ao servidor, então nenhum efeito colateral poderia ter ocorrido. Tente novamente, idealmente através de um gateway rotativo para que a próxima tentativa use uma saída diferente e mais rápida. ReadTimeout é mais arriscado para tentar novamente cegamente em métodos não-idempotentes como POST.

Uma vez que você pare de tratar ProxyError como uma única falha e comece a lê-lo por estágio do ciclo de vida, as correções são mecânicas: erros de esquema levantam antes da rede, falhas de conexão nomeiam um proxy morto, erros de SSL nomeiam uma chave errada, e falhas intermitentes querem rotação, não um reinício. Saídas limpas removem a maioria delas completamente.

Raspe em IPs residenciais que permanecem ativos