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 :
- ProxyError — levée lorsque la connexion au proxy elle-même échoue : hôte inaccessible, mauvais port, connexion refusée ou un
407rejeté. Le message contient souventCannot connect to proxyà l'intérieur d'un wrapperHTTPSConnectionPool(...). - SSLError — le proxy s'est connecté, mais la poignée de main TLS avec la cible a échoué. Dans les configurations de proxy, le déclencheur habituel est
https://écrit à l'intérieur de la cléhttpsau lieu dehttp://. - ConnectTimeout — le proxy n'a pas répondu dans la fenêtre de connexion. Il est une sous-classe de
ConnectionErroretTimeout, et est explicitement documenté comme étant sûr à réessayer. - ReadTimeout — le proxy s'est connecté et a transmis la requête, mais la cible était trop lente à répondre. C'est un problème de qualité de cible ou de sortie, pas un bug de configuration.
- MissingSchema / InvalidProxyURL — levée avant tout appel réseau lorsque l'URL du proxy est mal formée. Ce sont de simples fautes de frappe.
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 où 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",
}

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.

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.