Geonode logo
Geonode Team

Geonode Team

Mis à jour : 16 octobre 2026

Publié : 16 octobre 2026

Configuration du proxy aiohttp : sessions, authentification et limites de connexion

aiohttp est le client à choisir lorsqu’un scraper nécessite des centaines de requêtes en cours d’exécution et que vous souhaitez un contrôle total sur la boucle d’événements. Le faire passer par un proxy ne nécessite que quelques arguments-clés, mais les valeurs par défaut concernant l’authentification, les limites de connexion et les délais d’expiration diffèrent de celles de `requests` et d’httpx, et c’est là que la plupart des configurations de proxy aiohttp échouent. À la fin de ce guide, vous disposerez d’un `ClientSession` capable de s’authentifier auprès d’une passerelle résidentielle à rotation, d’exécuter des requêtes parallèles limitées, d’appliquer des délais d’expiration prévisibles et de signaler les erreurs de proxy par type. Les exemples ciblent aiohttp 3.14 sous Python 3.11 ou une version plus récente. Lorsque le comportement a changé dans une version récente, la version est indiquée.

aiohttp est le client à choisir lorsqu'un scraper nécessite des centaines de requêtes en cours d'exécution et que vous souhaitez disposer d'un contrôle total sur la boucle d'événements. Le faire passer par un proxy ne nécessite que quelques arguments-clés, mais les valeurs par défaut concernant l’authentification, les limites de connexion et les délais d’expiration diffèrent de celles de requests et d’httpx, et c’est là que la plupart des configurations de proxy aiohttp échouent. À la fin de ce guide, vous disposerez d’un ClientSession capable de s’authentifier auprès d’une passerelle résidentielle tournante, d’exécuter des requêtes parallèles limitées, d’appliquer des délais d’expiration prévisibles et de signaler les erreurs de proxy par type.

Les exemples ciblent aiohttp 3.14 sous Python 3.11 ou une version plus récente. Lorsque le comportement a changé dans une version récente, la version est indiquée.

Configurer un proxy aiohttp au niveau de la session

Le mot-clé proxy est accepté aussi bien au niveau de la requête qu’au niveau de la session. Le paramétrage « par requête » correspond à l’API d’origine et fonctionne toujours ; le paramétrage « au niveau de la session » est celui qu’il vous faut pour un scraper, car il élimine tout risque qu’un appel échappe au proxy.

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def main() -> None:
    async with aiohttp.ClientSession(proxy=PROXY) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(resp.status, await resp.json())

asyncio.run(main())

Le port 9000 correspond à la passerelle HTTP(S) à rotation de Geonode ; ainsi, chaque nouvelle connexion provient d’une adresse IP résidentielle différente. Un appel individuel peut toujours remplacer la valeur par défaut avec son propre argument proxy=, ce qui est pratique lorsqu’une requête doit être acheminée directement, par exemple vers un point de terminaison de métriques interne.

Forme par requête

async with aiohttp.ClientSession() as session:
    async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as resp:
        print(await resp.json())

Utilisez cette option lorsque le proxy varie d’un appel à l’autre, par exemple lorsqu’un gestionnaire de pool attribue une session « sticky » différente pour chaque tâche.

Authentification : identifiants dans l'URL ou proxy_headers

Il existe deux méthodes prises en charge pour envoyer les identifiants du proxy, ainsi qu'une méthode obsolète que vous trouverez encore dans d'anciens tutoriels.

MéthodeExempleStatut
Identifiants dans l'URL du proxyproxy="http://user:pass@host:9000"Prise en charge
proxy_headers avec encode_basic_authproxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}Prise en charge, recommandée
proxy_auth=aiohttp.BasicAuth("user", "pass")Obsolète depuis la version 3.14, supprimée dans la version 4.0

La forme URL est la plus courte. La forme en-tête permet de ne pas inclure la clé secrète dans l’URL, ce qui est important si les URL de proxy se retrouvent dans les journaux ou les messages d’exception. aiohttp.encode_basic_auth renvoie la chaîne Basic ... prête à l’emploi :

