Guía de Proxy curl_cffi: Configuración, Autenticación, Rotación y Async
curl_cffi te ofrece un apretón de manos TLS con forma de navegador. Un proxy te proporciona una IP de salida limpia. Aquí te explicamos exactamente cómo conectar ambos — y por qué el prefijo https:// en tu diccionario de proxies está generando ErrCode 35.
curl_cffi es el enlace de Python a un fork de curl-impersonate: reproduce las huellas digitales TLS/JA3 y HTTP/2 de un navegador real en lugar de anunciarse como urllib3. Eso soluciona un eje de bloqueo. El otro es la IP de salida, que es donde entra un proxy curl_cffi — y donde la documentación se vuelve escasa. La sección oficial de proxy tiene unas quince líneas, y un problema de GitHub de febrero de 2023 todavía está entre los cinco primeros para este tema. Esta guía cubre toda la superficie: el parámetro proxy, el diccionario estilo requests y sus nombres de claves reales, proxy_auth, sesiones, rotación por solicitud, async, SOCKS5 — y las cadenas de error exactas que pegarás en un cuadro de búsqueda.
Sintaxis de proxy curl_cffi: preferir proxy= sobre el diccionario proxies
curl_cffi acepta dos formas. La nativa es una sola cadena proxy=, añadida en la v0.6.0; el diccionario proxies= existe para compatibilidad con requests, y la documentación recomienda el único parámetro a menos que realmente necesites diferentes proxies por esquema. Internamente colapsan a lo mismo — proxy="..." se convierte en {"all": "..."} — y ambos funcionan en los ayudantes del módulo, en Session, en AsyncSession y en solicitudes individuales.
# 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>'}
Cuatro cosas sobre ese diccionario que vale la pena saber, porque ninguna de ellas es obvia desde el README:
- Las claves válidas son
all,http,https,wsywss.alles el comodín; las claves de websocket solo importan para el cliente WebSocket. - Las claves por host también funcionan.
https://api.example.comoall://example.comenrutan solo ese host a través de un proxy dado — útil para enviar un dominio difícil a través de IPs residenciales y dejar el resto directo. - No puedes pasar ambos.
proxy=másproxies=en la misma llamada generaTypeError: Cannot specify both 'proxy' and 'proxies', y la misma verificación se ejecuta a nivel de sesión. - Se respetan las variables de entorno —
http_proxy,https_proxy,ws_proxy,wss_proxy. Pasatrust_env=Falsea unaSessioncuando una variable corporativa secuestra tu scraper.
Una nota sobre las importaciones: desde la v0.10.0 el paquete es invocable directamente (curl_cffi.get, curl_cffi.Session). Los tutoriales más antiguos usan from curl_cffi import requests, lo cual aún funciona pero se lee mal junto a la biblioteca requests real — y explica por qué la mitad de los fragmentos en línea parecen de un proyecto diferente.
La trampa https://: ErrCode 35 y WRONG_VERSION_NUMBER
Este único error genera más preguntas sobre proxy curl_cffi que todo lo demás combinado. El problema #6 en el rastreador de proyectos — abierto y cerrado el mismo día en febrero de 2023 — todavía está en la primera página, porque el error que produce parece un error de TLS en lugar de un error de configuración:
# 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 clave nombra el protocolo del objetivo; el valor nombra cómo llegas al proxy. Un proxy HTTPS sobre HTTP normal toma un CONNECT en texto plano, luego tunela tu tráfico cifrado sin tocarlo — por lo que la URL del proxy comienza con http:// incluso cuando cada URL que obtienes es HTTPS. Los proxies HTTPS sobre HTTPS existen pero son raros y deben ser explícitamente soportados por la puerta de enlace. Requests formula el mismo fallo de manera mucho más útil — tu proxy parece usar solo HTTP y no HTTPS — por lo que una configuración idéntica puede parecer un error específico de curl_cffi. Las versiones recientes advierten y enlazan el problema #6, pero es solo una advertencia: la solicitud aún falla.
Autenticación: credenciales en la URL o proxy_auth
Las puertas de enlace autenticadas aceptan la forma incrustada habitual, http://USER:PASS@host:port, con la trampa habitual: un @, : o / sin escapar en la contraseña divide la URL en el lugar equivocado y produce un fallo de autenticación que parece un proxy muerto. curl_cffi ofrece una salida que requests no tiene — una tupla proxy_auth pasada a libcurl como opciones de nombre de usuario y contraseña separadas, por lo que no se involucra ninguna codificación.
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())
Una tercera opción elimina por completo esta clase de error: la lista blanca de IP. Cada plan residencial de QuantumProxies te permite autorizar la IP de tu servidor en lugar de enviar usuario:contraseña, por lo que la URL del proxy se convierte en un simple http://gate.quantumproxies.io:PORT — nada que codificar, ningún secreto en tu árbol de código fuente. Si las credenciales en sí están siendo rechazadas, nuestra guía sobre todas las causas de 407 Proxy Authentication Required cubre el resto.

