# SSL WRONG_VERSION_NUMBER behind a proxy: which hop failed

Source: https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy
Markdown: https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy.md

[Home](https://ipvolt.com/index.md) / [Guides](https://ipvolt.com/guides.md) / [SSL WRONG_VERSION_NUMBER behind a proxy: which hop failed](https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy.md)

Category: Troubleshooting
Published: 2026-10-04
Reading time: 8 minutes
Author: ipvolt

The same SSL wrong version number error comes from two hops behind a proxy. Tell which one failed in curl, Requests and HTTPX, then apply the right fix.

`wrong version number` is what OpenSSL reports when it starts a TLS handshake and the first bytes of the reply are not TLS. In practice they are plain HTTP: the client sent a ClientHello to a port that answers in cleartext, and a reply such as `HTTP/1.1 400 Bad Request` cannot be read as a TLS record. No certificate, protocol version or cipher is involved, so `--tlsv1.2`, `--insecure` and `verify=False` change nothing.

Behind a proxy that handshake can happen in two places, and most clients print the same text for both:

1. **Proxy hop.** The proxy URL says `https://`, so the client starts TLS with the proxy itself, but the proxy port speaks plain HTTP.
2. **Target hop.** The proxy URL says `http://`, `CONNECT` succeeds, and the client starts TLS through the tunnel with a target port that doesn't speak TLS, such as `https://example.com:80/`.

| Client | Case 1: proxy hop | Case 2: target hop |
| --- | --- | --- |
| curl 8.18.0 | `curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number` | The same line |
| 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`, with no hint |
| HTTPX 0.28.1 | `ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number` | The same line |

Only Requests names the hop. With curl and HTTPX you need one more check, described next. The Python rows come from Python 3.12.3 with OpenSSL 3.0.13; newer OpenSSL builds word the same failure differently, and the last table on this page lists every string recorded.

## Tell the two hops apart

**Read the proxy URL first.** Case 1 needs an `https://` proxy URL. Print what the process really uses, including variables you didn't set yourself:

```sh
env | grep -i _proxy
```

If every proxy URL already starts with `http://`, you are in case 2.

**curl: read the CONNECT status.** `%{http_connect}` is the status of the proxy's reply to `CONNECT`. Zero means curl never got that far.

```sh
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`: the handshake with the proxy failed. Case 1.
- `connect=200 exit=35`: the tunnel opened and the handshake with the target failed. Case 2.

`curl -v` shows the same thing. In case 2 these lines come before the error; in case 1 there is no `CONNECT` line at all:

```text
> 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 number
```

Verbose output also prints the `Proxy-Authorization` header, so share the write-out line, not the trace.

**Requests: read the exception class.** `requests.exceptions.ProxyError` means the connection to the proxy failed. `requests.exceptions.SSLError` means TLS failed after the tunnel was open.

```python
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__)
```

The hint comes from urllib3, which adds it only when the proxy URL scheme is `https` and the TLS error contains `wrong version number`, `unknown protocol` or `record layer failure`. Older releases were less careful: urllib3 1.26.8 printed the hint for `http://` proxy URLs too ([issue #2564](https://github.com/urllib3/urllib3/issues/2564)). On an old urllib3, trust the proxy URL you printed, not the hint.

**HTTPX: change the scheme, or read the httpcore log.** HTTPX raises the same `ConnectError` for both. If the proxy URL is `https://`, change it to `http://` and run again: when the error goes away it was case 1, and when the same error remains it is case 2. For a direct answer, turn on debug logging:

```python
import logging

logging.basicConfig(level=logging.DEBUG)
```

Case 2 logs a `CONNECT` that returns 200, then a failure in `httpcore.proxy`:

```text
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)'))
```

Case 1 logs no `CONNECT`, and the failing line is `httpcore.connection:start_tls.failed`. This log is from Python 3.14.4 with OpenSSL 3.5.5, which is why it says `RECORD_LAYER_FAILURE`.

## Fix case 1: write http:// in the proxy URL

The scheme in a proxy URL describes how the client talks to the proxy, not what it fetches through it. An `http://` proxy URL still carries HTTPS traffic: the client sends `CONNECT` in cleartext, then runs TLS with the target inside the tunnel. Use `https://` only when your provider documents a TLS proxy port. curl's `--proxy` documentation treats a missing scheme or `http://` as an HTTP proxy and `https://` as an HTTPS proxy.

```sh
curl --proxy http://USERNAME:PASSWORD@proxy.example.net:8080 https://example.com/
```

```python
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),
)
```

```python
import httpx

with httpx.Client(proxy="http://USERNAME:PASSWORD@proxy.example.net:8080") as client:
    response = client.get("https://example.com/")
```

```sh
export HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
export HTTP_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
```

In the lab, the proxy port that failed with `https://` returned 200 in all three clients once the scheme was `http://`, set in code or through `HTTPS_PROXY`. If the error survives the change, an environment variable may still be supplying the old value: Requests and HTTPX both read `HTTPS_PROXY` by default (`trust_env`). [Proxy environment variables](/guides/proxy-environment-variables) covers which variable each client reads, and the [curl](/guides/curl-proxy-setup), [Requests](/guides/python-requests-proxy) and [HTTPX](/guides/httpx-async-proxy) guides have complete setups.

## The trap: the dictionary key is not the proxy's scheme

In `proxies={"https": ...}` the key is the scheme of the URL you are requesting. The value is the proxy URL, with its own scheme. The two don't have to match, and for a plain HTTP proxy port they must not:

```python
# 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"}
```

Environment variables work the same way. The name `HTTPS_PROXY` selects which requests use the proxy, and the value says how to reach it.

## Fix case 2: correct the target URL

The proxy is fine. The URL asks for TLS on a port that serves plain HTTP. Typical causes are `https://` combined with port 80 or an internal HTTP port, and a base URL assembled from a scheme and a port that are configured separately. Confirm it by sending plain HTTP through the same tunnel:

```sh
curl --silent --show-error --proxy "$PROXY_URL" --proxytunnel \
  --write-out 'connect=%{http_connect} status=%{http_code}\n' \
  http://example.com:80/
```

In the lab this printed the page and `connect=200 status=200` for the port that had failed with `https://`, while a port that really speaks TLS gave `curl: (52) Empty reply from server`. An HTTP status here means the port speaks plain HTTP: change the URL to `http://`, or point it at the TLS port. If you are allowed to connect without the proxy, the same URL fails the same way directly, which also clears the proxy.

If the URL is right and the error comes and goes, the bytes arriving through the tunnel are not from your target's TLS server. urllib3 issue #2564 reports that pattern with an unstable proxy. Record the `connect=` line with a timestamp and send it to the provider.

## Retries won't fix either case

Both cases are configuration errors, and every attempt fails the same way. The `Max retries exceeded` in the Requests message doesn't mean retries ran: the default adapter is configured for zero retries and prints that text anyway. A retry policy only delays the same exception. Fix the URL, and keep retries for failures that can change between attempts.

Inside an MCP-compatible coding agent, the free [Proxy Toolkit MCP](/mcp) diagnoses proxy errors by request phase. Give it the client, the exception and the `connect=` value, without credentials.

## The wording depends on the OpenSSL build

The text comes from the TLS library, not from the client, so the same failure reads differently across builds. Each build printed the same string at both hops unless noted:

| Build | Error text |
| --- | --- |
| 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`, cause code `ERR_SSL_WRONG_VERSION_NUMBER` |
| Playwright 1.63.0 (Chromium 153) | Case 1: `net::ERR_PROXY_CONNECTION_FAILED`. Case 2: `net::ERR_SSL_PROTOCOL_ERROR` |

So `packet length too long` and `record layer failure` with a proxy in the path are the same two cases. On all three Python builds, Requests raised `ProxyError` with the hint in case 1 and a bare `SSLError` in case 2. `unknown protocol`, the third phrase urllib3 looks for, did not appear on any build tested. Chromium is the one client here that separates the hops by itself; [Fix ERR_TUNNEL_CONNECTION_FAILED](/guides/fix-err-tunnel-connection-failed) covers its proxy errors, and [Node.js fetch failed behind a proxy](/guides/fix-node-fetch-failed-proxy) covers reading the `cause` chain.

## How this was tested

On 4 October 2026, on an Ubuntu 26.04 VPS, with everything on `127.0.0.1`: a listener that answers any input with a plaintext `HTTP/1.1 400 Bad Request`, a minimal plain-HTTP `CONNECT` proxy, `python -m http.server` as the plain HTTP target, and the same handler behind a throwaway certificate as a working TLS target. No proxy account and no outbound traffic. Case 1 pointed an `https://` proxy URL at the plaintext listener and at the `CONNECT` proxy. Case 2 used the `CONNECT` proxy with an `https://` URL for the plain HTTP port. A control with the `http://` proxy URL and the TLS target returned 200 in every client.

Clients: the curl builds above; Requests 2.34.2 with urllib3 2.8.0 and HTTPX 0.28.1 with httpcore 1.0.9 on the three Python builds; Node.js 24.20.0 `fetch` with `NODE_USE_ENV_PROXY=1`; Playwright 1.63.0 with Chromium 153.0.8010.12. The [lab archive](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/wrong-version-number-lab.zip) contains the [lab script](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/lab.py), the client scripts, the [README](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/README.md) and the [recorded results](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/results.json).

Not tested: macOS and Windows, TLS libraries other than OpenSSL and Chromium's own, SOCKS proxies, real TLS proxy ports and any commercial proxy network. A proxy port that closes the connection or stays silent, with no plaintext reply, was not tested either and may produce a different error.

## Related guides

- [Proxy environment variables: HTTP_PROXY and NO_PROXY](/guides/proxy-environment-variables) explains which variable curl, Requests and Node each read.
- [Use a proxy with curl](/guides/curl-proxy-setup) covers `-x`, the `CONNECT` tunnel and curl's exit codes.
- [Configure Python Requests](/guides/python-requests-proxy) explains the `proxies` dictionary and `trust_env`.
- [HTTPX async proxies](/guides/httpx-async-proxy) covers explicit clients and pool timeouts.
- [Fix ERR_TUNNEL_CONNECTION_FAILED](/guides/fix-err-tunnel-connection-failed) covers proxy refusals in Playwright and Puppeteer.

ipvolt is a proxy service for developers that isn't open yet; [join the early-access list](https://ipvolt.com/#waitlist-closing) for one email when it opens.

## Sources & further reading

- [urllib3: Your proxy appears to only use HTTP and not HTTPS](https://urllib3.readthedocs.io/en/stable/advanced-usage.html#https-proxy-error-http-proxy)
- [urllib3 issue #2564: the HTTP-only proxy hint shown for http:// proxy URLs](https://github.com/urllib3/urllib3/issues/2564)
- [urllib3 2.8.0 source: _wrap_proxy_error in connection.py](https://github.com/urllib3/urllib3/blob/2.8.0/src/urllib3/connection.py)
- [curl: -x, --proxy and the proxy URL scheme](https://curl.se/docs/manpage.html#-x)
- [curl: --write-out variables including http_connect](https://curl.se/docs/manpage.html#-w)
- [Requests: proxies](https://requests.readthedocs.io/en/latest/user/advanced/#proxies)
- [HTTPX: proxies](https://www.python-httpx.org/advanced/proxies/)
- [HTTPX: logging](https://www.python-httpx.org/logging/)
- [RFC 9110: the CONNECT method](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect)

## Related guides

- [Proxy environment variables: HTTP_PROXY and NO_PROXY](https://ipvolt.com/guides/proxy-environment-variables.md)
- [Use a proxy with curl: -x, env vars, SOCKS5, auth](https://ipvolt.com/guides/curl-proxy-setup.md)
- [Python Requests proxy: proxies dict, auth, SOCKS5](https://ipvolt.com/guides/python-requests-proxy.md)
- [HTTPX async proxies: setup and PoolTimeout diagnosis](https://ipvolt.com/guides/httpx-async-proxy.md)
- [Fix ERR_TUNNEL_CONNECTION_FAILED in Playwright and Puppeteer](https://ipvolt.com/guides/fix-err-tunnel-connection-failed.md)
- [Fix Node.js TypeError: fetch failed behind a proxy](https://ipvolt.com/guides/fix-node-fetch-failed-proxy.md)

## About ipvolt

Examples use generic proxy settings, with links to the original technical documentation. Product-specific behavior must be checked with your provider. ipvolt is still in development.

## Know when access opens.

ipvolt · In development

We’re building proxy infrastructure for developers and data teams. Join the interest list for a heads-up when ipvolt is ready.

Consent: One email when access opens. Nothing else.

[Get early access](https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy#waitlist-closing). Use the email form on this page to join the interest list.

[Privacy](https://ipvolt.com/privacy)

