Correggere ProxyError, SSLError e ConnectTimeout in Requests

requests.exceptions.ProxyError è un sintomo, non una causa. Ecco la gerarchia delle eccezioni decodificata, ogni traceback associato alla sua vera soluzione e una funzione di controllo dello stato che classifica i fallimenti e ruota attorno alle uscite inattive.

requests.exceptions.ProxyError è uno dei messaggi di errore meno utili in Python: si attiva per un proxy inattivo, credenziali errate, uno schema sbagliato, un'uscita sovraccarica e un blocco del firewall, tutti con traceback quasi identici. Il trucco per risolverlo rapidamente è sapere che ProxyError non è una causa radice — è una categoria. Nel codice sorgente di Requests, ProxyError, SSLError e ConnectTimeout sono tutte sottoclassi di ConnectionError, e ognuna si attiva in una fase specifica del ciclo di vita della richiesta. Leggi la fase e leggi la causa. Questa guida decodifica la gerarchia delle eccezioni, associa ogni traceback comune alla sua vera soluzione e ti fornisce una funzione di controllo dello stato che classifica i fallimenti e ruota automaticamente attorno alle uscite morenti.

La gerarchia delle eccezioni di requests

Ogni errore relativo ai proxy in Requests discende da RequestException. Il ramo utile per il debug è ConnectionError, perché le tre eccezioni che effettivamente incontri vivono tutte sotto di esso:

Poiché i primi tre condividono un genitore, un solo except requests.exceptions.ConnectionError li cattura tutti per la logica di riprova — mentre catturare le sottoclassi individualmente ti permette di registrare perché ciascuno ha fallito. La configurazione pulita che evita la maggior parte di questi è trattata nella nostra guida ai proxy di Python Requests; questo post riguarda cosa fare una volta che il traceback è già sullo schermo.

Un'abitudine salva più tempo di qualsiasi singola soluzione: leggi il traceback dal basso verso l'alto. Requests incapsula il fallimento sottostante di urllib3, quindi i frame superiori descrivono dove è stata fatta la chiamata e i frame inferiori descrivono cosa è andato storto. La linea che vuoi è la clausola Caused by più interna — nomina il fallimento concreto (una connessione rifiutata, una discrepanza di certificato, un errore di porta analizzata) che l'esterno ProxyError o ConnectionError sta semplicemente rilanciando. Una volta che puoi leggere quella linea, il resto di questa guida è una tabella di consultazione.

Il classico ValueError prima di ProxyError

Il traceback proxy più cercato non è nemmeno un ProxyError — è ValueError: invalid literal for int() with base 10, lanciato in profondità dentro urllib3. Succede quando incorpori le credenziali nel valore del proxy senza uno schema, quindi il parser legge il testo dopo i due punti come un numero di 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",
}
Diagramma di flusso che mostra dove ogni eccezione di Python requests si attiva attraverso il ciclo di vita della richiesta: MissingSchema, ProxyError, SSLError e ReadTimeout
Il nome della fase del ciclo di vita indica la causa: un errore di battitura in fase di costruzione è MissingSchema, un fallimento di connessione proxy è ProxyError, un fallimento TLS è SSLError, un'uscita lenta è ReadTimeout.

Un controllo dello stato del proxy che classifica i fallimenti

Invece di indovinare, cattura ogni tipo di eccezione e trasformala in un verdetto in inglese semplice. Questa funzione restituisce l'IP di uscita in caso di successo e una ragione etichettata in caso di fallimento — inseriscila davanti a qualsiasi scraping per confermare che il proxy è attivo prima di bruciare richieste su di esso. Funziona contro qualsiasi gateway autenticato, inclusi proxy residenziali:

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 funziona ma Python lancia ProxyError

Se le stesse credenziali hanno successo in curl ma sollevano ProxyError in Python, una variabile d'ambiente sta quasi sempre sovrascrivendo il tuo dizionario. Requests legge HTTP_PROXY, HTTPS_PROXY e NO_PROXY dalla shell, e un valore aziendale obsoleto reindirizza silenziosamente ogni chiamata. Stampa session.proxies per vedere cosa viene realmente utilizzato, quindi disabilita completamente la ricerca dell'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())

Un 407 che sopravvive a credenziali corrette indica un piano di whitelist IP chiamato da un indirizzo non registrato — l'elenco completo delle cause è nella nostra guida 407 Proxy Authentication Required.

