Programming languages
Python [SSL: WRONG_VERSION_NUMBER] wrong version number: fixes
ssl.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 tohttp://(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 withNO_PROXY=example.com. - Fix: use
http://inHTTPS_PROXY, in theproxies=dict and inpip --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.comalso fails withwrong version number, andcurl http://example.com:443/returns HTML. - Fix: in Nginx the
listendirective needs thesslparameter (listen 443 ssl;) together withssl_certificateandssl_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.