Comment corriger les erreurs de timeout lors de l'utilisation d'un proxy
La requête s'est figée, le script s'est arrêté avec l'erreur TimeoutError, les données n'ont pas été reçues. Une situation familière ? Les erreurs de timeout via proxy sont l'un des problèmes les plus courants lors du scraping et de l'automatisation. Analysons les causes et fournissons des solutions concrètes.
Pourquoi les erreurs de timeout se produisent
Le timeout n'est pas un seul problème, mais un symptôme. Avant de traiter, il faut comprendre la cause :
Serveur proxy lent. Un serveur surchargé ou un proxy géographiquement éloigné ajoute un délai à chaque requête. Si votre timeout est de 10 secondes et que le proxy répond en 12 secondes — erreur.
Blocage du côté du site cible. Le site peut intentionnellement « suspendre » les requêtes suspectes au lieu de les refuser explicitement. C'est une tactique contre les bots — maintenir la connexion ouverte indéfiniment.
Problèmes DNS. Le proxy doit résoudre le domaine. Si le serveur DNS du proxy est lent ou indisponible — la requête se fige à l'étape de connexion.
Configuration incorrecte des timeouts. Un seul timeout global pour tout — erreur courante. Connect timeout et read timeout sont des choses différentes et doivent être configurés séparément.
Problèmes réseau. Perte de paquets, connexion proxy instable, problèmes de routage — tout cela conduit à des timeouts.
Types de timeouts et leur configuration
La plupart des bibliothèques HTTP supportent plusieurs types de timeouts. Comprendre la différence entre eux est la clé d'une configuration correcte.
Connect timeout
Temps pour établir une connexion TCP avec le proxy et le serveur cible. Si le proxy est indisponible ou le serveur ne répond pas — ce timeout s'activera. Valeur recommandée : 5-10 secondes.
Read timeout
Temps d'attente des données après l'établissement de la connexion. Le serveur s'est connecté mais reste silencieux — le read timeout s'activera. Pour les pages ordinaires : 15-30 secondes. Pour les API lourdes : 60+ secondes.
Total timeout
Temps total pour toute la requête du début à la fin. Protection contre les connexions figées. Généralement : connect + read + marge.
Exemple de configuration en Python avec la bibliothèque requests :
import requests
proxies = {
"http": "http://user:pass@proxy.example.com:8080",
"https": "http://user:pass@proxy.example.com:8080"
}
# Tuple : (connect_timeout, read_timeout)
timeout = (10, 30)
try:
response = requests.get(
"https://target-site.com/api/data",
proxies=proxies,
timeout=timeout
)
except requests.exceptions.ConnectTimeout:
print("Impossible de se connecter au proxy ou au serveur")
except requests.exceptions.ReadTimeout:
print("Le serveur n'a pas envoyé les données à temps")
Pour aiohttp (Python asynchrone) :
import aiohttp
import asyncio
async def fetch_with_timeout():
timeout = aiohttp.ClientTimeout(
total=60, # Timeout total
connect=10, # Pour la connexion
sock_read=30 # Pour la lecture des données
)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(
"https://target-site.com/api/data",
proxy="http://user:pass@proxy.example.com:8080"
) as response:
return await response.text()
Logique de retry : la bonne approche
Le timeout n'est pas toujours une erreur fatale. Souvent, une requête répétée réussit. Mais le retry doit être fait intelligemment.
Délai exponentiel
Ne bombardez pas le serveur avec des requêtes répétées sans pause. Utilisez un backoff exponentiel : chaque tentative suivante — avec un délai croissant.
import requests
import time
import random
def fetch_with_retry(url, proxies, max_retries=3):
"""Requête avec retry et délai exponentiel"""
for attempt in range(max_retries):
try:
response = requests.get(
url,
proxies=proxies,
timeout=(10, 30)
)
response.raise_for_status()
return response
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError) as e:
if attempt == max_retries - 1:
raise # Dernière tentative — on relève l'erreur
# Délai exponentiel : 1s, 2s, 4s...
# + jitter aléatoire pour éviter les vagues de requêtes
delay = (2 ** attempt) + random.uniform(0, 1)
print(f"Tentative {attempt + 1} échouée : {e}")
print(f"Nouvelle tentative dans {delay:.1f} secondes...")
time.sleep(delay)
Bibliothèque tenacity
Pour le code en production, il est plus pratique d'utiliser des solutions prêtes à l'emploi :
from tenacity import retry, stop_after_attempt, wait_exponential
from tenacity import retry_if_exception_type
import requests
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((
requests.exceptions.Timeout,
requests.exceptions.ConnectionError
))
)
def fetch_data(url, proxies):
response = requests.get(url, proxies=proxies, timeout=(10, 30))
response.raise_for_status()
return response.json()
Rotation de proxy lors de timeouts
Si un proxy donne constamment des timeouts — le problème vient de lui. Solution logique : basculer sur un autre.
import requests
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
import time
@dataclass
class ProxyManager:
"""Gestionnaire de proxy avec suivi des tentatives échouées"""
proxies: list
max_failures: int = 3
cooldown_seconds: int = 300
_failures: dict = field(default_factory=dict)
_cooldown_until: dict = field(default_factory=dict)
def get_proxy(self) -> Optional[str]:
"""Obtenir un proxy fonctionnel"""
current_time = time.time()
for proxy in self.proxies:
# Ignorer les proxies en cooldown
if self._cooldown_until.get(proxy, 0) > current_time:
continue
return proxy
return None # Tous les proxies sont en cooldown
def report_failure(self, proxy: str):
"""Signaler une requête échouée"""
self._failures[proxy] = self._failures.get(proxy, 0) + 1
if self._failures[proxy] >= self.max_failures:
# Mettre le proxy en cooldown
self._cooldown_until[proxy] = time.time() + self.cooldown_seconds
self._failures[proxy] = 0
print(f"Proxy {proxy} mis en cooldown")
def report_success(self, proxy: str):
"""Réinitialiser le compteur d'erreurs en cas de succès"""
self._failures[proxy] = 0
def fetch_with_rotation(url, proxy_manager, max_attempts=5):
"""Requête avec changement automatique de proxy en cas d'erreur"""
for attempt in range(max_attempts):
proxy = proxy_manager.get_proxy()
if not proxy:
raise Exception("Aucun proxy disponible")
proxies = {"http": proxy, "https": proxy}
try:
response = requests.get(url, proxies=proxies, timeout=(10, 30))
response.raise_for_status()
proxy_manager.report_success(proxy)
return response
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError):
proxy_manager.report_failure(proxy)
print(f"Timeout via {proxy}, essai d'un autre...")
continue
raise Exception(f"Impossible d'obtenir les données après {max_attempts} tentatives")
Lors de l'utilisation de proxies résidentiels avec rotation automatique, cette logique est simplifiée — le fournisseur bascule automatiquement l'IP à chaque requête ou selon l'intervalle spécifié.
Requêtes asynchrones avec contrôle des timeouts
Lors du scraping massif, les requêtes synchrones sont inefficaces. L'approche asynchrone permet de traiter des centaines d'URL en parallèle, mais nécessite une gestion prudente des timeouts.
import aiohttp
import asyncio
from typing import List, Tuple
async def fetch_one(
session: aiohttp.ClientSession,
url: str,
semaphore: asyncio.Semaphore
) -> Tuple[str, str | None, str | None]:
"""Chargement d'une URL avec gestion du timeout"""
async with semaphore: # Limiter la parallélité
try:
async with session.get(url) as response:
content = await response.text()
return (url, content, None)
except asyncio.TimeoutError:
return (url, None, "timeout")
except aiohttp.ClientError as e:
return (url, None, str(e))
async def fetch_all(
urls: List[str],
proxy: str,
max_concurrent: int = 10
) -> List[Tuple[str, str | None, str | None]]:
"""Chargement massif avec contrôle des timeouts et de la parallélité"""
timeout = aiohttp.ClientTimeout(total=45, connect=10, sock_read=30)
semaphore = asyncio.Semaphore(max_concurrent)
connector = aiohttp.TCPConnector(
limit=max_concurrent,
limit_per_host=5 # Pas plus de 5 connexions par hôte
)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector
) as session:
# Définir le proxy pour toutes les requêtes
tasks = [
fetch_one(session, url, semaphore)
for url in urls
]
results = await asyncio.gather(*tasks)
# Statistiques
success = sum(1 for _, content, _ in results if content)
timeouts = sum(1 for _, _, error in results if error == "timeout")
print(f"Succès : {success}, Timeouts : {timeouts}")
return results
# Utilisation
async def main():
urls = [f"https://example.com/page/{i}" for i in range(100)]
results = await fetch_all(
urls,
proxy="http://user:pass@proxy.example.com:8080",
max_concurrent=10
)
asyncio.run(main())
Important : Ne définissez pas une parallélité trop élevée. 50-100 requêtes simultanées via un seul proxy — c'est déjà beaucoup. Mieux vaut 10-20 avec plusieurs proxies.
Diagnostic : comment trouver la cause
Avant de modifier les paramètres, identifiez la source du problème.
Étape 1 : Vérifiez le proxy directement
# Test simple via curl avec mesure du temps
curl -x http://user:pass@proxy:8080 \
-w "Connect: %{time_connect}s\nTotal: %{time_total}s\n" \
-o /dev/null -s \
https://httpbin.org/get
Si time_connect dépasse 5 secondes — le problème vient du proxy ou du réseau jusqu'à lui.
Étape 2 : Comparez avec une requête directe
import requests
import time
def measure_request(url, proxies=None):
start = time.time()
try:
r = requests.get(url, proxies=proxies, timeout=30)
elapsed = time.time() - start
return f"OK: {elapsed:.2f}s, status: {r.status_code}"
except Exception as e:
elapsed = time.time() - start
return f"FAIL: {elapsed:.2f}s, error: {type(e).__name__}"
url = "https://target-site.com"
proxy = {"http": "http://proxy:8080", "https": "http://proxy:8080"}
print("Directement:", measure_request(url))
print("Via proxy:", measure_request(url, proxy))
Étape 3 : Testez différents types de proxy
Les timeouts peuvent dépendre du type de proxy :
| Type de proxy | Délai typique | Timeout recommandé |
|---|---|---|
| Datacenter | 50-200 ms | Connect: 5s, Read: 15s |
| Résidentiel | 200-800 ms | Connect: 10s, Read: 30s |
| Mobile | 300-1500 ms | Connect: 15s, Read: 45s |
Étape 4 : Enregistrez les détails
import logging
import requests
from requests.adapters import HTTPAdapter
# Activer l'enregistrement de débogage
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)
# Vous verrez maintenant toutes les étapes de la requête :
# - Résolution DNS
# - Établissement de la connexion
# - Envoi de la requête
# - Réception de la réponse
Liste de contrôle pour résoudre les erreurs de timeout
Algorithme rapide d'action en cas de timeout :
- Identifiez le type de timeout — connect ou read ? Ce sont des problèmes différents.
- Vérifiez le proxy séparément — fonctionne-t-il ? Quel est le délai ?
- Augmentez les timeouts — les valeurs sont peut-être trop agressives pour votre type de proxy.
- Ajoutez un retry avec backoff — les timeouts uniques sont normaux, l'important est la résilience.
- Configurez la rotation — basculez automatiquement sur un autre proxy en cas de problème.
- Limitez la parallélité — trop de requêtes simultanées surchargent le proxy.
- Vérifiez le site cible — il se peut qu'il bloque ou limite vos requêtes.
Conclusion
Les erreurs de timeout via proxy sont un problème résolvable. Dans la plupart des cas, il suffit de configurer correctement les timeouts selon le type de proxy, d'ajouter une logique de retry et de mettre en place une rotation en cas de défaillance. Pour les tâches avec des exigences élevées de stabilité, utilisez des proxies résidentiels avec rotation automatique — plus de détails sur proxycove.com.