Corriger ProxyError, SSLError & ConnectTimeout dans Requests

requests.exceptions.ProxyError est un symptôme, pas une cause. Voici la hiérarchie des exceptions décodée, chaque traceback associé à sa véritable solution, et une fonction de vérification de l'état qui classe les échecs et tourne autour des sorties mortes.

requests.exceptions.ProxyError est l'un des messages d'erreur les moins utiles en Python : il se déclenche pour un proxy mort, des identifiants incorrects, un mauvais schéma, une sortie surchargée et un blocage de pare-feu, tous avec des tracebacks presque identiques. Le secret pour le corriger rapidement est de savoir que ProxyError n'est pas une cause racine — c'est une catégorie. Dans le code source de Requests, ProxyError, SSLError et ConnectTimeout sont tous des sous-classes de ConnectionError, et chacun se déclenche à une étape spécifique du cycle de vie de la requête. Lire l'étape, c'est lire la cause. Ce guide décode la hiérarchie des exceptions, associe chaque traceback courant à sa véritable solution, et vous donne une fonction de vérification de l'état qui classe les échecs et tourne automatiquement autour des sorties mourantes.

La hiérarchie des exceptions requests

Chaque erreur liée aux proxies dans Requests descend de RequestException. La branche utile pour le débogage est ConnectionError, car les trois exceptions que vous rencontrez réellement vivent toutes sous elle :

Parce que les trois premiers partagent un parent, un except requests.exceptions.ConnectionError les attrape tous pour la logique de réessai — tandis qu'attraper les sous-classes individuellement vous permet de consigner pourquoi chacune a échoué. La configuration propre qui évite la plupart de ces erreurs est couverte dans notre guide des proxies Python Requests ; cet article traite de ce qu'il faut faire une fois que le traceback est déjà à l'écran.

Une habitude économise plus de temps qu'une seule correction : lire le traceback de bas en haut. Requests enveloppe l'échec sous-jacent de urllib3, donc les cadres supérieurs décrivent l'appel a été fait et les cadres inférieurs décrivent ce qui a mal tourné. La ligne que vous voulez est la clause Caused by la plus intérieure — elle nomme l'échec concret (une connexion refusée, un décalage de certificat, une erreur de port analysé) que l'ProxyError ou ConnectionError externe ne fait que relancer. Une fois que vous pouvez lire cette ligne, le reste de ce guide est une table de consultation.

La classique ValueError avant ProxyError

Le traceback de proxy le plus recherché n'est même pas un ProxyError — c'est ValueError: invalid literal for int() with base 10, lancé profondément à l'intérieur de urllib3. Cela se produit lorsque vous intégrez des identifiants dans la valeur du proxy sans schéma, donc l'analyseur lit le texte après le deux-points comme un numéro de port :

# 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",
}
Diagramme de flux montrant où chaque exception Python requests se déclenche à travers le cycle de vie de la requête : MissingSchema, ProxyError, SSLError et ReadTimeout
Le nom de l'étape du cycle de vie indique la cause : une faute de frappe à la construction est MissingSchema, une connexion proxy échouée est ProxyError, un échec TLS est SSLError, une sortie lente est ReadTimeout.

Un contrôle de santé du proxy qui classe les échecs

Au lieu de deviner, attrapez chaque type d'exception et transformez-le en un verdict en anglais simple. Cette fonction renvoie l'IP de sortie en cas de succès et une raison étiquetée en cas d'échec — placez-la devant toute extraction pour confirmer que le proxy est vivant avant de brûler des requêtes dessus. Elle fonctionne contre toute passerelle authentifiée, y compris les proxies résidentiels :

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)

Quand curl fonctionne mais que Python lance ProxyError

Si les mêmes identifiants réussissent dans curl mais lèvent ProxyError en Python, une variable d'environnement remplace presque toujours votre dictionnaire. Requests lit HTTP_PROXY, HTTPS_PROXY et NO_PROXY depuis le shell, et une valeur d'entreprise obsolète redirige silencieusement chaque appel. Imprimez session.proxies pour voir ce qui est réellement utilisé, puis désactivez complètement la recherche d'environnement :

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 qui survit aux identifiants corrects pointe vers un plan de liste blanche IP appelé depuis une adresse non enregistrée — la liste complète des causes est dans notre guide 407 Proxy Authentication Required.

ProxyError intermittent : tournez, ne redémarrez pas

