Guide des proxies curl_cffi : Configuration, Authentification, Rotation et Async
curl_cffi vous offre une poignée de main TLS en forme de navigateur. Un proxy vous fournit une IP de sortie propre. Voici exactement comment les connecter ensemble — et pourquoi le préfixe https:// dans votre dictionnaire de proxies génère ErrCode 35.
curl_cffi est la liaison Python à un fork de curl-impersonate : il reproduit les empreintes TLS/JA3 et HTTP/2 d'un vrai navigateur au lieu de s'annoncer comme urllib3. Cela résout un axe de blocage. L'autre est l'IP de sortie, c'est là qu'intervient un proxy curl_cffi — et où la documentation devient mince. La section proxy officielle fait environ quinze lignes, et un problème GitHub de février 2023 figure toujours dans le top cinq pour ce sujet. Ce guide couvre toute la surface : le paramètre proxy, le dictionnaire de style requests et ses vrais noms de clés, proxy_auth, les sessions, la rotation par requête, async, SOCKS5 — et les chaînes d'erreur exactes que vous collerez dans une boîte de recherche.
Syntaxe du proxy curl_cffi : préférer proxy= au dictionnaire proxies
curl_cffi accepte deux formes. La native est une chaîne unique proxy=, ajoutée en v0.6.0 ; le dictionnaire proxies= existe pour la compatibilité avec requests, et la documentation recommande le paramètre unique sauf si vous avez vraiment besoin de proxies différents par schéma. En interne, ils se réduisent à la même chose — proxy="..." devient {"all": "..."} — et les deux fonctionnent sur les aides du module, sur Session, sur AsyncSession et sur les requêtes individuelles.
# pip install curl_cffi --upgrade (Python 3.10+ since v0.14)
import curl_cffi
PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"
# Native form — one string, applies to every scheme
r = curl_cffi.get(
"https://tls.browserleaks.com/json",
impersonate="chrome",
proxy=PROXY,
timeout=30,
)
print(r.status_code, r.json()["ja3n_hash"])
# requests-compatible form
r = curl_cffi.get(
"https://httpbin.org/ip",
impersonate="chrome",
proxies={"http": PROXY, "https": PROXY},
timeout=30,
)
print(r.json()) # {'origin': '<proxy exit IP>'}
Quatre choses à savoir sur ce dictionnaire, car aucune n'est évidente à partir du README :
- Les clés valides sont
all,http,https,wsetwss.allest le fourre-tout ; les clés websocket ne comptent que pour le client WebSocket. - Les clés par hôte fonctionnent aussi.
https://api.example.comouall://example.comroutent juste cet hôte via un proxy donné — utile pour envoyer un domaine difficile via des IP résidentielles et laisser le reste direct. - Vous ne pouvez pas passer les deux.
proxy=plusproxies=sur le même appel génèreTypeError: Cannot specify both 'proxy' and 'proxies', et la même vérification s'exécute au niveau de la session. - Les variables d'environnement sont respectées —
http_proxy,https_proxy,ws_proxy,wss_proxy. Passeztrust_env=Falseà uneSessionlorsqu'une variable d'entreprise détourne votre scraper.
Une note sur les imports : depuis la v0.10.0, le package est directement appelable (curl_cffi.get, curl_cffi.Session). Les anciens tutoriels utilisent from curl_cffi import requests, ce qui fonctionne toujours mais se lit mal à côté de la vraie bibliothèque requests — et explique pourquoi la moitié des extraits en ligne ressemblent à un projet différent.
Le piège https:// : ErrCode 35 et WRONG_VERSION_NUMBER
Cette seule erreur génère plus de questions sur les proxies curl_cffi que tout le reste combiné. Le problème #6 dans le suivi du projet — ouvert et fermé le même jour en février 2023 — figure toujours sur la première page, car l'erreur qu'il produit ressemble à un bug TLS plutôt qu'à une faute de configuration :
# WRONG: this asks curl to open a TLS connection *to the proxy itself*
proxies = {"https": "https://USER:PASS@gate.quantumproxies.io:PORT"}
# Failed to perform, ErrCode: 35, Reason:
# 'error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER'
# RIGHT: plain HTTP CONNECT, then the TLS tunnel runs through to the target
proxies = {"https": "http://USER:PASS@gate.quantumproxies.io:PORT"}
# Or skip the dict entirely
proxy = "http://USER:PASS@gate.quantumproxies.io:PORT"
La clé nomme le protocole de la cible ; la valeur nomme comment vous atteignez le proxy. Un proxy HTTPS-sur-HTTP normal prend un CONNECT en texte clair, puis tunnelise votre trafic chiffré sans le toucher — donc l'URL du proxy commence par http:// même lorsque chaque URL que vous récupérez est HTTPS. Les proxies HTTPS-sur-HTTPS existent mais sont rares et doivent être explicitement pris en charge par la passerelle. Requests formule l'échec de manière bien plus utile — votre proxy semble n'utiliser que HTTP et non HTTPS — ce qui explique pourquoi une configuration identique peut ressembler à un bug spécifique à curl_cffi. Les versions récentes avertissent et lient le problème #6, mais ce n'est qu'un avertissement : la requête échoue toujours.
Authentification : Identifiants URL ou proxy_auth
Les passerelles authentifiées acceptent la forme intégrée habituelle, http://USER:PASS@host:port, avec le piège habituel : un @, : ou / non échappé dans le mot de passe divise l'URL au mauvais endroit et produit un échec d'authentification qui ressemble à un proxy mort. curl_cffi offre une échappatoire que requests n'a pas — un tuple proxy_auth passé à libcurl comme options de nom d'utilisateur et de mot de passe séparées, donc aucun encodage n'est impliqué.
import curl_cffi
from urllib.parse import quote
# Option A — credentials in the URL, password URL-encoded
pw = quote("p@ss:word", safe="")
r = curl_cffi.get(
"https://httpbin.org/ip",
proxy=f"http://USER:{pw}@gate.quantumproxies.io:PORT",
impersonate="chrome",
timeout=30,
)
# Option B — keep credentials out of the URL entirely
r = curl_cffi.get(
"https://httpbin.org/ip",
proxy="http://gate.quantumproxies.io:PORT",
proxy_auth=("USER", "PASS"),
impersonate="chrome",
timeout=30,
)
print(r.json())
Une troisième option élimine entièrement cette classe de bug : la liste blanche d'IP. Chaque plan résidentiel QuantumProxies vous permet d'autoriser l'IP de votre serveur au lieu d'envoyer user:pass, donc l'URL du proxy devient un simple http://gate.quantumproxies.io:PORT — rien à encoder, aucun secret dans votre arbre source. Si les identifiants eux-mêmes sont rejetés, notre guide sur chaque cause de 407 Proxy Authentication Required couvre le reste.

