curl: (56) CONNECT tunnel failed, response 403Eso es curl 8.15 a 8.17 pidiendo a un proxy que abra un túnel hacia un host https:// y recibiendo una negativa. curl 8.18.0 y 8.22.0 imprimen el mismo mensaje con código de salida 7 en lugar de 56. El mismo rechazo, registrado a través de un único proxy de prueba local, se ve así en Python Requests 2.34.2, que lanza requests.exceptions.ProxyError con este mensaje:
HTTPSConnectionPool(host='example.org', port=443): Max retries exceeded with url: / (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 403 Forbidden')))Y en el fetch integrado de Node.js 24.20.0 con NODE_USE_ENV_PROXY=1, imprimiendo un nivel de err.cause por línea:
depth 0: TypeError: fetch failed
depth 1: DOMException: Request was cancelled.
depth 2: RequestAbortedError [UND_ERR_ABORTED]: Proxy response (403) !== 200 when HTTP TunnelingEn los tres casos, el proxy dijo que no antes de que tu petición saliera de él. A través del mismo proxy, https://example.com/ devolvió 200 en todos los clientes. Solo cambió el host.
Qué significa el 403
Para una URL https://, el cliente envía CONNECT example.org:443 al proxy y espera permiso para abrir un túnel. El RFC 9110 dice que cualquier respuesta 2xx pasa la conexión a modo túnel y que «cualquier respuesta que no sea satisfactoria indica que el túnel todavía no se ha formado». Un 403 es el proxy negándose a abrir ese túnel. RFC 9110
De ahí salen dos cosas:
- No es un problema de credenciales. Un proxy que quiere credenciales responde 407 con un desafío
Proxy-Authenticate; la guía del 407 cubre ese caso. Un 403 significa que el proxy decidió no reenviar esta petición, así que cambiar la contraseña no servirá. - El sitio de destino nunca se alcanzó. Sin túnel no hay handshake TLS ni petición a
example.org. El write-out de curl lo muestra:connect=403es la respuesta del proxy yhttp=000significa que el sitio nunca respondió.
Por qué les pasa a los agentes de programación
Los sandboxes y los runners de CI suelen enviar todo el tráfico de salida a través de un proxy de egress con una lista de permitidos. La dirección del proxy llega en HTTPS_PROXY, así que curl, pip, npm y fetch pasan todos por él. Los hosts de la lista obtienen un túnel; todo lo demás recibe un 403. Por eso un host funciona y el siguiente falla desde la misma shell.
Dos informes públicos muestran este patrón:
- En anthropics/claude-code#56959, Claude Code ejecutándose con Bedrock tenía
HTTPS_PROXYapuntando al propio proxy del sandbox en localhost. Ese proxy permitía cuatro hosts (bedrock-runtime.us-east-1.amazonaws.com,api.anthropic.comy dos patrones de Sentry) y, en palabras de quien lo reportó, «cualquier otro host devuelve HTTP 403 desde el proxy local».curl https://github.comfallaba concurl: (56) CONNECT tunnel failed, response 403. Quien lo reportó dijo que las entradas desandbox.network.allowedDomainsen la configuración de usuario y en la gestionada se ignoraban en ese modo. La incidencia se cerró como duplicada de #37970. - En SocketDev/socket-sdk-js#659, un agente que ejecutaba una actualización semanal de dependencias en CI no pudo terminar. La herramienta de actualización, taze, buscaba versiones en
npm.antfu.dev, que «bloquea el firewall del sandbox de CI (403 CONNECT tunnel failed)». curl a ese host devolvió HTTP 000 con el error 56 de curl, mientras quecurl https://registry.npmjs.org/semverdevolvió 200. Los mantenedores lo arreglaron manteniendo las búsquedas enregistry.npmjs.org, que ya estaba en la lista de permitidos.
Cómo saber quién dijo que no
Ejecuta una vez la URL que falla con curl -v a través del mismo proxy. Las líneas que empiezan por > son lo que envió curl; las que empiezan por < antes de CONNECT tunnel failed son la respuesta del proxy. Esto es curl 8.16.0 contra el proxy de prueba:
* Trying 127.0.0.1:41745...
* CONNECT: no ALPN negotiated
* allocate connect buffer
* Establish HTTP proxy tunnel to example.org:443
> CONNECT example.org:443 HTTP/1.1
> Host: example.org:443
> User-Agent: curl/8.16.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain
< Content-Length: 32
< Connection: close
<
* CONNECT tunnel failed, response 403
* closing connection #0
curl: (56) CONNECT tunnel failed, response 403La línea Trying nombra al proxy, no al sitio. El 403 llega como respuesta al CONNECT y no hay líneas de TLS después, así que el sitio nunca participó. Si en cambio un 403 aparece después de un CONNECT correcto y un handshake TLS, viene del sitio web y el proxy no es el problema.
Después compara un host permitido y uno bloqueado a través del mismo proxy, usando el write-out para separar los dos códigos de estado:
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.com/
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.org/Con curl 8.18.0 el primero imprimió http=200 connect=200 exit=0. El segundo imprimió http=000 connect=403 exit=7 y curl: (7) CONNECT tunnel failed, response 403. Mismo cliente, mismo proxy, distinto host: el proxy tiene una regla por host. Repite la comparación con tus propios hosts permitido y bloqueado. En el sandbox de un agente, usa un host que sepas que funciona, como el registro de paquetes al que el agente ya llegó.
Por último, comprueba qué proxy usa realmente el proceso. Imprime HTTPS_PROXY, HTTP_PROXY, NO_PROXY y sus variantes en minúsculas en la misma shell o el mismo paso del job que falla. Un host incluido en NO_PROXY se salta el proxy y conecta directamente, algo que un sandbox puede bloquear de otra manera. La guía de variables de entorno de proxy explica cómo eligen los clientes entre estas variables, y NO_PROXY matching, tested muestra que los clientes no coinciden en qué encaja con una entrada de NO_PROXY. El fetch integrado de Node ignora estas variables salvo que se defina NODE_USE_ENV_PROXY=1 o --use-env-proxy, así que un script de Node y un comando curl en la misma shell pueden tomar rutas distintas. Documentación de la CLI de Node.js
Arréglalo sin rodear el proxy
El 403 es una decisión de política, así que el arreglo está en el lado de la política o en el de la fuente:
- Permite el host. Añade el dominio exacto que necesita la herramienta a la lista de permitidos de egress del sandbox o del CI. Toma el dominio de la línea
CONNECTdecurl -vo del propio error de la herramienta, no de su configuración: en el informe de Socket, taze buscaba versiones ennpm.antfu.dev, no en el registro de npm. - Usa una fuente que ya esté permitida. Un espejo, un registro interno o el registro oficial que la política ya permite suele evitar el cambio de política. Ese fue el arreglo de Socket.
- Revisa el puerto. Algunos proxies solo abren túneles al puerto 443. La configuración sugerida de Squid incluye
http_access deny CONNECT !SSL_ports, conSSL_portsdefinido como el puerto 443. Squid http_access En el proxy de prueba, que permitíaexample.comsolo en el puerto 443,https://example.com:8443/recibió el mismoCONNECT tunnel failed, response 403que el host bloqueado. - Revisa las reglas de destino del proveedor. Los proveedores comerciales de proxies también pueden rechazar destinos. El catálogo de errores de Bright Data enumera errores de política bajo HTTP 403, por ejemplo «You tried to target %HOST% which is blocked by Bright Data policy». Catálogo de errores de Bright Data Decodo enumera categorías de destinos, como sitios bancarios y gubernamentales, que restringe por defecto. Destinos restringidos de Decodo
No rodees las reglas de egress de un sandbox. Quitar HTTPS_PROXY, añadir el host a NO_PROXY o enrutar por otro proxy para saltarte una lista de permitidos anula el control que alguien puso a propósito, y puede que simplemente no funcione: quien reportó #56959 comprobó que el sandbox también bloqueaba el acceso directo a la red por debajo del proxy. Pide a quien gestiona el sandbox o el runner de CI que permita el host y dale la línea CONNECT exacta de curl -v. El agente del informe de Socket hizo justo eso: se detuvo, informó del host bloqueado y no intentó evadir el firewall.
Por qué curl informa distinto con http://
Para una URL http:// no hay CONNECT. curl envía la petición completa al proxy, y el 403 del proxy es la respuesta a esa petición. El proxy de prueba respondió 403 en ambos casos, pero curl los informó de forma distinta:
| A través del mismo proxy | curl 8.15.0 a 8.17.0 | curl 8.18.0 y 8.22.0 |
|---|---|---|
https://example.org/ (bloqueado) | salida 56, curl: (56) CONNECT tunnel failed, response 403 | salida 7, curl: (7) CONNECT tunnel failed, response 403 |
http://example.org/ (bloqueado) | salida 0, la página 403 del proxy impresa como cuerpo | salida 0, la página 403 del proxy impresa como cuerpo |
http://example.org/ con --fail | salida 22, curl: (22) The requested URL returned error: 403 | salida 22, curl: (22) The requested URL returned error: 403 |
https://example.com/ (permitido) | salida 0, http=200 connect=200 | salida 0, http=200 connect=200 |
En el caso http://, curl -v muestra > GET http://example.org/ HTTP/1.1 seguido de < HTTP/1.1 403 Forbidden, y curl imprime el cuerpo. El mantenedor de curl Daniel Stenberg describió la diferencia en la discusión de curl #15718, que trata de un 407 pero se aplica a cualquier rechazo: no lograr establecer el túnel significa que la transferencia no se realiza, lo cual es un fallo, mientras que un GET a un proxy que devuelve una respuesta «es una transferencia correcta que contiene un código 4xx». Por eso una URL http:// en un script sin --fail puede parecer un éxito.
Los demás clientes se dividen igual, con una excepción. Python Requests devolvió un Response normal con estado 403 para http://example.org/. El fetch de Node.js 24.20.0 con NODE_USE_ENV_PROXY=1 abrió un túnel CONNECT example.org:80 incluso para la URL http://, así que falló con la misma causa Proxy response (403) !== 200 when HTTP Tunneling.
Un detalle más de versiones: la compilación estática de curl 8.22.0 que usamos incluye HTTP/3. Cuando el host anunciaba HTTP/3, primero intentó un túnel CONNECT-UDP, así que -v mostró dos respuestas 403 antes del curl: (7) CONNECT tunnel failed, response 403 final.
Reprodúcelo
Todo lo anterior se registró el 2 de octubre de 2026 con el laboratorio de la carpeta de descarga: un proxy de Node.js en 127.0.0.1 que solo abre túneles a example.com en el puerto 443 y responde 403 a cualquier otro CONNECT y a las peticiones HTTP normales, con curl 8.15.0, 8.16.0, 8.17.0, 8.18.0 y 8.22.0, Python 3.14.4 con Requests 2.34.2 y urllib3 2.8.0, y Node.js 24.20.0. La salida en bruto está en results.json. El cuerpo 403 del proxy de prueba (403 Forbidden: lab proxy policy) es suyo; el proxy real de un sandbox o de un proveedor enviará otro texto.
Relacionado
- Corrige el error de proxy 407, cuando el proxy pide credenciales.
- Corrige ERR_TUNNEL_CONNECTION_FAILED, para el mismo rechazo dentro de Chromium, Playwright y Puppeteer.
- Variables de entorno de proxy, para saber qué variable lee realmente un cliente.
- NO_PROXY matching, tested, para entender por qué una entrada de exclusión coincide en un cliente y no en otro.
- Proxies y cómputo para agentes de IA, para decidir qué partes de un agente necesitan siquiera un proxy.
ipvolt está construyendo infraestructura de proxies para desarrolladores y agentes. Únete a la lista de espera para recibir un correo cuando se abra el acceso.
Fuentes
- RFC 9110: CONNECT
- anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT
- SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host
- curl discussion #15718: proxy responses for http and https URLs
- Squid configuration: http_access suggested rules
- Bright Data: proxy error catalog
- Decodo: residential proxy restricted targets
- Node.js CLI: NODE_USE_ENV_PROXY