wrong version number es lo que informa OpenSSL cuando inicia un handshake TLS y los primeros bytes de la respuesta no son TLS. En la práctica son HTTP en texto plano: el cliente envió un ClientHello a un puerto que responde sin cifrar, y una respuesta como HTTP/1.1 400 Bad Request no puede leerse como un registro TLS. No interviene ningún certificado, versión de protocolo ni cifrado, así que --tlsv1.2, --insecure y verify=False no cambian nada.
Detrás de un proxy ese handshake puede ocurrir en dos lugares, y la mayoría de los clientes imprimen el mismo texto para ambos:
- Salto al proxy. La URL del proxy dice
https://, así que el cliente inicia TLS con el propio proxy, pero el puerto del proxy habla HTTP plano. - Salto al destino. La URL del proxy dice
http://, elCONNECTtiene éxito y el cliente inicia TLS a través del túnel con un puerto de destino que no habla TLS, comohttps://example.com:80/.
| Cliente | Caso 1: salto al proxy | Caso 2: salto al destino |
|---|---|---|
| curl 8.18.0 | curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number | La misma línea |
| Requests 2.34.2 | ProxyError: Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. | SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number, sin pista |
| HTTPX 0.28.1 | ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number | La misma línea |
Solo Requests nombra el salto. Con curl y HTTPX hace falta una comprobación más, descrita a continuación. Las filas de Python proceden de Python 3.12.3 con OpenSSL 3.0.13; las compilaciones más recientes de OpenSSL redactan el mismo fallo de otra forma, y la última tabla de esta página recoge todas las cadenas registradas.
Cómo distinguir los dos saltos
Lee primero la URL del proxy. El caso 1 necesita una URL de proxy con https://. Imprime lo que el proceso usa de verdad, incluidas las variables que no definiste tú:
env | grep -i _proxySi todas las URL de proxy ya empiezan por http://, estás en el caso 2.
curl: lee el estado de CONNECT. %{http_connect} es el estado de la respuesta del proxy al CONNECT. Cero significa que curl no llegó tan lejos.
curl --silent --show-error --output /dev/null \
--proxy "$PROXY_URL" \
--write-out 'connect=%{http_connect} exit=%{exitcode}\n' \
https://example.com/connect=000 exit=35: falló el handshake con el proxy. Caso 1.connect=200 exit=35: el túnel se abrió y falló el handshake con el destino. Caso 2.
curl -v muestra lo mismo. En el caso 2 estas líneas aparecen antes del error; en el caso 1 no hay ninguna línea CONNECT:
> CONNECT 127.0.0.1:18000 HTTP/1.1
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
* TLS connect error: error:0A00010B:SSL routines::wrong version numberLa salida detallada también imprime la cabecera Proxy-Authorization, así que comparte la línea de write-out, no la traza.
Requests: lee la clase de la excepción. requests.exceptions.ProxyError significa que falló la conexión con el proxy. requests.exceptions.SSLError significa que TLS falló con el túnel ya abierto.
import requests
proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
proxies = {"http": proxy_url, "https": proxy_url}
try:
requests.get("https://example.com/", proxies=proxies, timeout=(10, 20))
except requests.exceptions.ProxyError as error:
print("proxy hop:", type(error).__name__)
except requests.exceptions.SSLError as error:
print("target hop:", type(error).__name__)La pista viene de urllib3, que solo la añade cuando el esquema de la URL del proxy es https y el error TLS contiene wrong version number, unknown protocol o record layer failure. Las versiones antiguas eran menos cuidadosas: urllib3 1.26.8 imprimía la pista también con URL de proxy http:// (issue #2564). Con un urllib3 antiguo, fíate de la URL del proxy que imprimiste, no de la pista.
HTTPX: cambia el esquema o lee el registro de httpcore. HTTPX lanza el mismo ConnectError en ambos casos. Si la URL del proxy es https://, cámbiala a http:// y vuelve a ejecutar: si el error desaparece era el caso 1, y si persiste el mismo error es el caso 2. Para una respuesta directa, activa el registro de depuración:
import logging
logging.basicConfig(level=logging.DEBUG)El caso 2 registra un CONNECT que devuelve 200 y después un fallo en httpcore.proxy:
DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'Connection established', [])
DEBUG:httpcore.proxy:start_tls.failed exception=ConnectError(SSLError(1, '[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1081)'))El caso 1 no registra ningún CONNECT, y la línea que falla es httpcore.connection:start_tls.failed. Este registro es de Python 3.14.4 con OpenSSL 3.5.5, por eso dice RECORD_LAYER_FAILURE.
Corrección del caso 1: escribe http:// en la URL del proxy
El esquema de una URL de proxy describe cómo habla el cliente con el proxy, no lo que obtiene a través de él. Una URL de proxy http:// transporta igualmente tráfico HTTPS: el cliente envía CONNECT en texto plano y después ejecuta TLS con el destino dentro del túnel. Usa https:// solo cuando tu proveedor documente un puerto de proxy con TLS. La documentación de --proxy de curl trata la ausencia de esquema o http:// como proxy HTTP y https:// como proxy HTTPS.
curl --proxy http://USERNAME:PASSWORD@proxy.example.net:8080 https://example.com/import requests
proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
response = requests.get(
"https://example.com/",
proxies={"http": proxy_url, "https": proxy_url},
timeout=(10, 20),
)import httpx
with httpx.Client(proxy="http://USERNAME:PASSWORD@proxy.example.net:8080") as client:
response = client.get("https://example.com/")export HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
export HTTP_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080En el laboratorio, el puerto de proxy que fallaba con https:// devolvió 200 en los tres clientes en cuanto el esquema fue http://, definido en el código o mediante HTTPS_PROXY. Si el error sobrevive al cambio, puede que una variable de entorno siga aportando el valor antiguo: Requests y HTTPX leen HTTPS_PROXY de forma predeterminada (trust_env). Variables de entorno de proxy explica qué variable lee cada cliente, y las guías de curl, Requests y HTTPX contienen configuraciones completas.
La trampa: la clave del diccionario no es el esquema del proxy
En proxies={"https": ...} la clave es el esquema de la URL que solicitas. El valor es la URL del proxy, con su propio esquema. No tienen por qué coincidir, y con un puerto de proxy HTTP plano no deben coincidir:
# Case 1: TLS to a proxy port that speaks plain HTTP
proxies = {"https": "https://USERNAME:PASSWORD@proxy.example.net:8080"}
# Correct: HTTPS targets through an HTTP proxy
proxies = {"https": "http://USERNAME:PASSWORD@proxy.example.net:8080"}Las variables de entorno funcionan igual. El nombre HTTPS_PROXY selecciona qué solicitudes usan el proxy, y el valor indica cómo llegar a él.
Corrección del caso 2: corrige la URL de destino
El proxy está bien. La URL pide TLS en un puerto que sirve HTTP plano. Las causas habituales son https:// combinado con el puerto 80 o con un puerto HTTP interno, y una URL base montada a partir de un esquema y un puerto que se configuran por separado. Confírmalo enviando HTTP plano por el mismo túnel:
curl --silent --show-error --proxy "$PROXY_URL" --proxytunnel \
--write-out 'connect=%{http_connect} status=%{http_code}\n' \
http://example.com:80/En el laboratorio esto imprimió la página y connect=200 status=200 para el puerto que había fallado con https://, mientras que un puerto que de verdad habla TLS dio curl: (52) Empty reply from server. Un estado HTTP aquí significa que el puerto habla HTTP plano: cambia la URL a http:// o apúntala al puerto TLS. Si se te permite conectar sin el proxy, la misma URL falla igual de forma directa, lo que además descarta al proxy.
Si la URL es correcta y el error va y viene, los bytes que llegan por el túnel no proceden del servidor TLS de tu destino. El issue #2564 de urllib3 describe ese patrón con un proxy inestable. Anota la línea connect= con una marca de tiempo y envíasela al proveedor.
Los reintentos no arreglan ninguno de los dos casos
Ambos casos son errores de configuración, y cada intento falla igual. El Max retries exceeded del mensaje de Requests no significa que se hayan hecho reintentos: el adaptador predeterminado está configurado con cero reintentos e imprime ese texto de todos modos. Una política de reintentos solo retrasa la misma excepción. Corrige la URL y reserva los reintentos para fallos que puedan cambiar entre intentos.
Dentro de un agente de programación compatible con MCP, el Proxy Toolkit MCP, que es gratuito, diagnostica errores de proxy por fase de la solicitud. Indícale el cliente, la excepción y el valor de connect=, sin credenciales.
El texto depende de la compilación de OpenSSL
El texto procede de la biblioteca TLS, no del cliente, así que el mismo fallo se lee distinto según la compilación. Cada compilación imprimió la misma cadena en ambos saltos salvo que se indique lo contrario:
| Compilación | Texto del error |
|---|---|
| curl 8.18.0 (OpenSSL 3.5.5), curl 8.17.0 (OpenSSL 3.6.0), curl 8.22.0 (OpenSSL 4.0.2) | curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number |
| curl 8.12.1 (OpenSSL 3.4.1) | curl: (35) TLS connect error: error:0A0000C6:SSL routines::packet length too long |
| curl 8.7.1 (OpenSSL 3.2.1) | curl: (35) OpenSSL/3.2.1: error:0A0000C6:SSL routines::packet length too long |
| Python 3.11.3 (OpenSSL 1.1.1s), Python 3.12.3 (OpenSSL 3.0.13) | [SSL: WRONG_VERSION_NUMBER] wrong version number |
| Python 3.14.4 (OpenSSL 3.5.5) | [SSL: RECORD_LAYER_FAILURE] record layer failure |
Node.js 24.20.0 fetch (OpenSSL 3.5.7) | TypeError: fetch failed, código de causa ERR_SSL_WRONG_VERSION_NUMBER |
| Playwright 1.63.0 (Chromium 153) | Caso 1: net::ERR_PROXY_CONNECTION_FAILED. Caso 2: net::ERR_SSL_PROTOCOL_ERROR |
Así que packet length too long y record layer failure con un proxy de por medio son los mismos dos casos. En las tres compilaciones de Python, Requests lanzó ProxyError con la pista en el caso 1 y un SSLError sin más en el caso 2. unknown protocol, la tercera frase que busca urllib3, no apareció en ninguna compilación probada. Chromium es aquí el único cliente que separa los saltos por sí solo; Corrige ERR_TUNNEL_CONNECTION_FAILED cubre sus errores de proxy, y Node.js fetch failed detrás de un proxy explica cómo leer la cadena cause.
Cómo se probó
El 4 de octubre de 2026, en un VPS con Ubuntu 26.04, con todo en 127.0.0.1: un listener que responde a cualquier entrada con un HTTP/1.1 400 Bad Request en texto plano, un proxy CONNECT mínimo sobre HTTP plano, python -m http.server como destino HTTP plano y el mismo manejador detrás de un certificado desechable como destino TLS funcional. Sin cuenta de proxy y sin tráfico saliente. El caso 1 apuntó una URL de proxy https:// al listener de texto plano y al proxy CONNECT. El caso 2 usó el proxy CONNECT con una URL https:// para el puerto HTTP plano. Un control con la URL de proxy http:// y el destino TLS devolvió 200 en todos los clientes.
Clientes: las compilaciones de curl anteriores; Requests 2.34.2 con urllib3 2.8.0 y HTTPX 0.28.1 con httpcore 1.0.9 en las tres compilaciones de Python; fetch de Node.js 24.20.0 con NODE_USE_ENV_PROXY=1; Playwright 1.63.0 con Chromium 153.0.8010.12. El archivo del laboratorio contiene el script del laboratorio, los scripts de cliente, el README y los resultados registrados.
No se probó: macOS ni Windows, bibliotecas TLS distintas de OpenSSL y la propia de Chromium, proxies SOCKS, puertos de proxy TLS reales ni ninguna red de proxies comercial. Tampoco se probó un puerto de proxy que cierre la conexión o guarde silencio sin responder en texto plano; puede producir un error distinto.
Guías relacionadas
- Variables de entorno de proxy: HTTP_PROXY y NO_PROXY explica qué variable leen curl, Requests y Node.
- Usar un proxy con curl cubre
-x, el túnelCONNECTy los códigos de salida de curl. - Configurar Python Requests explica el diccionario
proxiesytrust_env. - Proxies asíncronos con HTTPX cubre clientes explícitos y tiempos de espera del pool.
- Corrige ERR_TUNNEL_CONNECTION_FAILED cubre los rechazos del proxy en Playwright y Puppeteer.
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.
- urllib3: Your proxy appears to only use HTTP and not HTTPS
- urllib3 issue #2564: the HTTP-only proxy hint shown for http:// proxy URLs
- urllib3 2.8.0 source: _wrap_proxy_error in connection.py
- curl: -x, --proxy and the proxy URL scheme
- curl: --write-out variables including http_connect
- Requests: proxies
- HTTPX: proxies
- HTTPX: logging
- RFC 9110: the CONNECT method