Python [SSL: WRONG_VERSION_NUMBER] wrong version number (tested)
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number almost never means a TLS version mismatch. We reproduced it with urllib and requests: it happens when the client speaks TLS and the other side answers in plain HTTP -- an https:// URL pointed at an HTTP-only port, or a proxy URL written as https:// when the proxy itself speaks HTTP. OpenSSL reads the server's HTTP/1.1 text as a TLS record and rejects its "version".
1. Reproduce it: https:// to a port that speaks plain HTTP
import socket, threading, time, ssl, urllib.request, requests
PORT = 8963
def serve(): # a plain-HTTP server (no TLS)
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", PORT))
srv.listen()
while True:
conn, _ = srv.accept()
conn.recv(65536)
conn.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
conn.close()
threading.Thread(target=serve, daemon=True).start()
time.sleep(0.2)
url = f"https://127.0.0.1:{PORT}/" # https:// to a port that speaks plain HTTP
try:
urllib.request.urlopen(url)
except Exception as e:
print("urlopen ->", type(e).__name__ + ":", e)
try:
requests.get(url)
except Exception as e:
print("requests ->", type(e).__name__ + ":", e)
print("http:// ->", requests.get(url.replace("https://", "http://")).status_code)
print(ssl.OPENSSL_VERSION)
# The proxy variant: proxy URL written with https:// for a proxy that speaks plain HTTP
for proxy in ("https://127.0.0.1:8963", "http://127.0.0.1:8963"):
try:
requests.get("https://example.com/", proxies={"https": proxy}, timeout=5)
except Exception as e:
print(f"proxy {proxy:24} ->", type(e).__name__ + ":", str(e)[-90:])
urlopen -> URLError: <urlopen error [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1029)>
requests -> SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8963): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1029)')))
http:// -> 400
OpenSSL 3.5.7 9 Jun 2026
proxy https://127.0.0.1:8963 -> ProxyError: SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1029)'))))
proxy http://127.0.0.1:8963 -> ProxyError: Error('Unable to connect to proxy', OSError('Tunnel connection failed: 400 Bad Request')))
Measured: the client started a TLS handshake, the server answered with a plain-text HTTP/1.1 400 ... line, and OpenSSL tried to read those bytes as a TLS record header. "HT" is not a valid TLS version, hence WRONG_VERSION_NUMBER. The same URL with http:// reached the server fine. Nothing is wrong with your TLS version settings or certificates.
2. The usual causes
https://on a plain-HTTP port: a dev server on:8000/:8080/:5000, orhttps://host:80. Usehttp://, or put TLS in front of it.- A proxy URL written with
https://: most corporate and local proxies (Squid, Fiddler, mitmproxy's default) accept plain HTTP and tunnel HTTPS withCONNECT. Measured above:proxies={"https": "https://127.0.0.1:8963"}gives the sameWRONG_VERSION_NUMBER;http://127.0.0.1:8963reaches the proxy (our stand-in then refuses the tunnel with 400; a real proxy would open it). CheckHTTPS_PROXY/https_proxyin your environment -- requests and urllib read them automatically -- and write it ashttp://proxy:port. - Wrong port for the service: e.g. SMTP on 587 needs
smtplib.SMTP(...).starttls(), notSMTP_SSL(which expects TLS from the first byte, port 465). - Something in the middle answering in plain text: a captive portal, a misconfigured load balancer listener, or a firewall returning an HTTP block page on 443.
3. Check what the port actually speaks
curl -sv http://HOST:PORT/ -o /tmp/out # answers? it's plain HTTP
openssl s_client -connect HOST:PORT -brief </dev/null # TLS servers print the protocol and cipher
If plain http:// works, fix the URL or the proxy setting. If neither works, the service on that port is not HTTP at all.
Related: ERR_SSL_PROTOCOL_ERROR in Chrome, the browser-side version of the same mismatch.
Frequently asked questions
Does WRONG_VERSION_NUMBER mean I need a different TLS version?
Almost never. We reproduced it by sending a TLS handshake to a plain-HTTP server: OpenSSL read the server's 'HTTP/1.1 400' text as a TLS record. Changing ssl minimum/maximum versions doesn't help.
Why does requests show it through a proxy?
If HTTPS_PROXY or proxies= says https://proxy:port, requests speaks TLS to the proxy. Most proxies expect plain HTTP and tunnel with CONNECT, so write the proxy as http://proxy:port. We measured both forms.
I get it with smtplib. Why?
smtplib.SMTP_SSL expects TLS from the first byte (port 465). Port 587 starts in plain text: use smtplib.SMTP(host, 587) and call .starttls().
How do I tell if a port speaks HTTP or HTTPS?
Try curl -sv http://HOST:PORT/ -- if it answers, it's plain HTTP. openssl s_client -connect HOST:PORT -brief shows a TLS handshake if the port speaks TLS.
Should I set verify=False?
No -- it doesn't help here (the failure is before certificate checks) and it removes protection you need. Fix the URL scheme or proxy setting instead.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 (OpenSSL 3.5.7), urllib.request and requests 2.32.3, connecting with https:// to a local plain-HTTP socket server, directly and through a proxies= setting. PlainHeaders is our extension; Python, requests and OpenSSL are free.