import asyncio
import os
import aiohttp

PROXY = "http://premium-residential.geonode.com:9000"

async def main() -> None:
    proxy_headers = {
        "Proxy-Authorization": aiohttp.encode_basic_auth(
            os.environ["GEONODE_USER"], os.environ["GEONODE_PASS"]
        )
    }
    async with aiohttp.ClientSession(proxy=PROXY, proxy_headers=proxy_headers) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

Si votre base de code utilise encore proxy_auth=aiohttp.BasicAuth(...), cela fonctionne sous la version 3.14 avec un DeprecationWarning. Effectuez la migration dès maintenant : le paramètre et la classe sont supprimés dans aiohttp 4.0, et le remplacement ne nécessite qu’une modification d’une ligne.

Requêtes parallèles avec asyncio.gather et un Semaphore

La raison d'utiliser aiohttp réside dans la concurrence, et la raison pour laquelle la concurrence perturbe les proxys est qu'elle n'est limitée par rien par défaut. Avec un asyncio.gather, plus d’un millier de coroutines tenteront d’ouvrir un millier de connexions ; le connecteur limite ce nombre à 100, et les autres sont mises en file d’attente au sein d’aiohttp, ce qui est mieux que rien mais ne vous donne aucun contrôle sur le rythme. Un Semaphore rend cette limite explicite et vous permet de la régler indépendamment du connecteur.

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
CONCURRENCY = 20

async def fetch(session: aiohttp.ClientSession, sem: asyncio.Semaphore, url: str) -> tuple[str, int]:
    async with sem:
        async with session.get(url) as resp:
            await resp.read()
            return url, resp.status

async def main() -> None:
    urls = [f"https://httpbin.org/get?i={i}" for i in range(100)]
    sem = asyncio.Semaphore(CONCURRENCY)
    connector = aiohttp.TCPConnector(limit=CONCURRENCY, ttl_dns_cache=300)
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(
        proxy=PROXY, connector=connector, timeout=timeout
    ) as session:
        results = await asyncio.gather(
            *(fetch(session, sem, u) for u in urls), return_exceptions=True
        )
    ok = sum(1 for r in results if not isinstance(r, Exception))
    print(f"{ok}/{len(urls)} succeeded")

asyncio.run(main())

return_exceptions=True empêche une seule requête ayant échoué d’annuler le lot. Faites correspondre le sémaphore à la limite du connecteur afin qu’aucune des deux parties ne mette silencieusement en file d’attente des tâches que l’autre ne peut pas traiter.

TCPConnector : limit et limit_per_host

Le connecteur gère le pool de sockets. Deux paramètres sont importants pour le trafic du proxy aiohttp.

limit correspond au nombre total de connexions simultanées, dont la valeur par défaut est 100. limit_per_host limite les connexions à un triple (host, port, is_ssl) ; la valeur par défaut est 0, ce qui signifie qu’il n’y a pas de limite. Via un proxy, chaque requête vers chaque cible partage l’hôte et le port du proxy ; la limite par hôte s’applique donc à la passerelle, et non aux sites de destination. Pour une charge de travail exclusivement proxy, définissez limit sur le nombre de connexions simultanées souhaité et laissez limit_per_host à 0 ; ne définissez limit_per_host que lorsque la même session communique également directement avec les serveurs d’origine et que vous souhaitez les protéger.

connector = aiohttp.TCPConnector(limit=30, limit_per_host=10, ttl_dns_cache=300)

ttl_dns_cache=300 conserve la réponse DNS de la passerelle pendant cinq minutes au lieu des dix secondes par défaut, ce qui élimine un aller-retour vers le résolveur pour la plupart des nouvelles connexions. La session prend possession du connecteur et le ferme à la fermeture ; ne définissez connector_owner=False que si vous partagez délibérément un même connecteur entre plusieurs sessions.

ClientTimeout au lieu de la valeur par défaut de 5 minutes