Sesiones, cookies y el detalle de reutilización de credenciales
Una Session mantiene cookies, agrupación de conexiones y tus valores predeterminados en un solo lugar, que es lo que deseas para cualquier cosa de varios pasos. Establece impersonate y proxy una vez y cada solicitud los hereda:
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())
Dos comportamientos merecen mención. Primero, siempre que se configura un proxy, curl_cffi activa la opción de no reutilización de credenciales de proxy de libcurl: se fuerza una nueva conexión cuando cambia el nombre de usuario del proxy, y la caché de sesión TLS se basa en la dirección del proxy, por lo que una IP de salida anterior no puede filtrarse en una solicitud posterior a través de una sesión reutilizada. Si codificas identificadores de sesión persistente en el nombre de usuario, como hacen la mayoría de las puertas de enlace rotativas, obtienes ese aislamiento gratis. Segundo, retry (un int, o una RetryStrategy de curl_cffi.requests con retraso, retroceso y variación) solo se vuelve a ejecutar en una excepción de transporte. No reintenta un 403 o 429 como lo hace el status_forcelist de urllib3 — ese bucle aún es tuyo para escribir. Los documentos de compatibilidad enumeran los reintentos como no soportados, lo cual está desactualizado: el parámetro llegó en la v0.15.0.
Apunta curl_cffi a una puerta de enlace residencial rotativa
Rotación por solicitud y async
curl_cffi anuncia asyncio con rotación de proxy en cada solicitud, y eso es literal: un argumento proxy= en una llamada individual anula lo que la sesión mantiene. Rara vez necesitas una lista de proxies para explotarlo — una puerta de enlace rotativa asigna un servidor de salida fresco del lado del servidor en cada conexión, por lo que un solo endpoint más concurrencia ya es rotación. Donde sí quieres control (una IP estable por trabajador, por cuenta, por carrito) coloca un token de sesión en el nombre de usuario y deja que la puerta de enlace fije esa salida.
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 limita los manejadores de curl concurrentes en el pool (10 por defecto), por lo que es tu dial de concurrencia real — añadir un semáforo a un gather sin límites es el error habitual. La misma lógica de dimensionamiento se aplica a cualquier cliente async, que cubrimos en scraping async en Python con httpx y aiohttp. Si rotar por solicitud o fijar una sesión depende de si el sitio rastrea el estado a través de solicitudes; los compromisos están en sesiones persistentes vs proxies rotativos.

