Programming languages

Python [SSL: WRONG_VERSION_NUMBER] wrong version number: fixes

By · Last verified

Error messagessl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number

[SSL: WRONG_VERSION_NUMBER] wrong version number means Python started a TLS handshake, but the reply was not TLS at all. The other side is speaking plain HTTP or another plaintext protocol on that port, most often because a proxy URL starts with https://, the code uses the wrong port, or the server is not running TLS where you expect it.

Quick fix

An https:// proxy URL or a plaintext port causes most cases.

Check what is listening on the host and port, and which proxy Python is using:

# Does the port answer plain HTTP?
curl -sv http://example.com:443/ -o /dev/null

# Which proxies will Python use?
python3 -c "import urllib.request; print(urllib.request.getproxies())"
env | grep -i _proxy
  • If the proxy value starts with https://, change it to http:// (the scheme describes how you talk to the proxy, not to the destination):
export HTTPS_PROXY=http://proxy.example.com:8080
export HTTP_PROXY=http://proxy.example.com:8080
  • If curl http://...:443/ returns an HTTP response, that port is serving plain text; fix the server or use the right port.
  • For mail and other protocols, match the client class to the port (see below).

Causes

1. An https:// proxy URL for a plain HTTP proxy

An https:// proxy URL for a plain HTTP proxy is the most common cause with requests, pip and other urllib3-based tools. Corporate proxies, debugging proxies (Fiddler, Charles, mitmproxy) and local proxy clients normally accept plain HTTP on their port and tunnel HTTPS with CONNECT.

  • Confirm: getproxies() shows 'https': 'https://...', or the error disappears with NO_PROXY=example.com.
  • Fix: use http:// in HTTPS_PROXY, in the proxies= dict and in pip --proxy. On Windows, also check the system proxy settings, which Python reads from the registry.

2. TLS client on a plaintext port

Implicit-TLS clients fail immediately when the port expects plain text first.

  • Confirm: the URL or code uses a port such as 80, 25, 587, 143 or 5432 with a TLS-only client.
  • Fix: use the matching combination:
import smtplib
# Port 465: implicit TLS
with smtplib.SMTP_SSL("smtp.example.com", 465) as s:
    s.login(user, password)
# Port 587: plain text, then STARTTLS
with smtplib.SMTP("smtp.example.com", 587) as s:
    s.starttls()
    s.login(user, password)

The same applies to imaplib.IMAP4_SSL (993) versus IMAP4 plus starttls() (143).

3. The server does not run TLS on that port

A web server listening on 443 without TLS enabled, or a load balancer forwarding plain HTTP to a port the client treats as HTTPS, produces the same error.

  • Confirm: openssl s_client -connect example.com:443 -servername example.com also fails with wrong version number, and curl http://example.com:443/ returns HTML.
  • Fix: in Nginx the listen directive needs the ssl parameter (listen 443 ssl;) together with ssl_certificate and ssl_certificate_key.

4. A captive portal or middlebox answering in plain text

Hotel Wi-Fi login pages and some filtering gateways intercept port 443 and reply with an HTTP redirect; see captive portal SSL errors.

  • Fix: complete the portal login, or test from another network to confirm.

Fixes by environment

Every fix points the TLS client at a port that speaks TLS, and gives the proxy an http:// URL. curl reports the same mistake as error 35.

pip: pip install --proxy http://proxy.example.com:8080 package, or set it once with pip config set global.proxy http://proxy.example.com:8080.

requests: requests.get(url, proxies={"https": "http://proxy.example.com:8080"}). requests also reads environment variables unless session.trust_env = False.

Docker and CI: proxy variables are often injected by the runner or by the Docker daemon configuration. Print the environment inside the container to see what Python receives.

Server side: after fixing the listener, check the result with the SSL checker; for mail servers use the STARTTLS checker.

Don't do this

  • Switching the URL to http:// to make the error go away. The request then travels in clear text, including any tokens or passwords.
  • verify=False. It does not touch this error and removes certificate checks from every later request in the same code path.
  • Forcing an old protocol such as ssl.PROTOCOL_TLSv1. The problem is not the TLS version but the absence of TLS on the other side; downgrading only weakens connections that do work.

FAQ

Will `verify=False` fix WRONG_VERSION_NUMBER?

No. The handshake fails before the server sends any certificate, so there is nothing to verify. Turning verification off only adds a second problem. Fix the proxy URL, the port or the server configuration.

Why did this start after I upgraded pip or requests?

Newer urllib3 releases, which pip and requests use, support HTTPS proxies. An https:// proxy URL is now taken literally: Python opens a TLS connection to the proxy itself. If the proxy listens for plain HTTP, the reply is not TLS and you get WRONG_VERSION_NUMBER. Change the variable to http://proxy.example.com:8080.

Which smtplib class goes with which port?

Port 465 uses implicit TLS, so use smtplib.SMTP_SSL(host, 465). Port 587 (and usually 25) starts in plain text and upgrades with STARTTLS, so use smtplib.SMTP(host, 587) followed by starttls(). RFC 8314 recommends implicit TLS on 465 for message submission.

What does the same problem look like in curl?

curl prints curl: (35) with wrong version number in the OpenSSL message. The cause is the same; see curl (35) SSL connect error.

Sources

  1. Python docs: ssl — TLS/SSL wrapper for socket objects
  2. Requests: Advanced Usage (Proxies)
  3. RFC 8314: Cleartext Considered Obsolete (implicit TLS for mail)
  4. RFC 3207: SMTP Service Extension for Secure SMTP over TLS
  5. nginx: Configuring HTTPS servers