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 the ssl flag serves plain HTTP on 443. It needs listen 443 ssl; plus ssl_certificate and ssl_certificate_key paths 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.