ProxyError intermittente: ruota, non riavviare

Il caso frustrante è il codice che funziona per venti minuti, poi lancia ProxyError, poi funziona di nuovo. Non è un bug nel tuo script — è un singolo IP di uscita che muore o viene limitato a metà esecuzione. La soluzione è riprovare con rotazione: avvolgi la chiamata, cattura ConnectionError, e lascia che un gateway rotante ti fornisca un nuovo IP al tentativo successivo. Attraverso proxy rotanti ogni riprova viaggia su un'uscita diversa, quindi un indirizzo morto non può mai fallire una richiesta due volte:

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 persiste attraverso molti IP nuovi, il problema si è spostato dal tuo pool al target: sei bloccato, non disconnesso. Questa è una lotta diversa — vedi la checklist anti-ban per il ritmo, gli header e l'igiene della sessione.

C'è un'altra distinzione che vale la pena interiorizzare, perché cambia il modo in cui rispondi. Un ProxyError o ConnectTimeout significa che la richiesta non è mai stata completata, quindi riprovarla è sicuro anche per un POST — nulla è successo dall'altra parte. Un ReadTimeout, al contrario, significa che il target ha ricevuto la tua richiesta e ha semplicemente impiegato troppo tempo a rispondere; riprovare una scrittura non idempotente può causare un doppio invio. Quando costruisci il ciclo di riprova, tratta i fallimenti in fase di connessione come liberamente riprovabili e i fallimenti in fase di lettura come riprovabili solo per GET e HEAD. Questa singola regola previene il bug sottile in cui un proxy instabile trasforma un checkout in tre.

Checklist che associa i messaggi di errore proxy comuni di Python requests alle loro soluzioni in una riga, da impossibile connettersi al proxy a fallimenti intermittenti
Leggi il messaggio, applica la soluzione: host errato, schema mancante, chiave TLS sbagliata, password non codificata o un'uscita morente si mappano ciascuno a un singolo passaggio correttivo.

Domande frequenti

Cosa causa requests.exceptions.ProxyError: impossibile connettersi al proxy?

L'host o la porta del proxy sono sbagliati, il proxy è inattivo o un firewall sta bloccando la connessione prima che qualsiasi richiesta parta. Verifica l'endpoint con curl -x usando le stesse credenziali; se curl fallisce anche, il proxy è irraggiungibile, e se curl ha successo, una variabile d'ambiente o un dizionario malformato nel tuo Python è il colpevole.

Perché ProxyError è incapsulato in HTTPSConnectionPool?

Quell'incapsulamento nomina semplicemente il pool di connessioni che urllib3 ha usato per raggiungere il target — è rumore attorno al vero messaggio annidato all'interno. Leggi la clausola Caused by più interna: Cannot connect to proxy significa un fallimento di connessione, mentre un messaggio 407 o SSL all'interno dello stesso pool indica un problema di autenticazione o di TLS/schema invece.

Come faccio a impedire che requests legga i proxy dall'ambiente?

Imposta session.trust_env = False sulla tua Session, o passa trust_env=False in modo equivalente, così Requests ignora HTTP_PROXY e HTTPS_PROXY. Questa è la soluzione quando un proxy funziona in una shell ma lancia ProxyError in un'altra, o quando una variabile aziendale dirotta uno scraper che non hai configurato per usare un proxy.

ConnectTimeout è sicuro da riprovare?

Sì — la documentazione di Requests segna ConnectTimeout come sicuro da riprovare perché la richiesta non ha mai raggiunto il server, quindi non si è potuto verificare alcun effetto collaterale. Riprovalo, idealmente attraverso un gateway rotante in modo che il tentativo successivo utilizzi un'uscita diversa e più veloce. ReadTimeout è più rischioso da riprovare alla cieca su metodi non idempotenti come POST.

Una volta che smetti di trattare ProxyError come un singolo errore e inizi a leggerlo per fase del ciclo di vita, le soluzioni sono meccaniche: errori di battitura nello schema si sollevano prima della rete, fallimenti di connessione nominano un proxy morto, errori SSL nominano una chiave sbagliata, e fallimenti intermittenti vogliono rotazione, non un riavvio. Uscite pulite rimuovono la maggior parte di essi completamente.

Scrape su IP residenziali che rimangono attivi