Debug guide · you're seeing
This site can't provide a secure connection. example.com sent an invalid response. ERR_SSL_PROTOCOL_ERRORERR_SSL_PROTOCOL_ERRORSSL_ERROR_RX_RECORD_TOO_LONGcurl: (35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version numbererror:0A00010B:SSL routines:tls_validate_record_header:wrong version number
ERR_SSL_PROTOCOL_ERROR: Causes and Fixes in Chrome
Fix ERR_SSL_PROTOCOL_ERROR: plain HTTP on port 443, TLS version mismatches, SNI, TLS-inspecting proxies and Cloudflare, with curl and openssl checks.
On this page
- What it means
- Who sent it?
- Fix it, in order of likelihood
- 1. HTTPS to a port that speaks plain HTTP
- 2. TLS version or cipher mismatch
- 3. Broken SNI, or no site for this hostname
- 4. A middlebox, antivirus or corporate proxy intercepting TLS
- 5. Cloudflare: what SSL mode does and does not affect
- Reproduce and verify
- Related
TL;DR: Chrome started a TLS handshake and got back something that is not valid TLS. The most common cause by far is a port serving plain HTTP: nginx
listen 443;withoutssl, orhttps://localhost:3000against a dev server. Runopenssl s_client -connect HOST:443 -servername HOST;wrong version numberconfirms plain HTTP. If the handshake works from your machine but fails for some users, look for a TLS-inspecting proxy, antivirus or firewall on their network.
What it means
Chrome shows:
This site can't provide a secure connection
example.com sent an invalid response.
ERR_SSL_PROTOCOL_ERROR
This is not an HTTP error. No request was sent, so there is no status code and nothing useful in your access log (with one exception, covered below). ERR_SSL_PROTOCOL_ERROR (net error -107) is Chrome’s catch-all: when BoringSSL reports a handshake failure that Chrome has no more specific code for, it lands here. Several neighbours have their own codes, and seeing one of them instead narrows the search:
| Chrome error | What actually happened |
|---|---|
ERR_SSL_PROTOCOL_ERROR | Bytes that are not TLS, an unexpected alert, or a broken handshake |
ERR_SSL_VERSION_OR_CIPHER_MISMATCH | No shared TLS version or cipher suite (TLS 1.0/1.1-only servers land here) |
ERR_SSL_UNRECOGNIZED_NAME_ALERT | The server has no site for the SNI hostname and said so |
ERR_CONNECTION_RESET | A TCP RST arrived during the handshake (see ERR_CONNECTION_RESET) |
ERR_CONNECTION_CLOSED | The server closed the connection (TCP FIN) mid-handshake |
NET::ERR_CERT_* | The handshake worked, but the certificate failed (certificate errors) |
The same failure in other clients:
Firefox: Secure Connection Failed ... SSL_ERROR_RX_RECORD_TOO_LONG (plain HTTP on an HTTPS port)
Firefox: Secure Connection Failed ... PR_END_OF_FILE_ERROR (connection closed during the handshake)
curl: curl: (35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version number
Node.js: Error: write EPROTO ...:SSL routines:tls_validate_record_header:wrong version number
Who sent it?
Whoever answers TCP on that port: your web server, a load balancer, a CDN, or a box in the middle that the client does not know about. Three quick checks tell you which:
# 1. What does the port actually speak?
openssl s_client -connect example.com:443 -servername example.com </dev/null
# 2. Who issued the certificate the client sees? Run it from a working and a failing network.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -issuer -subject
# 3. Is the hostname behind a CDN? A cf-ray header means Cloudflare terminates TLS.
curl -sI https://example.com/ | grep -iE '^(server|cf-ray|via|x-amz-cf-id)'
If the issuer on the failing network is a firewall or antivirus vendor rather than your CA, the connection is being intercepted and the server is not the problem.
Fix it, in order of likelihood
1. HTTPS to a port that speaks plain HTTP
This is the cause most of the time, and the error text is misleading because nothing about TLS is wrong: the server never attempted TLS. Chrome sends a ClientHello, the server reads it as a malformed HTTP request and replies HTTP/1.1 400 Bad Request in clear text. Chrome tries to parse HTTP/ as a TLS record header and gives up.
That also explains the odd error names elsewhere. The first five bytes of a TLS record are a content type, a two-byte version and a two-byte length. In HTTP/, the TT sits where the version should be (so OpenSSL says wrong version number), and P/ decodes to a length of 20,527 bytes, larger than TLS allows (so Firefox says SSL_ERROR_RX_RECORD_TOO_LONG).
Confirm with openssl. On OpenSSL 3.x:
openssl s_client -connect example.com:443 -servername example.com </dev/null
80A1BFF901000000:error:0A00010B:SSL routines:tls_validate_record_header:wrong version number:ssl/record/methods/tlsany_meth.c:78:
CONNECTED(00000005)
---
no peer certificate available
---
SSL handshake has read 5 bytes and written 1540 bytes
read 5 bytes and no peer certificate available are the tell: the server sent one bogus record header and no certificate. Older OpenSSL 3.0 builds name the function ssl3_get_record instead; the wrong version number part is stable. One trap: the system curl on macOS is built against LibreSSL and reports this same situation as curl: (35) LibreSSL/3.3.6: error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version, which sends people chasing TLS versions that have nothing to do with it. Trust openssl s_client from Homebrew or a Linux box.
Plain HTTP on 443 is also the one case that leaves a trace in the server’s access log, because the server did parse a “request”. In nginx it looks like this (request bytes trimmed):
203.0.113.7 - - [04/Oct/2026:10:12:01 +0000] "\x16\x03\x01\x07\x1A\x01\x00\x07\x16\x03\x03" 400 157 "-" "-"
\x16 is the TLS handshake record type and \x03\x01 the legacy version field of a ClientHello. A line like this means a client spoke TLS to a port that served HTTP.
Where it comes from:
-
nginx
listen 443;withoutssl. nginx loads the config happily and serves HTTP on port 443. Without thesslparameter, thessl_certificatelines in that block are ignored for that listener.server { listen 443 ssl; # "listen 443;" here serves plain HTTP on 443 listen [::]:443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; }The reverse mistake,
listen 443 sslwith no certificate, does not reach users: current nginx refuses to start withno "ssl_certificate" is defined for the "listen ... ssl" directive. Check withgrep -rn 'listen.*443' /etc/nginx/and look for any 443 listener missingssl, including the IPv6 one. -
Apache
<VirtualHost *:443>withoutSSLEngine on. Same symptom. -
A local dev server.
https://localhost:3000against Express, Vite, Next.js or Django’s runserver, which speak plain HTTP unless told otherwise. Usehttp://, or enable HTTPS in the dev server (for examplenext dev --experimental-https, or Vite’sserver.httpsoption with a certificate). -
Chrome forcing HTTPS on a host you serve over HTTP. If anything on
localhostever sentStrict-Transport-Security, Chrome upgrades every laterhttp://localhostrequest to HTTPS and hits your plain dev server. Delete the entry under Delete domain security policies atchrome://net-internals/#hsts. Entries from the HSTS preload list cannot be deleted: every.devand.appdomain is preloaded, so a dev hostname under those TLDs always needs real TLS. -
An HTTPS proxy setting that is not HTTPS.
HTTPS_PROXY=https://proxy.internal:3128against a proxy that speaks plain HTTP gives curl and most CLIs the samewrong version number. The scheme in the proxy URL is how the client talks to the proxy, so it is usuallyhttp://. -
A port or redirect mix-up. An app that redirects to
https://example.com:8080/where 8080 is plain HTTP, or a load balancer listener on 443 forwarding raw TCP to a backend port that expects TLS on another port.
2. TLS version or cipher mismatch
A server that only offers TLS 1.0 or 1.1 does not usually produce ERR_SSL_PROTOCOL_ERROR. Chrome stopped connecting to those servers entirely in Chrome 98 (after a click-through warning period starting in Chrome 84), and it reports them as ERR_SSL_VERSION_OR_CIPHER_MISMATCH with the text “example.com uses an unsupported protocol”. RFC 8996 formally deprecates both versions. The same code appears when the server and browser share no cipher suite.
You get ERR_SSL_PROTOCOL_ERROR from version problems when the server mishandles a modern ClientHello instead of refusing it cleanly: an old TLS stack or appliance that sends an unexpected alert, or garbage, in reply to TLS 1.3. Probe each version separately:
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null 2>&1 | grep -E 'Protocol|Cipher|alert|error'
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null 2>&1 | grep -E 'Protocol|Cipher|alert|error'
A server that refuses TLS 1.3 cleanly answers with a protocol_version alert:
error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version:ssl/record/rec_layer_s3.c:918:SSL alert number 70
and the TLS 1.2 probe succeeds with New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384. That server is fine for Chrome. If both probes fail, test the legacy versions. OpenSSL 3 disables TLS 1.0 and 1.1 at its default security level, so lower it for the probe:
openssl s_client -connect example.com:443 -servername example.com -tls1 -cipher 'DEFAULT:@SECLEVEL=0' </dev/null
For the full picture of what the server accepts, with a letter grade per cipher suite:
nmap --script ssl-enum-ciphers -p 443 example.com
The output lists each protocol version the server accepts (TLSv1.0:, TLSv1.2: and so on) with its cipher suites, plus warnings such as 64-bit block cipher 3DES vulnerable to SWEET32 attack. The fix is to enable TLS 1.2 and 1.3 with ECDHE and AES-GCM or ChaCha20 suites: in nginx ssl_protocols TLSv1.2 TLSv1.3;, in Apache SSLProtocol -all +TLSv1.2 +TLSv1.3.
3. Broken SNI, or no site for this hostname
On a shared IP the server picks the site and certificate from the Server Name Indication the client sends in the ClientHello (SNI, RFC 6066 section 3). When the hostname is not configured, what the browser sees depends on the server:
- nginx with
ssl_reject_handshake onin the default server sends a fatalunrecognized_namealert, which Chrome reports asERR_SSL_UNRECOGNIZED_NAME_ALERT, notERR_SSL_PROTOCOL_ERROR. - Servers that abort with
internal_erroror another generic alert, or that drop the connection, produceERR_SSL_PROTOCOL_ERROR. - Servers that fall back to a default certificate for another site produce
NET::ERR_CERT_COMMON_NAME_INVALIDinstead.
Compare the handshake with and without SNI:
openssl s_client -connect 203.0.113.10:443 -servername example.com </dev/null 2>&1 | grep -E 'alert|subject=|Protocol'
openssl s_client -connect 203.0.113.10:443 -noservername </dev/null 2>&1 | grep -E 'alert|subject=|Protocol'
If the first fails and the second works, the server has a TLS site but not for example.com: add the hostname to server_name in the TLS server block (or the matching vhost, ingress host or load balancer certificate list). If you test by IP address, always pass -servername, because browsers never send an IP as SNI.
4. A middlebox, antivirus or corporate proxy intercepting TLS
When the site works from your network and fails for some users, something between them and you is terminating or inspecting TLS. Typical sources:
- Corporate TLS inspection (next-generation firewalls, secure web gateways) that cannot parse something in Chrome’s ClientHello. Chrome’s post-quantum key share made the ClientHello too large for one packet: Chrome turned on a hybrid Kyber key share by default on desktop in Chrome 124 and moved to the standardized X25519MLKEM768 in Chrome 131, and inspection devices that assumed the ClientHello arrives in a single packet broke. Fortinet, for example, documented
ERR_SSL_PROTOCOL_ERRORwith flow-based deep inspection and ML-KEM. Google offered thePostQuantumKeyAgreementEnabledenterprise policy as a temporary escape hatch and said it would be removed after Chrome 145; the lasting fix is a firmware update from the vendor. - Antivirus HTTPS scanning that re-signs traffic, especially older versions that mishandle TLS 1.3.
- A firewall blocking the site with a TLS alert. Some firewalls send an
access_deniedalert when they block a page. Per the spec that alert is only for client certificates, so Chrome maps it toERR_SSL_PROTOCOL_ERRORwhen it never received a certificate request. Cloudflare Zero Trust Gateway block pages produce the same error when the Cloudflare root certificate is not installed on the device.
To prove interception, compare the certificate issuer from the failing machine with the one from your own (check 2 under “Who sent it?”). Then test from the same machine with security software paused, or over a phone hotspot or VPN. If the error disappears off the corporate network, take it to whoever owns the inspection policy, with a Chrome net log from chrome://net-export.
5. Cloudflare: what SSL mode does and does not affect
The SSL/TLS encryption mode controls only the Cloudflare-to-origin connection. In Flexible mode Cloudflare talks to your origin over plain HTTP; in Full and Full (strict) it uses HTTPS. A mismatch there does not produce ERR_SSL_PROTOCOL_ERROR in the visitor’s browser, because the browser’s handshake is with Cloudflare. It produces a Cloudflare error page instead:
- Full or Full (strict) with an origin that has no working TLS on 443 gives 525 SSL handshake failed (or 521 if nothing listens). An origin with
listen 443;withoutsslis a classic 525. - Flexible with an origin that redirects HTTP to HTTPS gives a redirect loop (ERR_TOO_MANY_REDIRECTS), because Cloudflare keeps fetching the origin over HTTP. Cloudflare’s docs say not to use Flexible when the origin forces HTTPS.
- Flexible applies only to HTTPS on port 443. Cloudflare falls back to Full mode for HTTPS on other ports, so a non-standard port suddenly needs TLS on the origin.
When the browser itself shows ERR_SSL_PROTOCOL_ERROR on a Cloudflare hostname, the browser-to-edge handshake failed. Cloudflare’s troubleshooting page lists, in order:
- The edge certificate is not active yet (a recently added domain waiting for Universal SSL), or the hostname is a multi-level subdomain such as
dev.docs.example.com, which Universal SSL does not cover. - Intermittent failures for some visitors: temporarily turn off HTTP/3 (with QUIC) under Protocol Optimization. If that fixes it, the visitor’s network blocks or mangles UDP on port 443.
- Failures on corporate networks or with antivirus HTTPS scanning: temporarily turn off TLS 1.3 under SSL/TLS > Edge Certificates. If that fixes it, a middlebox on the visitor’s side cannot handle TLS 1.3. Turn it back on afterwards; this is a diagnostic, not a fix.
- Ask the visitor for the output of
https://example.com/cdn-cgi/trace, which shows which Cloudflare data center, TLS version and HTTP version they reach.
Also check whether the DNS record is proxied. A DNS-only (grey cloud) record sends the browser straight to your origin, so every origin cause above applies, and Cloudflare’s certificate is not involved at all.
Reproduce and verify
# Full handshake as curl sees it; look at the lines starting with "*"
curl -v https://example.com/ -o /dev/null
# Speak plain HTTP to the TLS port and see what comes back
curl -si --max-time 5 http://example.com:443/ | head -n 12
# Handshake details: protocol, cipher, certificate subject
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep -E '^(New|Protocol|Verify return code)|subject='
The plain-HTTP probe separates the two cases. If port 443 returns your normal page (a 200, or the redirect your site usually sends), it is serving plain HTTP and you have found the problem. A correctly configured nginx TLS port answers that probe with 400 Bad Request and the body text The plain HTTP request was sent to HTTPS port, which is the 497 case seen from the other side. Other TLS servers close the connection, and curl prints Empty reply from server.
A fixed server shows New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 (or a TLS 1.2 ECDHE suite) and Verify return code: 0 (ok). Then reload the page in Chrome; if the error persists only in the browser, clear the HSTS entry and test in a fresh profile, so a cached HSTS or certificate decision is not hiding the fix.
Related
- NET::ERR_CERT_COMMON_NAME_INVALID is the next error you meet once the handshake works but the certificate does not match.
- ERR_CONNECTION_RESET covers handshakes killed by a TCP RST rather than a TLS alert.
- 525 SSL Handshake Failed is the same class of failure on Cloudflare’s leg to your origin.
- 497 HTTP Request Sent to HTTPS Port is the mirror image: plain HTTP sent to a TLS port.
- TLS handshake and HTTPS and TLS explain what each step of the handshake does.
Frequently asked questions
Why do I get ERR_SSL_PROTOCOL_ERROR on https://localhost?
Your dev server speaks plain HTTP on that port, and Chrome is sending it a TLS ClientHello. The server answers with something like "HTTP/1.1 400 Bad Request", which Chrome cannot parse as TLS. Use http://localhost:3000, or start the dev server with HTTPS enabled. If Chrome keeps forcing https:// for localhost, delete the HSTS entry at chrome://net-internals/#hsts.
What does "wrong version number" mean in curl or openssl?
The first bytes the server sent were not a TLS record. OpenSSL reads the response's first five bytes as a record header, and the "TT" in "HTTP/" lands where the protocol version should be, so it reports a wrong version. In practice it almost always means the port is serving plain HTTP, or an HTTPS_PROXY setting points at a proxy that does not speak TLS.
Does a server that only supports TLS 1.0 or 1.1 cause ERR_SSL_PROTOCOL_ERROR?
Usually not. Chrome removed TLS 1.0 and 1.1 in Chrome 98 and reports those servers as ERR_SSL_VERSION_OR_CIPHER_MISMATCH ("uses an unsupported protocol"). ERR_SSL_PROTOCOL_ERROR is the catch-all Chrome uses when the TLS exchange breaks in a way that has no more specific error code, such as garbage bytes, an unexpected alert or a middlebox fault.
Can Cloudflare SSL mode cause ERR_SSL_PROTOCOL_ERROR?
Not directly. The SSL mode (Flexible, Full, Full strict) only controls the Cloudflare-to-origin connection, and failures there show as Cloudflare error pages such as 521, 525 or 526, or as a redirect loop with Flexible. If the browser shows ERR_SSL_PROTOCOL_ERROR on a proxied hostname, the browser-to-Cloudflare handshake failed: check that the edge certificate is active, that the hostname is not a multi-level subdomain, and whether TLS 1.3 or HTTP/3 is tripping a middlebox on the visitor's network.
Why does the site work on one network but show ERR_SSL_PROTOCOL_ERROR on another?
Something on the failing network is touching the TLS connection: a corporate firewall doing TLS inspection, antivirus HTTPS scanning, or an ISP filter. Compare the certificate issuer with openssl s_client from both networks. If the issuer on the failing network is a security vendor instead of your CA, the connection is being intercepted.
Sources
- MDN: Transport Layer Security (TLS)developer.mozilla.org
- RFC 8446 Section 6: Alert Protocol (TLS 1.3)rfc-editor.org
- RFC 8996: Deprecating TLS 1.0 and TLS 1.1rfc-editor.org
- RFC 6066 Section 3: Server Name Indicationrfc-editor.org
- Chromium source: net_error_list.hsource.chromium.org
- Cloudflare Docs: Troubleshoot ERR_SSL_PROTOCOL_ERRORdevelopers.cloudflare.com
- Cloudflare Docs: Flexible SSL modedevelopers.cloudflare.com
- nginx: ngx_http_ssl_modulenginx.org
- Chrome Enterprise: PostQuantumKeyAgreementEnabled policychromeenterprise.google
Related
NET::ERR_CERT_COMMON_NAME_INVALID: Fix the Name Mismatch
Fix NET::ERR_CERT_COMMON_NAME_INVALID and related ERR_CERT errors: SAN vs CN, wildcard limits, missing SNI, expired or incomplete chains, with openssl checks.
HTTPS Explained: How TLS Secures HTTP
HTTPS is HTTP over TLS. What the TLS handshake does, how certificates prove identity, what HTTPS hides and what it does not, and how to migrate a site safely.
497 HTTP Request Sent to HTTPS Port (nginx)
nginx 497 means plain HTTP hit an HTTPS port; clients see "400 The plain HTTP request was sent to HTTPS port". Fix with error_page 497. Also 494, 495, 496.
525 SSL Handshake Failed (Cloudflare)
Cloudflare 525 means the TLS handshake with your origin server failed. Diagnose with openssl s_client, check SSL modes, ciphers, SNI and port 443, and fix it.