Sessions, cookies et le détail de la réutilisation des identifiants
Une Session conserve les cookies, le pool de connexions et vos paramètres par défaut en un seul endroit, ce qui est ce que vous voulez pour tout ce qui est multi-étapes. Définissez impersonate et proxy une fois et chaque requête les hérite :
from curl_cffi import Session
with Session(
impersonate="chrome",
proxy="http://USER-session-a1b2:PASS@gate.quantumproxies.io:PORT",
timeout=30,
retry=3,
) as s:
s.get("https://httpbin.org/cookies/set/foo/bar")
r = s.get("https://httpbin.org/cookies")
print(r.json(), s.cookies.get_dict())
Deux comportements méritent d'être mentionnés. Premièrement, chaque fois qu'un proxy est configuré, curl_cffi active l'option proxy-credential-no-reuse de libcurl : une nouvelle connexion est forcée lorsque le nom d'utilisateur du proxy change, et le cache de session TLS est indexé sur l'adresse du proxy, donc une IP de sortie précédente ne peut pas fuir dans une requête ultérieure via une session réutilisée. Si vous encodez des ID de session persistante dans le nom d'utilisateur, comme le font la plupart des passerelles rotatives, vous obtenez cette isolation gratuitement. Deuxièmement, retry (un entier, ou une RetryStrategy de curl_cffi.requests avec délai, backoff et jitter) ne se relance que sur une exception de transport. Il ne réessaie pas un 403 ou 429 comme le fait status_forcelist d'urllib3 — cette boucle est toujours à écrire. Les documents de compatibilité listent les réessais comme non pris en charge, ce qui est obsolète : le paramètre est arrivé en v0.15.0.
Pointez curl_cffi vers une passerelle résidentielle rotative
Rotation par requête et async
curl_cffi annonce asyncio avec rotation de proxy à chaque requête, et c'est littéral : un argument proxy= sur un appel individuel remplace ce que la session contient. Vous avez rarement besoin d'une liste de proxies pour l'exploiter — une passerelle rotative attribue un nouveau serveur de sortie côté serveur à chaque connexion, donc un seul point de terminaison plus la concurrence est déjà une rotation. Là où vous voulez du contrôle (une IP stable par travailleur, par compte, par panier), mettez un jeton de session dans le nom d'utilisateur et laissez la passerelle épingler cette sortie.
import asyncio
from curl_cffi import AsyncSession
GATE = "gate.quantumproxies.io:PORT"
URLS = ["https://httpbin.org/ip"] * 20
async def fetch(session, url, worker):
# one sticky exit IP per worker; drop the -session- suffix for full rotation
proxy = f"http://USER-session-{worker}:PASS@{GATE}"
r = await session.get(url, proxy=proxy, timeout=30)
return r.status_code, r.json()["origin"]
async def main():
async with AsyncSession(impersonate="chrome", max_clients=10) as s:
return await asyncio.gather(
*(fetch(s, u, i % 5) for i, u in enumerate(URLS))
)
for status, ip in asyncio.run(main()):
print(status, ip)
max_clients limite les poignées curl simultanées dans le pool (10 par défaut), donc c'est votre véritable réglage de concurrence — ajouter un sémaphore à un rassemblement non borné est l'erreur habituelle. La même logique de dimensionnement s'applique à tout client async, que nous avons couvert dans le scraping Python async avec httpx et aiohttp. Que ce soit pour tourner par requête ou épingler une session dépend de si le site suit l'état à travers les requêtes ; les compromis sont dans sessions persistantes vs proxies rotatifs.