SOCKS5, HTTP/3 y los interruptores de seguridad
SOCKS no necesita instalación extra — libcurl está compilado, por lo que a diferencia de requests no hay un [socks] extra para recordar. Usa socks5h://USER:PASS@gate.quantumproxies.io:PORT: el h empuja la resolución DNS al proxy, lo que detiene las filtraciones desde tu propia red y resuelve nombres de host geocercados desde la ubicación de la salida. curl_cffi detecta el prefijo socks y omite la bandera de tunelización HTTP, ya que el protocolo SOCKS maneja eso por sí mismo. Cada plan aquí expone endpoints HTTP y SOCKS5 en la misma puerta de enlace, por lo que cambiar es un intercambio de esquema en lugar de un nuevo pedido.
- HTTP/3 sobre un proxy llegó en la v0.15.0 junto con las huellas digitales de http/3, pero necesita un servidor SOCKS5 que hable UDP, no una puerta de enlace HTTP simple. Específico hasta que tu objetivo recompense QUIC.
- Endurecimiento SSRF. La misma versión incluyó un aviso: si obtienes URLs suministradas por otras personas, las redirecciones pueden ser llevadas a tu red interna. Establece
allow_redirects="safe", o desactiva las redirecciones. - Depuración. La v0.15 lanzó una CLI:
curl-cffi get tls.browserleaks.com/json --impersonate chromete dice en una línea si la suplantación está funcionando, antes de culpar al proxy.
Cuando curl_cffi más un proxy es suficiente
Más a menudo de lo que la gente espera. Si el objetivo sirve JSON desde una API interna o HTML renderizado por el servidor, y el único obstáculo es una verificación de huellas digitales, un apretón de manos igualado más una salida residencial lo soluciona a una fracción del costo y latencia de un navegador. Las preguntas frecuentes del proyecto son directas sobre el techo: las huellas digitales son un factor entre varios, junto con la calidad de la IP, la tasa de solicitudes y las verificaciones de JavaScript, y los niveles de protección más altos necesitan tanto un mejor pool de proxies como una automatización real del navegador. Cuando la suplantación está configurada correctamente y aún estás bloqueado, la variable restante casi siempre es la IP de salida — aislar eso en cinco minutos es el tema de curl_cffi vs requests. Si prefieres no ejecutar ninguno, la Scraper API maneja huellas digitales, proxies y renderizado JS opcional detrás de una sola llamada.
Preguntas frecuentes
¿Cómo uso un proxy con curl_cffi?
Pasa proxy="http://USER:PASS@host:port" a cualquier método de solicitud, sesión o sesión async. El diccionario estilo requests proxies={"http": ..., "https": ...} también funciona, pero el proyecto recomienda el único parámetro a menos que necesites diferentes proxies por esquema. Pasar ambos genera un TypeError.
¿Por qué curl_cffi lanza ErrCode 35 WRONG_VERSION_NUMBER?
Porque la URL del proxy comienza con https://. Un proxy estándar espera una solicitud CONNECT en texto plano y luego tunela tu TLS; un prefijo https:// hace que curl intente hacer un apretón de manos TLS con el proxy mismo, que responde en HTTP plano. Cambia el valor a http:// — la clave https se refiere al objetivo, no al salto.
¿curl_cffi soporta proxies SOCKS5?
Sí, de forma nativa — libcurl está incluido, por lo que no hay un extra opcional para instalar. Usa el esquema socks5h:// para que los nombres de host sean resueltos por el proxy en lugar de tu máquina. SOCKS4, SOCKS4a y socks5:// simple también son aceptados; la biblioteca omite la tunelización HTTP para cualquier proxy cuyo esquema comience con socks.
¿curl_cffi puede rotar proxies en cada solicitud?
Sí. Un argumento proxy= en una llamada individual anula el valor predeterminado de la sesión, incluso dentro de una AsyncSession, que es lo que el README significa por asyncio con rotación por solicitud. Con una puerta de enlace rotativa a menudo no necesitas ninguna lógica: el mismo endpoint entrega una IP de salida diferente por conexión.
¿curl_cffi puede eludir Cloudflare?
A veces. Elimina la señal de huella digital TLS y HTTP/2, lo cual es suficiente para niveles de protección básicos. No puede ejecutar desafíos de JavaScript, resolver Turnstile, o reparar una IP de centro de datos que una base de datos de reputación ya ha marcado. Trata la suplantación como uno de tres requisitos, no la respuesta.
Toda la configuración es más pequeña de lo que su reputación sugiere: una cadena proxy, impersonate configurada una vez en la sesión, un tiempo de espera en cada llamada, y credenciales ya sea codificadas en URL o pasadas como una tupla proxy_auth. Hazlo bien y la variable restante es la calidad de la IP — un apretón de manos perfecto de Chrome desde una dirección de centro de datos marcada sigue siendo una dirección de centro de datos marcada. Nuestro desglose de huellas digitales JA3 y JA4 explica por qué las dos verificaciones son independientes.