ERR_SSL_PROTOCOL_ERROR: what it means and how to fix it
- ssl
ERR_SSL_PROTOCOL_ERROR is Chrome saying the TLS handshake never
completed. This is different from an expired
certificate - the browser never got
far enough to judge the certificate at all. The two sides failed to
agree on how to speak encrypted in the first place, and the most common
reason is that one of them wasn't speaking TLS at all.
What does ERR_SSL_PROTOCOL_ERROR actually mean?
The browser opened a connection to port 443 and expected a TLS
handshake. What came back was something else: plain HTTP, an old
protocol version the browser refuses (SSLv3, TLS 1.0, TLS 1.1), a
reset, or bytes that don't parse as TLS. Firefox shows the same failure
as SSL_ERROR_RX_RECORD_TOO_LONG or "Secure Connection Failed"; curl
says "error in the TLS handshake" or "wrong version number."
"Wrong version number" is the tell worth memorizing: it almost always means an HTTP response arrived where a TLS record was expected - a web server answering unencrypted on 443.
How do I see what the server is actually sending?
Two commands from any machine:
# What does the handshake look like?
curl -v https://example.com 2>&1 | head -20
# Ask openssl to negotiate and print the result
echo | openssl s_client -connect example.com:443 -servername example.com 2>&1 | head -15
Read the failure, not the summary. wrong version number means plain
HTTP on 443. no protocols available or handshake failure means a
protocol-version or cipher mismatch. Connection refused means nothing
is listening on 443 at all - a firewall or a missing listen
directive, not a TLS problem.
Why is my server speaking plain HTTP on 443?
The usual suspects, in order:
- The TLS block never loaded. In nginx,
listen 443;without thesslflag serves plain HTTP on 443. It needslisten 443 ssl;plusssl_certificateandssl_certificate_keypaths that exist. - A port forward or container mapping points 443 at the app's plain HTTP port - the handshake reaches Node or Django directly, which answer in HTTP.
- Behind Cloudflare or another CDN: the edge terminates TLS fine, then forwards to an origin misconfigured as above. The error can appear only for the edge-to-origin leg, which surfaces as 5xx errors at the edge instead - test your origin's IP directly to see the truth.
After any config change: nginx -t, then reload, then re-run the curl
above. Servers load TLS config at startup, so an edit alone changes
nothing.
What if the config looks right?
Then check protocol versions. Browsers require TLS 1.2 or newer; a server pinned to TLS 1.0/1.1 by an old hardening guide now fails modern clients:
openssl s_client -connect example.com:443 -tls1_2 </dev/null
If that fails while the plain s_client line above also fails, update
ssl_protocols TLSv1.2 TLSv1.3; (nginx) or the equivalent. And if
visitors report the error while every test of yours passes, suspect
their side: corporate proxies and antivirus "HTTPS scanning" intercept
handshakes, and a wrong system clock breaks TLS in confusing ways.
How do I catch this before visitors do?
A broken handshake is a full outage that never shows in your server's access log - requests die before a request line exists. It takes an outside client to see it. YoPingMe's HTTP and SSL checks perform the full handshake from both of your check regions on every run, so a bad reload or a protocol misconfiguration pages you by email, webhook, or Slack instead of waiting for a user's screenshot. Ten monitors are free, and the first check runs while you sign up.