Le délai d'expiration total par défaut d'aiohttp est de 300 secondes. Ce délai est généreux pour les requêtes directes, mais bien trop long pour le trafic via proxy, où un point de sortie résidentiel bloqué doit échouer rapidement afin que vous puissiez réessayer avec une autre adresse IP.

timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)

total limite la durée totale de la requête, y compris les redirections et le téléchargement du corps de la requête. connect limite le temps nécessaire à l'acquisition d'une connexion à partir du pool, ainsi qu'à la négociation TCP et proxy. sock_read limite l’intervalle entre deux octets consécutifs, ce qui permet de détecter les cibles qui acceptent la connexion puis transmettent les données au compte-gouttes. Transmettez l’objet à la session pour utiliser la valeur par défaut, ou à une requête unique pour la remplacer. L’expiration déclenche asyncio.TimeoutError, décrit ci-dessous.

trust_env et les proxys d'environnement

Contrairement à requests, aiohttp ignore les fichiers HTTP_PROXY, HTTPS_PROXY et NO_PROXY, sauf si vous activez cette option avec trust_env=True. Lorsqu’il est activé, il utilise urllib.request.getproxies(), prend en compte no_proxy et lit également les identifiants de proxy à partir de ~/.netrc s’ils sont présents.

async with aiohttp.ClientSession(trust_env=True) as session:
    async with session.get("https://api.ipify.org?format=json") as resp:
        print(await resp.json())

Pour un scraper qui doit toujours utiliser une passerelle spécifique, désactivez trust_env et configurez explicitement proxy=. Ainsi, un proxy d'entreprise sur un exécuteur CI ne pourra pas détourner le routage, et no_proxy ne pourra pas exclure discrètement une cible.

SOCKS5 avec aiohttp-socks

aiohttp ne prend pas en charge SOCKS de manière native ; par conséquent, un proxy aiohttp passant par SOCKS5 nécessite un connecteur tiers. Le paquet aiohttp-socks fournit un ProxyConnector qui remplace TCPConnector et gère la négociation SOCKS. Installez-le avec pip install aiohttp-socks. Le port rotatif SOCKS5 de Geonode est le 11000 :

import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector

SOCKS_URL = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"

async def main() -> None:
    connector = ProxyConnector.from_url(SOCKS_URL)
    async with aiohttp.ClientSession(connector=connector) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

Ne combinez pas ProxyConnector avec proxy= sur la session ; le connecteur achemine déjà tout. ProxyConnector accepte les mêmes mots-clés limit et limit_per_host que TCPConnector.

La limitation du TLS-in-TLS

aiohttp communique avec les proxys HTTP et achemine les cibles https:// via ceux-ci à l'aide de la commande CONNECT. Ce qu’il ne fait pas de manière fiable, c’est se connecter au proxy lui-même via TLS, c’est-à-dire à une URL de proxy de type https://, car le TLS-in-TLS dépend de la prise en charge du transport asyncio, qui a varié d’une version de Python à l’autre. La documentation indique clairement qu’il ne faut pas partir de ce principe. En pratique, cela n’a pas d’importance pour Geonode : la passerelle est accessible via HTTP en clair sur les ports 9000/10000 et votre trafic https:// reste chiffré de bout en bout à l’intérieur du tunnel CONNECT ; conservez donc le schéma d’URL du proxy sous la forme http://.

Sessions persistantes pour les flux en plusieurs étapes

Le port rotatif attribue une nouvelle adresse IP à chaque connexion. Pour les flux de connexion ou les listes paginées, vous avez besoin de la même adresse IP pendant quelques minutes ; c’est ce que propose Geonode sur le port persistant HTTP(S) 10000 via le nom d’utilisateur :

import secrets
import aiohttp

def sticky_proxy(country: str = "us", lifetime: int = 600) -> str:
    session_id = secrets.token_hex(4)  # 8 alphanumeric characters
    user = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
    return f"http://{user}:PASSWORD@premium-residential.geonode.com:10000"

session = aiohttp.ClientSession(proxy=sticky_proxy())

La durée de vie varie entre 180 et 86 400 secondes. Le ciblage par État et par ville ne peut pas être combiné, et asn nécessite la spécification d'un pays. La génération d'un nouvel identifiant de session permet d'effectuer la rotation à la demande.

