curl: (56) CONNECT tunnel failed, response 403That is curl 8.15 to 8.17 asking a proxy to open a tunnel to an https:// host and being refused. curl 8.18.0 and 8.22.0 print the same message with exit code 7 instead of 56. The same refusal, recorded through one local test proxy, looks like this in Python Requests 2.34.2, which raises requests.exceptions.ProxyError with this message:
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')))And in Node.js 24.20.0's built-in fetch with NODE_USE_ENV_PROXY=1, printed one err.cause level per line:
depth 0: TypeError: fetch failed
depth 1: DOMException: Request was cancelled.
depth 2: RequestAbortedError [UND_ERR_ABORTED]: Proxy response (403) !== 200 when HTTP TunnelingIn all three, the proxy said no before your request left it. Through the same proxy, https://example.com/ returned 200 in every client. Only the host changed.
What the 403 means
For an https:// URL the client sends CONNECT example.org:443 to the proxy and waits for permission to open a tunnel. RFC 9110 says any 2xx reply switches the connection to tunnel mode, and "any response other than a successful response indicates that the tunnel has not yet been formed." A 403 is the proxy refusing that tunnel. RFC 9110
Two things follow:
- It is not a credentials problem. A proxy that wants credentials answers 407 with a
Proxy-Authenticatechallenge; the 407 guide covers that case. A 403 means the proxy decided not to forward this request, so changing the password will not help. - The target site was never reached. No tunnel means no TLS handshake and no request to
example.org. curl's write-out shows it:connect=403is the proxy's answer andhttp=000means the site never sent one.
Why coding agents hit it
Sandboxes and CI runners often send all outbound traffic through an egress proxy with an allowlist. The proxy address arrives in HTTPS_PROXY, so curl, pip, npm and fetch all go through it. Hosts on the list get a tunnel; everything else gets a 403. That is why one host works and the next one fails from the same shell.
Two public reports show this pattern:
- In anthropics/claude-code#56959, Claude Code running with Bedrock had
HTTPS_PROXYpointing at the sandbox's own proxy on localhost. That proxy allowed four hosts (bedrock-runtime.us-east-1.amazonaws.com,api.anthropic.comand two Sentry patterns) and, in the reporter's words, "any other host returns HTTP 403 from the local proxy".curl https://github.comfailed withcurl: (56) CONNECT tunnel failed, response 403. The reporter saidsandbox.network.allowedDomainsentries in user and managed settings were ignored in that mode. The issue was closed as a duplicate of #37970. - In SocketDev/socket-sdk-js#659, an agent running a weekly dependency update in CI could not finish. The update tool, taze, looked up versions at
npm.antfu.dev, which "the CI sandbox firewall blocks (403 CONNECT tunnel failed)". curl to that host returned HTTP 000 with curl error 56, whilecurl https://registry.npmjs.org/semverreturned 200. The maintainers fixed it by keeping lookups onregistry.npmjs.org, which was already allowlisted.
How to tell who said no
Run the failing URL once with curl -v through the same proxy. Lines starting with > are what curl sent; lines starting with < before CONNECT tunnel failed are the proxy's reply. This is curl 8.16.0 against the test proxy:
* 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 403The Trying line names the proxy, not the site. The 403 arrives in answer to CONNECT, and there are no TLS lines after it, so the site never took part. If a 403 instead appears after a successful CONNECT and a TLS handshake, it came from the website, and the proxy is not the problem.
Then compare an allowed host and a blocked host through the same proxy, using the write-out to separate the two status codes:
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/With curl 8.18.0 the first printed http=200 connect=200 exit=0. The second printed http=000 connect=403 exit=7 plus curl: (7) CONNECT tunnel failed, response 403. Same client, same proxy, different host: the proxy has a per-host rule. Repeat the comparison with your own allowed and blocked hosts. In an agent sandbox, use a host you know works, such as the package registry the agent already reached.
Finally, check which proxy the process actually uses. Print HTTPS_PROXY, HTTP_PROXY, NO_PROXY and their lowercase forms in the same shell or job step that fails. A host listed in NO_PROXY skips the proxy and connects directly, which a sandbox may block in a different way. The proxy environment variables guide explains how clients choose between these variables, and NO_PROXY matching, tested shows that clients do not agree on what a NO_PROXY entry matches. Node's built-in fetch ignores these variables unless NODE_USE_ENV_PROXY=1 or --use-env-proxy is set, so a Node script and a curl command in the same shell can take different routes. Node.js CLI documentation
Fix it without going around the proxy
The 403 is a policy decision, so the fix is on the policy side or the source side:
- Allow the host. Add the exact domain the tool needs to the sandbox or CI egress allowlist. Find the domain in the
CONNECTline ofcurl -vor the tool's own error, not in its configuration: in the Socket report, taze looked up versions atnpm.antfu.dev, not at the npm registry. - Use a source that is already allowed. A mirror, an internal registry or the official registry the policy already permits often removes the need for a policy change. That was the Socket fix.
- Check the port. Some proxies only tunnel port 443. Squid's suggested configuration includes
http_access deny CONNECT !SSL_portswithSSL_portsset to port 443. Squid http_access In the test proxy, which allowedexample.comon port 443 only,https://example.com:8443/got the sameCONNECT tunnel failed, response 403as the blocked host. - Check the provider's destination rules. Commercial proxy providers can also refuse destinations. Bright Data's error catalog lists policy errors under HTTP 403, for example "You tried to target %HOST% which is blocked by Bright Data policy". Bright Data error catalog Decodo lists categories of targets, such as banking and government sites, that it restricts by default. Decodo restricted targets
Do not bypass a sandbox's egress rules. Unsetting HTTPS_PROXY, adding the host to NO_PROXY or routing through a different proxy to get around an allowlist defeats the control someone put there on purpose, and it may simply fail: the reporter of #56959 found that the sandbox blocked direct network access underneath the proxy as well. Ask whoever manages the sandbox or CI runner to allow the host, and give them the exact CONNECT line from curl -v. The agent in the Socket report did exactly that: it stopped, reported the blocked host and did not try to evade the firewall.
Why curl reports http:// differently
For an http:// URL there is no CONNECT. curl sends the whole request to the proxy, and the proxy's 403 is the response to that request. The test proxy answered both forms with 403, but curl reported them differently:
| Through the same proxy | curl 8.15.0 to 8.17.0 | curl 8.18.0 and 8.22.0 |
|---|---|---|
https://example.org/ (blocked) | exit 56, curl: (56) CONNECT tunnel failed, response 403 | exit 7, curl: (7) CONNECT tunnel failed, response 403 |
http://example.org/ (blocked) | exit 0, the proxy's 403 page printed as the body | exit 0, the proxy's 403 page printed as the body |
http://example.org/ with --fail | exit 22, curl: (22) The requested URL returned error: 403 | exit 22, curl: (22) The requested URL returned error: 403 |
https://example.com/ (allowed) | exit 0, http=200 connect=200 | exit 0, http=200 connect=200 |
For the http:// case, curl -v shows > GET http://example.org/ HTTP/1.1 followed by < HTTP/1.1 403 Forbidden, and curl prints the body. curl maintainer Daniel Stenberg described the difference in curl discussion #15718, which is about a 407 but applies to any refusal: failing to set up the tunnel means the transfer is not performed, which is a failure, while a GET to a proxy that returns a response "is a successful transfer containing a 4xx code." So an http:// URL in a script without --fail can look like success.
The other clients split the same way, with one exception. Python Requests returned a normal Response with status 403 for http://example.org/. Node.js 24.20.0's fetch with NODE_USE_ENV_PROXY=1 opened a CONNECT example.org:80 tunnel even for the http:// URL, so it failed with the same Proxy response (403) !== 200 when HTTP Tunneling cause.
One more version detail: the curl 8.22.0 static build we used includes HTTP/3. When the host advertised HTTP/3, it first tried a CONNECT-UDP tunnel, so -v showed two 403 replies before the final curl: (7) CONNECT tunnel failed, response 403.
Reproduce it
Everything above was recorded on 2 October 2026 with the lab in the download folder: a Node.js proxy on 127.0.0.1 that tunnels only to example.com on port 443 and answers 403 to every other CONNECT and plain-HTTP request, with curl 8.15.0, 8.16.0, 8.17.0, 8.18.0 and 8.22.0, Python 3.14.4 with Requests 2.34.2 and urllib3 2.8.0, and Node.js 24.20.0. The raw output is in results.json. The test proxy's 403 body (403 Forbidden: lab proxy policy) is its own; a real sandbox or provider proxy will send different text.
Related
- Fix proxy error 407, when the proxy asks for credentials instead.
- Fix ERR_TUNNEL_CONNECTION_FAILED, for the same refusal inside Chromium, Playwright and Puppeteer.
- Proxy environment variables, for which variable a client actually reads.
- NO_PROXY matching, tested, for why a bypass entry matches in one client and not another.
- AI agent proxies and compute, for deciding which parts of an agent need a proxy at all.
ipvolt is building proxy infrastructure for developers and agents. Join the waitlist for one email when access opens.
Sources
- 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