Le cas frustrant est un code qui fonctionne pendant vingt minutes, puis lance ProxyError, puis fonctionne à nouveau. Ce n'est pas un bug dans votre script — c'est une seule IP de sortie qui meurt ou est limitée en cours d'exécution. La solution est de réessayer avec rotation : enveloppez l'appel, attrapez ConnectionError, et laissez une passerelle rotative vous fournir une nouvelle IP à la prochaine tentative. À travers les proxies rotatifs chaque réessai passe par une sortie différente, donc une adresse morte ne peut jamais échouer deux fois une requête :

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}")

Si ProxyError persiste à travers de nombreuses nouvelles IP, le problème est passé de votre pool à la cible : vous êtes bloqué, pas déconnecté. C'est un autre combat — consultez la checklist anti-ban pour le rythme, les en-têtes et l'hygiène des sessions.

Il y a une autre distinction qui vaut la peine d'être internalisée, car elle change la façon dont vous répondez. Un ProxyError ou ConnectTimeout signifie que la requête n'a jamais été complétée, donc la réessayer est sûr même pour un POST — rien ne s'est passé de l'autre côté. Un ReadTimeout, en revanche, signifie que la cible a reçu votre requête et a simplement mis trop de temps à répondre ; réessayer une écriture non idempotente peut entraîner une double soumission. Lorsque vous construisez la boucle de réessai, traitez les échecs de l'étape de connexion comme librement réessayables et les échecs de l'étape de lecture comme réessayables uniquement pour GET et HEAD. Cette règle unique empêche le bug subtil où un proxy instable transforme un paiement en trois.

Checklist associant les messages d'erreur proxy courants de Python requests à leurs corrections en une ligne, de impossible de se connecter au proxy à des échecs intermittents
Lisez le message, appliquez la correction : mauvais hôte, schéma manquant, mauvaise clé TLS, mot de passe non encodé ou une sortie mourante se mappent chacun à une étape corrective unique.

Questions fréquemment posées

Qu'est-ce qui cause requests.exceptions.ProxyError : impossible de se connecter au proxy ?

L'hôte ou le port du proxy est incorrect, le proxy est en panne, ou un pare-feu bloque la connexion avant que toute requête ne parte. Vérifiez le point de terminaison avec curl -x en utilisant les mêmes identifiants ; si curl échoue également, le proxy est inaccessible, et si curl réussit, une variable d'environnement ou un dictionnaire mal formé dans votre Python est le coupable.

Pourquoi ProxyError est-il enveloppé dans HTTPSConnectionPool ?

Cet emballage ne fait que nommer le pool de connexions qu'urllib3 a utilisé pour atteindre la cible — c'est du bruit autour du véritable message imbriqué à l'intérieur. Lisez la clause Caused by la plus intérieure : Cannot connect to proxy signifie un échec de connexion, tandis qu'un message 407 ou SSL à l'intérieur du même pool pointe plutôt vers un problème d'authentification ou de TLS/schéma.

Comment arrêter requests de lire les proxies depuis l'environnement ?

Définissez session.trust_env = False sur votre Session, ou passez trust_env=False équivalemment, pour que Requests ignore HTTP_PROXY et HTTPS_PROXY. C'est la solution lorsque un proxy fonctionne dans un shell mais lance ProxyError dans un autre, ou lorsqu'une variable d'entreprise détourne un scraper que vous n'avez pas configuré pour utiliser un proxy.

Est-ce que ConnectTimeout est sûr à réessayer ?

Oui — la documentation de Requests marque ConnectTimeout comme sûr à réessayer parce que la requête n'a jamais atteint le serveur, donc aucun effet secondaire n'a pu se produire. Réessayez-le, idéalement à travers une passerelle rotative pour que la prochaine tentative utilise une sortie différente et plus rapide. ReadTimeout est plus risqué à réessayer aveuglément sur des méthodes non idempotentes comme POST.

Une fois que vous arrêtez de traiter ProxyError comme une faute unique et commencez à le lire par étape du cycle de vie, les corrections deviennent mécaniques : les fautes de schéma se lèvent avant le réseau, les échecs de connexion nomment un proxy mort, les erreurs SSL nomment une mauvaise clé, et les échecs intermittents veulent une rotation, pas un redémarrage. Des sorties propres les éliminent presque entièrement.

Extraire sur des IP résidentielles qui restent vivantes