Pon las credenciales en la URL del proxy y pásala como proxy:
from curl_cffi import requests
PROXY = "http://user:pass@proxy.example.com:8080"
with requests.Session(impersonate="chrome", proxy=PROXY) as session:
response = session.get("https://example.com/")
print(response.status_code)Para detectar los fallos del proxy, captura ProxyError y ConnectionError, y comprueba si status_code es 407. En curl_cffi 0.16, un proxy que rechaza CONNECT lanza ConnectionError, no ProxyError.
En nuestro laboratorio esto autenticó peticiones https:// y http:// contra un proxy local que exige autenticación Basic. Todo lo que sigue se registró el 10 de octubre de 2026 con curl_cffi 0.16.3 (libcurl 8.21.0) en Python 3.14.4. impersonate elige el perfil de navegador del que curl_cffi copia los ajustes de TLS y HTTP/2. Todos los resultados fueron iguales con y sin él.
Tres detalles antes de los errores:
- Usa
proxyoproxies, no los dos.proxies={"http": PROXY, "https": PROXY}funcionó igual en una petición y en unaSession. La clave es el esquema de la URL que pides: con solo la clave"https", una peticiónhttp://salió directa. Pasar los dos argumentos lanzóTypeError: Cannot specify both 'proxy' and 'proxies'. - Codifica la contraseña de la URL con porcentajes.
p@ss:w/rdfuncionó comop%40ss%3Aw%2Frd. Sin codificar, la petición lanzóProxyErrorconcurl: (5) Unsupported proxy syntax.proxy_auth=("user", "p@ss:w/rd")junto a una URL de proxy sin credenciales también funcionó y no necesita codificación. - Escribe
http://para un proxy HTTP, también con URLhttps://. El esquema describe la conexión con el proxy. Una URL de proxyhttps://apuntada al proxy HTTP sin TLS del laboratorio lanzóSSLErrorconcurl: (35) TLS connect erroryWRONG_VERSION_NUMBER. SSL WRONG_VERSION_NUMBER con proxy explica ese mensaje.
CONNECT tunnel failed, response 407: ConnectionError, no ProxyError
curl_cffi.requests.exceptions.ConnectionError: Failed to perform, curl: (7) CONNECT tunnel failed, response 407. See https://curl.se/libcurl/c/libcurl-errors.html first for more details.Para una URL https://, curl_cffi envía primero CONNECT al proxy. El proxy del laboratorio respondió 407 y la petición lanzó este error. El mensaje fue idéntico sin credenciales y con una contraseña incorrecta.
isinstance(error, ProxyError)esFalse.ProxyErroryConnectionErrorson clases hermanas bajoRequestException, así queexcept ProxyErrorno lo captura.error.codees 7.error.responsees unaResponsecuyostatus_codees 0, no 407. El estado solo está en el mensaje.- El código 7 no es exclusivo de un 407. Cuando el proxy del laboratorio no pudo llegar al destino respondió 502, y el mensaje fue
curl: (7) CONNECT tunnel failed, response 502. Sin nada escuchando en el puerto del proxy fuecurl: (7) Failed to connect to 127.0.0.1:8443 over proxy 127.0.0.1 after 0 ms: Could not connect to server.
La clase depende del código de error de curl, y ese código cambió entre versiones. Ejecutamos la misma petición en doce de ellas:
| curl_cffi | libcurl | CONNECT rechazado | Fallo de SOCKS |
|---|---|---|---|
| 0.16.0 a 0.16.3 | 8.21.0 | ConnectionError, código curl 7 | ProxyError, código curl 97 |
| 0.7.4 a 0.15.0 | 8.7.0 a 8.15.0 | ProxyError, código curl 56 | ProxyError, código curl 97 |
Hasta la 0.15.0 el mensaje era curl: (56) CONNECT tunnel failed, response 407. La línea de comandos de curl pasó del código de salida 56 al 7 en la 8.18.0. Corrige el error de proxy 407 tiene esa tabla y el orden de comprobaciones para las credenciales.
Este bloque gestiona un CONNECT rechazado antes y después del cambio, un fallo de SOCKS y una respuesta 407:
import re
from curl_cffi.requests.exceptions import ConnectionError, ProxyError
CONNECT_STATUS = re.compile(r"CONNECT tunnel failed, response (\d{3})")
class ProxyFailure(Exception):
pass
def fetch(session, url):
try:
response = session.get(url)
except ProxyError as error:
raise ProxyFailure(f"proxy error, curl {error.code}") from error
except ConnectionError as error:
rejected = CONNECT_STATUS.search(str(error))
if error.code == 7 and rejected:
raise ProxyFailure(f"proxy answered CONNECT with {rejected[1]}") from error
raise
if response.status_code == 407:
raise ProxyFailure("proxy answered 407")
return responseEn el laboratorio informó proxy answered CONNECT with 407, proxy answered CONNECT with 502, proxy answered 407 para una URL http:// y proxy error, curl 97 para un fallo de SOCKS. Con el puerto del proxy cerrado volvió a lanzar el ConnectionError.
407 en URL http://: una respuesta, no una excepción
Para una URL http:// no hay túnel. curl_cffi envía la URL completa al proxy en un GET normal, como hace desde la 0.7.0b7 (issue #326), y el 407 del proxy vuelve como respuesta.
No se lanzó ninguna excepción. response.status_code fue 407, response.ok fue False y las cabeceras traían Proxy-Authenticate: Basic realm="lab". El código que solo captura excepciones trata esto como una petición correcta con el cuerpo vacío.
response.raise_for_status() lanzó curl_cffi.requests.exceptions.HTTPError: HTTP Error 407: Proxy Authentication Required. En esa excepción, error.response.status_code es 407.
Variables de entorno: http_proxy se lee, HTTP_PROXY no
Sin proxy en el código, libcurl toma uno del entorno:
- Para una URL
http://,HTTP_PROXYen mayúsculas se ignoró y la petición salió directa. Conhttp_proxyen minúsculas pasó por el proxy. - Para una URL
https://se usaron tantoHTTPS_PROXYcomohttps_proxy. Con solohttp_proxy, una peticiónhttps://salió directa. ALL_PROXYyall_proxyse aplicaron a los dos esquemas.NO_PROXY=127.0.0.1yno_proxyen minúsculas enviaron la petición directa.
libcurl documenta la excepción: las variables también se comprueban en mayúsculas, "except the http_proxy one which is only used lowercase" (variables de entorno de libcurl). El motivo es CGI: un servidor convierte una cabecera de petición Proxy: en una variable HTTP_PROXY para el script (Everything curl).
Scrapy lee HTTP_PROXY en mayúsculas (Proxies en Scrapy 2.19), y aiohttp no lee ninguna variable salvo que pases trust_env=True (Proxies en aiohttp 3.14). Variables de entorno de proxy compara más clientes.
Dos resultados son fáciles de pasar por alto:
NO_PROXYanula el argumentoproxy. ConNO_PROXY=127.0.0.1definida,requests.get(url, proxy=PROXY)salió directa con los dos esquemas. Al añadircurl_options={CurlOpt.NOPROXY: ""}volvió a pasar por el proxy. Frente ahttp_proxyganó el argumento.trust_env=Falseno desactivó las variables. UnaSession(trust_env=False)siguió usandohttp_proxyyHTTPS_PROXY.proxy=""tampoco las desactivó.
Dos ajustes sí enviaron la petición directa con las variables definidas:
from curl_cffi import CurlOpt, requests
requests.get("https://example.com/", proxies={"all": ""})
requests.get("https://example.com/", curl_options={CurlOpt.NOPROXY: "*"})Los dos funcionan también como argumentos de Session. Una cadena de proxy vacía "explicitly disables the use of a proxy, even if there is an environment variable set for it" (CURLOPT_PROXY).
SOCKS: socks5:// resuelve en local, socks5h:// envía el nombre de host
El laboratorio pidió http://dnsprobe.example:8080/, un nombre que solo existe en el archivo hosts como 127.0.0.2, a través de un servidor SOCKS5 que registra cada destino:
- Con
socks5://127.0.0.1:1080el servidor registróCONNECT atyp=IPv4 dest=127.0.0.2:8080. curl_cffi resolvió el nombre por su cuenta. - Con
socks5h://127.0.0.1:1080registróCONNECT atyp=DOMAIN dest=dnsprobe.example:8080. El proxy recibió el nombre de host.
Una URL https:// dio los mismos dos resultados. Es la convención del propio curl (Usar un proxy con curl). socks5 vs socks5h enumera los clientes que no la siguen.
Un handshake SOCKS fallido es el caso que sí lanza ProxyError:
curl_cffi.requests.exceptions.ProxyError: Failed to perform, curl: (97) cannot complete SOCKS5 connection to 127.0.0.1. (1). See https://curl.se/libcurl/c/libcurl-errors.html first for more details.error.code es 97. El último número entre paréntesis es el código de respuesta que envió el servidor SOCKS: el del laboratorio respondió 1, fallo general. Cuando el puerto de destino estaba cerrado y el servidor respondió 5, el mensaje terminó en (5).
Un esquema que no corresponde al puerto falla de otras maneras:
socks5://apuntado al puerto del proxy HTTP lanzóProxyErrorconcurl: (97) Received invalid version in initial SOCKS5 response.http://apuntado al puerto SOCKS lanzóProxyErrorconcurl: (56) Proxy CONNECT abortedpara una URLhttps://.- Sin nada escuchando en el puerto SOCKS, el error fue otra vez el
ConnectionErrorcon código 7.
AsyncSession: la misma configuración, los mismos errores
import asyncio
from curl_cffi import AsyncSession
PROXY = "http://user:pass@proxy.example.com:8080"
async def main():
async with AsyncSession(impersonate="chrome", proxy=PROXY) as session:
response = await session.get("https://example.com/")
print(response.status_code)
asyncio.run(main())Cada caso asíncrono coincidió con su equivalente síncrono. Un CONNECT rechazado lanzó el mismo ConnectionError con código 7. Una URL http:// devolvió el 407 como respuesta. Un fallo de SOCKS lanzó ProxyError con código 97. socks5:// y socks5h:// enviaron los mismos tipos de dirección, y HTTP_PROXY se ignoró mientras que http_proxy se usó.
Reintentos: retry repite las peticiones POST
curl_cffi no reintenta por defecto. El laboratorio envió un POST a través del proxy a un destino que lee la petición y cierra la conexión sin responder. La llamada lanzó ConnectionError con curl: (52) Empty reply from server, y el destino había recibido un POST.
Con requests.Session(proxy=PROXY, retry=1) la misma llamada devolvió 201, y el destino había recibido el cuerpo dos veces. retry repitió el POST igual que repite un GET.
retry=2 ante un CONNECT rechazado envió tres peticiones CONNECT y lanzó el mismo error, con Session y con AsyncSession. Un reintento no arregla unas credenciales incorrectas.
Deja retry en 0 para las peticiones que no es seguro repetir, o envía una clave de idempotencia. Reintentos con proxy: una respuesta perdida, dos trabajos creados muestra qué cambia una clave. Si quieres ayuda de diagnóstico dentro de un agente de programación, Proxy Toolkit MCP acepta descripciones de errores saneadas. Mantén las credenciales fuera de sus argumentos.
Cómo se probó
En Ubuntu 26.04.1 con Python 3.14.4 y curl_cffi 0.16.3 (libcurl 8.21.0-IMPERSONATE con BoringSSL), todo en direcciones de loopback: python -m http.server como destinos HTTP y HTTPS, tres proxies que exigen autenticación Basic y registran cada petición, un servidor SOCKS5 que registra cada destino y otro que responde a cada petición con el código 1, y un destino que pierde la respuesta al primer POST. dnsprobe.example solo existía en una copia privada del archivo hosts. Cada uno de los 86 casos se ejecutó en un intérprete nuevo con un entorno limpio. La comparación de versiones instaló doce versiones, de la 0.7.4 a la 0.16.3. No se usó ninguna cuenta de proxy.
El archivo del laboratorio contiene el ejecutor, los casos, el proxy, el servidor SOCKS5, el README, los resultados registrados y la comparación de versiones.
No se probó: macOS y Windows, autenticación de proxy Digest y NTLM, proxies SOCKS con autenticación, proxies a los que se llega por TLS, HTTP/3 y WebSockets.
ipvolt es un servicio de proxies para desarrolladores que todavía no está abierto; únete a la lista de acceso anticipado y recibirás un solo correo cuando abra.
Fuentes y lecturas adicionales
Referencias técnicas usadas para esta guía. Consulta la documentación de tu versión instalada y la configuración compatible de tu proveedor.
- curl_cffi documentation: Proxies
- curl_cffi API reference: Session (proxy, proxies, proxy_auth, trust_env, retry)
- curl_cffi API reference: AsyncSession
- curl_cffi API reference: Exceptions and warnings (ProxyError, ConnectionError)
- curl_cffi documentation: Using CURLOPT_* in requests API (curl_options)
- curl_cffi issue #326: CONNECT was sent for http:// URLs, changed in 0.7.0b7
- libcurl: CURLOPT_PROXY (schemes, the empty string, socks5h)
- libcurl: CURLOPT_NOPROXY and the no_proxy variable
- libcurl environment variables: http_proxy is only used lowercase
- Everything curl: proxy environment variables and the CGI reason
- libcurl error codes: CURLE_COULDNT_CONNECT (7), CURLE_RECV_ERROR (56), CURLE_PROXY (97)