SOCKS5, HTTP/3 et les interrupteurs de sécurité
SOCKS n'a besoin d'aucune installation supplémentaire — libcurl est compilé, donc contrairement à requests, il n'y a pas de [socks] extra à retenir. Utilisez socks5h://USER:PASS@gate.quantumproxies.io:PORT : le h pousse la résolution DNS vers le proxy, ce qui empêche les fuites de votre propre réseau et résout les noms d'hôte géo-clôturés depuis l'emplacement de sortie. curl_cffi détecte le préfixe socks et saute le drapeau de tunnelisation HTTP, puisque le protocole SOCKS gère cela lui-même. Chaque plan ici expose des points de terminaison HTTP et SOCKS5 sur la même passerelle, donc le changement est un échange de schéma plutôt qu'une nouvelle commande.
- HTTP/3 sur un proxy est arrivé en v0.15.0 aux côtés des empreintes http/3, mais il nécessite un serveur SOCKS5 qui parle UDP, pas une passerelle HTTP ordinaire. Niche jusqu'à ce que votre cible récompense QUIC.
- Renforcement SSRF. La même version a publié un avis : si vous récupérez des URL fournies par d'autres personnes, les redirections peuvent être dirigées vers votre réseau interne. Réglez
allow_redirects="safe", ou désactivez les redirections. - Débogage. La v0.15 a expédié une CLI :
curl-cffi get tls.browserleaks.com/json --impersonate chromevous dit en une ligne si l'imitation atterrit, avant de blâmer le proxy.
Quand curl_cffi plus un proxy suffit
Plus souvent que les gens ne le pensent. Si la cible sert du JSON à partir d'une API interne ou du HTML rendu par le serveur, et que le seul obstacle est une vérification d'empreinte, une poignée de main assortie plus une sortie résidentielle le résout à une fraction du coût et de la latence d'un navigateur. La FAQ du projet est franche sur le plafond : les empreintes sont un facteur parmi plusieurs, aux côtés de la qualité de l'IP, du taux de requêtes et des vérifications JavaScript, et les niveaux de protection plus élevés nécessitent à la fois un meilleur pool de proxies et une véritable automatisation de navigateur. Lorsque l'imitation est correctement configurée et que vous êtes toujours bloqué, la variable restante est presque toujours l'IP de sortie — isoler cela en cinq minutes est le sujet de curl_cffi vs requests. Si vous préférez ne pas en utiliser, l'API Scraper gère les empreintes, les proxies et le rendu JS optionnel derrière un seul appel.
Questions fréquemment posées
Comment utiliser un proxy avec curl_cffi ?
Passez proxy="http://USER:PASS@host:port" à toute méthode de requête, session ou session async. Le dictionnaire de style requests proxies={"http": ..., "https": ...} fonctionne également, mais le projet recommande le paramètre unique sauf si vous avez besoin de proxies différents par schéma. Passer les deux génère un TypeError.
Pourquoi curl_cffi génère-t-il ErrCode 35 WRONG_VERSION_NUMBER ?
Parce que l'URL du proxy commence par https://. Un proxy standard s'attend à une requête CONNECT en texte clair, puis tunnelise votre TLS ; un préfixe https:// fait que curl essaie de faire une poignée de main TLS avec le proxy lui-même, qui répond en HTTP simple. Changez la valeur en http:// — la clé https se réfère à la cible, pas au saut.
curl_cffi prend-il en charge les proxies SOCKS5 ?
Oui, nativement — libcurl est inclus, donc il n'y a pas d'extra optionnel à installer. Utilisez le schéma socks5h:// pour que les noms d'hôte soient résolus par le proxy plutôt que par votre machine. SOCKS4, SOCKS4a et socks5:// simple sont également acceptés ; la bibliothèque saute la tunnelisation HTTP pour tout proxy dont le schéma commence par socks.
curl_cffi peut-il faire tourner les proxies à chaque requête ?
Oui. Un argument proxy= sur un appel individuel remplace le défaut de session, y compris à l'intérieur d'une AsyncSession, ce que le README signifie par asyncio avec rotation par requête. Avec une passerelle rotative, vous n'avez souvent besoin d'aucune logique : le même point de terminaison distribue une IP de sortie différente par connexion.
curl_cffi peut-il contourner Cloudflare ?
Parfois. Il supprime l'indice d'empreinte TLS et HTTP/2, ce qui est suffisant pour les niveaux de protection de base. Il ne peut pas exécuter de défis JavaScript, résoudre Turnstile, ou réparer une IP de centre de données qu'une base de données de réputation a déjà signalée. Considérez l'imitation comme l'un des trois besoins, pas la réponse.
Toute la configuration est plus petite que sa réputation : une chaîne proxy, impersonate définie une fois sur la session, un délai d'attente à chaque appel, et des identifiants soit encodés dans l'URL soit passés comme un tuple proxy_auth. Obtenez ces éléments correctement et la variable restante est la qualité de l'IP — une poignée de main Chrome parfaite depuis une adresse de centre de données signalée reste une adresse de centre de données signalée. Notre décomposition de l'empreinte JA3 et JA4 explique pourquoi les deux vérifications sont indépendantes.
Obtenez des IP résidentielles qui correspondent à votre imitation