Proxy error · WRONG_VERSION_NUMBER

SSL: wrong version number

Your client opened a TLS handshake and the other end answered in plain text. OpenSSL reads the start of that answer as a TLS version, finds nonsense, and stops. With a proxy set, the usual cause is a proxy URL that starts with https:// when the proxy speaks plain HTTP.
Whose fault is it?

Yours, and a one-word fix. Your client tried TLS where the other end speaks plain HTTP.

Usually from
Your config: the scheme in the proxy URL
Try first
Change the proxy URL to http://, for https sites too
With a proxy in the path

What a wrong version number error means through a proxy

Every TLS record starts with a type byte and a version. Send a TLS hello to a port that speaks plain HTTP and the reply is text such as HTTP/1.1 400 Bad Request, whose letters OpenSSL reads as a version it has never heard of. Python shows [SSL: WRONG_VERSION_NUMBER] wrong version number; curl and Node show error:0A00010B:SSL routines::wrong version number. Other TLS libraries and versions say unknown protocol or record layer failure.

The scheme of a proxy URL says how to talk to the proxy, not to the site. https:// asks for TLS to the proxy itself. The gateway expects plain HTTP on its port, and the tunnel it opens to an HTTPS site is encrypted end to end anyway, so the right proxy URL is http:// even when every site you visit is HTTPS.

The same error turns up without any proxy mistake when the address you fetch says https:// but names a port that serves plain HTTP, such as https://example.com:80/. Then the fix is in the URL you are fetching.

The key question

Your credentials, the proxy, or the target?

Yours, and a one-word fix. Your client tried TLS where the other end speaks plain HTTP.

  • Your credentials

    Not the cause. The handshake fails before a username or password is sent.

  • The proxy

    Only as the thing your client spoke TLS to. The gateway expects plain HTTP on its port; the https:// in your proxy URL is the mistake.

  • The target site

    Only when the URL you fetch says https:// but points at a port that serves plain HTTP. That fails the same way without a proxy.

How to tell

  • The error arrives at once, before any status code. A certificate problem names the certificate; this one names a version.
  • Print the proxy URL your code actually uses, including HTTPS_PROXY from the environment. An https:// at the front is the answer.
  • In Python, a ProxyError wrapping WRONG_VERSION_NUMBER means the handshake with the proxy failed. A bare SSLError means the handshake with the site did.
  • urllib3 adds Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP to the message when it spots this case.
Cheapest first

Fixes, in the order to try them

  1. Write the proxy URL with http://

    Under both the http and https keys, in Playwright’s server, and in HTTP_PROXY and HTTPS_PROXY. Traffic to HTTPS sites stays encrypted inside the tunnel.

    Costs nothing
  2. Check the environment and system settings

    A stale HTTPS_PROXY=https://... in a shell profile or CI config applies without appearing in your code. On Windows and macOS, Python also reads the system proxy settings; python -c "import urllib.request; print(urllib.request.getproxies())" shows what it found.

    Costs nothing
  3. Check the port in the URL you fetch

    An https:// address with a port that serves plain HTTP, such as :80 or :8080, fails the same way. Use http:// for that address, or the port that serves TLS.

    Costs nothing
  4. Leave certificate checks alone

    verify=False and -k change nothing here: the handshake fails before any certificate is exchanged. Fix the scheme instead.

    Costs nothing
  5. Add a smoke test after upgrades

    Some older client releases quietly spoke plain HTTP to an https:// proxy URL, so an upgrade can surface a typo that was always there. Fix the URL rather than pinning the old release, and send one request through the proxy in CI.

    Costs some time
Per tool

See the real status and headers

Each sample sends HTTPS traffic through an http:// proxy URL, the setup that avoids this error, and shows what the wrong scheme produces.

curl
terminal
# wrong: an https:// proxy makes curl do TLS with the gateway itself
curl -s -o /dev/null -x https://USER:[email protected]:8000 https://example.com/
# curl: (35) ... error:0A00010B:SSL routines::wrong version number