Diagnostics : signification des exceptions

ExceptionSignificationSolution type
aiohttp.ClientProxyConnectionErrorImpossible d'ouvrir une connexion TCP vers l'hôte proxyVérifiez le nom d'hôte, le port et le pare-feu ; augmentez le délai d'expiration de connect
aiohttp.ClientHttpProxyError avec .status == 407Le proxy a rejeté les identifiants lors de la requête CONNECTCorrigez le nom d'utilisateur ou le mot de passe ; vérifiez la syntaxe de l'option de nom d'utilisateur
aiohttp.ClientHttpProxyError avec 502/503La passerelle n’a pas pu établir le tunnelRéessayez avec un nouvel identifiant de session ; vérifiez que le port correspond au protocole
asyncio.TimeoutErrorUn champ de ClientTimeout a expiréRéessayez avec une nouvelle adresse IP ; ajustez sock_read
aiohttp.ClientConnectorErrorÉchec de connexion générique (DNS, refusé)Vérifiez .os_error

ClientProxyConnectionError est une sous-classe de ClientConnectorError, et ClientHttpProxyError est une sous-classe de ClientResponseError ; traitez donc d’abord les cas spécifiques.

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def fetch_ip(session: aiohttp.ClientSession) -> str | None:
    try:
        async with session.get("https://api.ipify.org?format=json") as resp:
            resp.raise_for_status()
            return (await resp.json())["ip"]
    except aiohttp.ClientHttpProxyError as e:
        if e.status == 407:
            print("proxy auth failed: check credentials")
        else:
            print("proxy returned", e.status)
    except aiohttp.ClientProxyConnectionError as e:
        print("cannot reach proxy:", e)
    except asyncio.TimeoutError:
        print("timed out through proxy")
    return None

async def main() -> None:
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(proxy=PROXY, timeout=timeout) as session:
        print(await fetch_ip(session))
    await asyncio.sleep(0.25)  # let SSL transports close before the loop stops

asyncio.run(main())

Notez que pour une cible http:// simple, le code 407 du proxy est renvoyé sous forme de réponse ordinaire plutôt que d’exception ; vérifiez donc également resp.status dans ce cas.

Arrêt en douceur

Lorsqu'une session se ferme, asyncio peut encore être en train de fermer les transports SSL sous-jacents. Si la boucle d'événements s'arrête en premier, vous obtenez le code d'erreur ResourceWarning: unclosed transport. La solution documentée consiste à insérer une courte pause après le bloc async with de la session : await asyncio.sleep(0) pour les sessions HTTP simples, et await asyncio.sleep(0.25) lorsque la session a effectué des requêtes HTTPS, ce qui est presque toujours le cas via un tunnel CONNECT. L'exemple ci-dessus inclut cette solution.

Erreurs courantes

Création d’une session par requête

ClientSession gère le pool de connexions et le « cookie jar ». En ouvrir une par requête annule la fonctionnalité « keep-alive » vers la passerelle, multiplie les handshakes du proxy et déclenche l’avertissement de fermeture à chaque appel. Créez une seule session par worker et réutilisez-la.

Oublier que le proxy est le seul « hôte »

Si limit_per_host est défini sur une valeur faible, chaque requête proxy est comptabilisée sur l’entrée unique (host, port) de la passerelle ; ainsi, une limite de 5 par hôte restreint l’ensemble de votre scraper à 5 connexions. Définissez plutôt limit, ou laissez limit_per_host à 0.

Se fier au délai d’expiration par défaut

Cinq minutes suffisent pour qu’une sortie résidentielle bloquée occupe un emplacement de sémaphore pendant toute la durée de cette fenêtre. Transmettez toujours un ClientTimeout accompagné des valeurs connect et sock_read.

Transmission persistante de proxy_auth

Cela fonctionne actuellement avec un avertissement, mais ne fonctionnera plus à partir de la version 4.0. Remplacez-le par proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(user, pw)} ou indiquez les identifiants dans l’URL du proxy.

Étape suivante

Grâce à la session décrite ci-dessus, vous avez authentifié, délimité et rendu observable le trafic du proxy aiohttp ; il ne reste plus qu’à se pencher sur le pool par lequel il transite. Les proxys résidentiels Geonode desservent les points de terminaison utilisés ici : 2,5 millions d’adresses IP en ligne, 30 à 50 millions d’adresses IP uniques tournant chaque mois dans plus de 195 pays, avec ciblage par pays, région, ville et ASN via le nom d’utilisateur et des sessions persistantes pouvant aller jusqu’à 24 heures sur HTTP, HTTPS et SOCKS5. Les tarifs commencent à 0,27 $/Go pour les achats en volume ; la formule d’entrée de gamme est de 10 Go pour 7,92 $/mois, et un essai de 3 jours coûte 5 $ sans engagement. Les cas d’utilisation du web scraping et de la veille tarifaire montrent comment les équipes combinent cette configuration de connecteur avec une logique de réessais et de planification.

FAQ

Comment configurer un proxy aiohttp pour toute une session ?

Passez proxy= au constructeur de ClientSession : aiohttp.ClientSession(proxy="http://user:pass@host:port"). Chaque requête de cette session l'utilisera, sauf si l'appel remplace proxy= par une autre valeur. Le proxy au niveau de la session est disponible depuis la version 3.11 d'aiohttp ; dans les versions antérieures, vous devez passer proxy= à chaque appel de get() ou post().

Pourquoi proxy_auth est-il déprécié dans aiohttp et par quoi est-il remplacé ?

Depuis la version 3.14 d’aiohttp, le paramètre proxy_auth et la classe aiohttp.BasicAuth sont dépréciés et leur suppression est prévue dans la version 4.0. Il est remplacé par un simple en-tête : proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}. Celui-ci effectue le même encodage Base64 sans classe d’aide et fonctionne aussi bien au niveau de chaque requête qu’au niveau de la session.

aiohttp prend-il en charge les proxys HTTPS ou SOCKS5 ?

aiohttp communique nativement avec les proxys HTTP et utilise CONNECT pour acheminer les cibles https:// à travers ceux-ci. La connexion au proxy lui-même via TLS (TLS-in-TLS) dépend de la prise en charge d’asyncio dans votre version de Python et ne doit pas être considérée comme fiable. SOCKS4 et SOCKS5 nécessitent le package tiers aiohttp-socks et son ProxyConnector.

Combien de requêtes simultanées puis-je exécuter via un proxy aiohttp ?

TCPConnector autorise par défaut 100 connexions simultanées, sans limite par hôte. Via un proxy, chaque requête partage le même hôte et le même port ; vous devez donc configurer explicitement limit, ajouter limit_per_host si vous effectuez également des appels directs, et encapsuler les coroutines de requête dans un asyncio.Semaphore afin que les tâches mises en file d’attente n’ouvrent pas toutes des sockets en même temps.

Pourquoi ma requête aiohttp via un proxy se bloque-t-elle au lieu d’échouer ?

ClientSession a un délai d’expiration total de 5 minutes par défaut, ce qui ressemble à un blocage lorsque la sortie résidentielle est ralentie. Transmettez aiohttp.ClientTimeout(total=30, connect=10, sock_read=20) à la session ou à la requête afin que les blocages apparaissent rapidement sous la forme de asyncio.TimeoutError, puis réessayez avec un nouvel identifiant de session plutôt que d’attendre.

Quelles sont les limites pratiques du routage des scrapers via des proxys résidentiels ?

Un proxy ne fait que modifier l’adresse IP source. Il ne donne pas le droit d’ignorer le fichier robots.txt, les conditions d’utilisation du site ou les limites de débit, et il ne rend pas la collecte de données personnelles légale. Maintenez une concurrence raisonnable, réduisez la fréquence en cas de codes 429, et n’oubliez pas que les sessions persistantes Geonode durent au maximum 24 heures avant que l’adresse IP ne soit libérée.