# right: plain HTTP to the proxy, TLS to the site inside the tunnel
curl -s -o /dev/null -w 'proxy %{http_connect}  site %{http_code}\n' -x http://USER:[email protected]:8000 https://example.com/
Python Requests
scheme.py
import os

import requests

for key in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
    value = os.environ.get(key) or os.environ.get(key.lower())
    if value:
        print(key, "starts with", value.split("://")[0] + "://")

PROXY = "http://USER:[email protected]:8000"
r = requests.get("https://example.com/", proxies={"http": PROXY, "https": PROXY}, timeout=30)
print(r.status_code)

With https:// under either key, urllib3 2 raises a ProxyError with WRONG_VERSION_NUMBER inside. The loop prints only the scheme of each variable, so no password reaches your terminal.

Scrapy
in the spider
RESIDENTIAL = "http://USER:[email protected]:8000"

async def start(self):
    for url in self.start_urls:
        yield scrapy.Request(url, meta={"proxy": RESIDENTIAL}, errback=self.on_error)

def on_error(self, failure):
    if failure.check(NotImplementedError):
        self.logger.error("Scrapy cannot use an https:// proxy for an https:// site: %s", failure.value)
    elif "wrong version number" in repr(failure.value).lower():
        self.logger.error("TLS met plain HTTP: check the scheme and port of %s", failure.request.url)
    else:
        self.logger.error("%r", failure.value)

Write the proxy with http:// in meta, and in http_proxy and https_proxy if you set those.

Playwright
scheme.py
from playwright.sync_api import Error, sync_playwright

PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}

assert PROXY["server"].startswith("http://"), "the proxy server takes http://, even for https sites"

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()
    try:
        print(page.goto("https://example.com/").status)
    except Error as err:
        print(err.message.splitlines()[0])
    browser.close()

# net::ERR_SSL_PROTOCOL_ERROR   an https:// address on a port that speaks plain HTTP
Node.js
scheme.mjs
import { fetch, ProxyAgent } from "undici";

const proxy = new URL(process.env.PROXY_URL ?? "http://USER:[email protected]:8000");
if (proxy.protocol !== "http:") {
  throw new Error("use http:// for the proxy URL, not " + proxy.protocol);
}

const dispatcher = new ProxyAgent(proxy.href);
const res = await fetch("https://example.com/", { dispatcher });
console.log(res.status);

// with https:// here: SecureProxyConnectionError, caused by ERR_SSL_WRONG_VERSION_NUMBER

The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.

Before you buy anything

Will a different proxy line fix it?

When switching helps

No line fixes this. The fix is one word in your config, and it is the same word on every line.

When it will not

A new order with the same https:// in front of it fails the same way.

The meter

Is a failed request billed?

We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.

From the metering section of our terms of service.

The terms say connections that fail before transferring data are not billed. With the wrong scheme, the handshake with the gateway fails before a tunnel opens or a request is sent. If rows show up in your usage log for these attempts, ask in Discord.

Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.

WRONG_VERSION_NUMBER, asked often

Questions people ask about a wrong version number error

Should the proxy URL be http:// or https:// for HTTPS sites?

http://. The scheme says how you talk to the proxy. Your client then asks the proxy for a tunnel and does TLS with the site inside it, so the site traffic is encrypted either way.

Why did this start after I upgraded requests?

Newer urllib3, which Requests is built on, treats an https:// proxy URL as a request for TLS to the proxy. Some older releases quietly spoke plain HTTP instead. The typo was always there; the upgrade made it visible.

Is WRONG_VERSION_NUMBER a certificate problem?

No. It fails before any certificate is sent. Your client sent a TLS hello and got plain text back, so turning off verification changes nothing.

My proxy URL already says http:// and I still get it. What else?

Look at the URL you are fetching. An https:// address on a port that serves plain HTTP gives the same error, with or without a proxy. Then check HTTPS_PROXY in the environment: Requests uses it for any key you did not set yourself.

The community layer

Still stuck on a wrong version number error?

Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk