The browser error SSL_ERROR_RX_RECORD_TOO_LONG (frequently accompanied in Chrome by ERR_SSL_PROTOCOL_ERROR) indicates a fundamental wire-protocol mismatch: the client sent a TLS ClientHello to port 443 expecting an encrypted TLS handshake, but the server responded with unencrypted, plaintext HTTP.
Because the browser's cryptographic engine treats incoming data on port 443 as a TLS frame, it parses ASCII characters (such as HTTP/1.1 200 OK) as numeric binary lengths. This triggers an immediate parser exception when the computed record length exceeds the protocol's 16 KB boundary.
This guide details the byte-level root cause under RFC 8446, CLI diagnostic workflows, step-by-step configuration fixes across Nginx, Apache, and Cloudflare, and automated monitoring strategies.
[!TIP] Free Diagnostic Tools for Webmasters & SREs:
- π Pingzo Free SSL Inspector β Test whether your domain's port 443 returns valid TLS records or plaintext HTTP responses.
- π Pingzo HTTP Header Checker β Inspect raw response headers, redirects, and HTTP status codes over port 80/443.
- β‘ Pingzo Free Ping & Port Test β Check TCP socket reachability and packet latency across global nodes.
1. Protocol Anatomy: Why Plaintext HTTP Looks Like an "Oversized" TLS Record
In the TLS Record Layer (RFC 5246 / RFC 8446), every TLS record begins with a mandatory 5-byte header:
TLS RECORD HEADER FORMAT
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ContentType(8)| ProtocolVersion(16) | Length(16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Fragment Data ... |
In standard TLS framing, the record structure is represented as:
$$\text{TLS Record} = \text{ContentType}{8\text{ bits}} + \text{LegacyVersion}{16\text{ bits}} + \text{Length}{16\text{ bits}} + \text{Fragment}{\text{Length}}$$
Per RFC 8446, the maximum allowed length of an uncompressed TLS plaintext fragment is:
$$2^{14} = 16,384\text{ bytes (16 KiB)}$$
The ASCII-to-Binary Byte Collision
When a web server is misconfigured to serve unencrypted HTTP on port 443, it responds to the client's TLS handshake with an ASCII HTTP response line:
HTTP/1.1 200 OK
The browser receives the first four ASCII bytes:
$$\text{Bytes: } \text{0x48 ('H')} \quad \text{0x54 ('T')} \quad \text{0x54 ('T')} \quad \text{0x50 ('P')}$$
The browser's TLS parser reads the third and fourth bytes (0x48 0x54) as the 16-bit Length field:
$$\text{Length Value} = (\text{0x48} \times 256) + \text{0x54} = (72 \times 256) + 84 = 18,516\text{ bytes}$$
Because $18,516\text{ bytes} > 16,384\text{ bytes}$, the TLS parser immediately aborts the connection with SSL_ERROR_RX_RECORD_TOO_LONG. The error does not mean the packet is physically largeβit means plaintext ASCII text was received where binary TLS framing was expected.
2. Comparative Matrix: HTTP vs TLS Handshake Signatures
ββββββββββββββββββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β Connection Stage β Plain HTTP (Port 80) β HTTPS / TLS (Port 443) β
ββββββββββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ€
β Transport Layer β TCP 3-Way Handshake β TCP 3-Way Handshake β
β First Client Payload β GET / HTTP/1.1 β TLS ClientHello (0x16 0x03) β
β First Server Payload β HTTP/1.1 200 OK β TLS ServerHello + Certificate β
β Failure Signature β 400 Bad Request β SSL_ERROR_RX_RECORD_TOO_LONG β
β Nginx Directive β listen 80; β listen 443 ssl; β
β Apache Directive β <VirtualHost *:80> β <VirtualHost *:443> + SSLEngineβ
β Wire First Bytes β 0x48 0x54 ("HT") β 0x16 0x03 0x01 / 0x03 β
ββββββββββββββββββββββββ΄βββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββ
3. Step-by-Step CLI Diagnostic Procedures
Follow these structured terminal commands to isolate which machine, port, or proxy layer is emitting plaintext responses:
1. Inspect the TLS Handshake via OpenSSL
Execute openssl s_client with the -servername (SNI) flag:
openssl s_client -connect example.com:443 -servername example.com
- Healthy Output: Prints the full certificate chain (
Certificate chain 0 s:CN=example.com) and negotiated cipher suite (Protocol : TLSv1.3). - Broken Plaintext Output: Outputs
error:0A00010B:SSL routines:ssl3_get_record:wrong version numberor prints raw HTML/HTTP headers (HTTP/1.1 400 Bad Request).
2. Probe Protocol Headers with cURL
Send an insecure verbose request to inspect both connection and TLS state:
curl -vIk https://example.com:443/
If the endpoint is returning HTTP on port 443, cURL outputs:
OpenSSL SSL_connect: SSL_ERROR_SYSCALL or Received HTTP/0.9 when not allowed.
3. Verify the Process Bound to Port 443
Log into the origin host and inspect active TCP listeners:
sudo ss -lntp | grep -E ':(80|443)\b'
Ensure the intended web server process (e.g., nginx or apache2) owns port 443 rather than an unencrypted internal service, Docker container, or legacy proxy.
4. How to Fix in Nginx
In modern Nginx releases, SSL is enabled via the ssl parameter on the listen directive.
flowchart TD
A["Incoming Request on Port 443"] --> B{"Nginx Listen Directive?"}
B -- "listen 443;" --> C["Plaintext HTTP Handler -> Error RX_RECORD_TOO_LONG"]
B -- "listen 443 ssl;" --> D["TLS Engine Initialized -> ServerHello Sent"]
B -- "ssl on; (Deprecated)" --> E["Syntax Warning or Failure in Nginx 1.25+"]
1. Add the ssl Parameter to the listen Directive
A common mistake is configuring SSL certificates inside a server block that is missing the ssl flag:
# β BROKEN: Listens on port 443 in plaintext HTTP mode
server {
listen 443;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}
# β
FIXED: Explicitly enables TLS on port 443
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.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;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/example;
location / {
try_files $uri $uri/ =404;
}
}
2. Remove Deprecated ssl on; Directives
Nginx deprecated ssl on; in version 1.15.0 and removed it in 1.25.1+. Replace any legacy ssl on; declarations with listen 443 ssl;.
3. Fix Default Server Catch-All Blocks
If an unmatched request hits an unencrypted catch-all block listening on 443, it triggers a protocol mismatch:
# β
PROPER TLS DEFAULT SERVER BLOCK
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default.crt;
ssl_certificate_key /etc/nginx/ssl/default.key;
return 444; # Closes unauthenticated connections silently
}
4. Validate and Reload Nginx
sudo nginx -t && sudo systemctl reload nginx
5. How to Fix in Apache (httpd)
Apache requires the mod_ssl module and the explicit SSLEngine on directive within a dedicated <VirtualHost *:443> block.
1. Enable mod_ssl
On Debian and Ubuntu systems:
sudo a2enmod ssl
sudo systemctl restart apache2
On RHEL / Rocky Linux / CentOS:
sudo dnf install mod_ssl
sudo systemctl restart httpd
2. Add SSLEngine on to the Port 443 VirtualHost
If an Apache virtual host binds to port 443 without SSLEngine on, Apache serves unencrypted HTTP over port 443:
# β BROKEN: Missing SSLEngine directive
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example
</VirtualHost>
# β
FIXED: Production-ready Apache HTTPS configuration
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
<Directory /var/www/example>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
3. Verify VirtualHost Mappings
Check for overlapping virtual host bindings using apachectl:
sudo apachectl -S
Ensure *:80 and *:443 virtual hosts are strictly segregated. Then test syntax and reload:
sudo apachectl configtest && sudo systemctl reload apache2
6. Cloudflare & Reverse Proxy Configuration
Cloudflare operates as a reverse proxy with two distinct connection legs:
ββββββββββββ Leg 1: Client to Cloudflare ββββββββββββββ Leg 2: Cloudflare to Origin βββββββββββ
β Browser β ββββββββββββββββββββββββββββ> β Cloudflare β ββββββββββββββββββββββββββββ> β Origin β
ββββββββββββ (Strict HTTPS) ββββββββββββββ (Mode-Dependent) βββββββββββ
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β Cloudflare Mode β Leg 2 (Origin Protocol)β Origin Requirement β
ββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ€
β Flexible β Plaintext HTTP (Port 80)β Port 80 only (NO HTTPS) β
β Full β HTTPS (Port 443) β Port 443 SSL (Self-signed OK)β
β Full (Strict) β HTTPS (Port 443) β Valid, trusted CA SSL cert β
ββββββββββββββββββββββββ΄ββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββ
The Flexible Mode Redirect Loop & Protocol Trap
When Cloudflare is set to Flexible Mode, Cloudflare connects to your origin server over port 80 (HTTP). If your origin web server (Nginx/Apache) is configured with an unconditional HTTP $\to$ HTTPS redirect (return 301 https://$host$request_uri;), an infinite redirect loop (ERR_TOO_MANY_REDIRECTS) occurs.
If the origin firewall or router maps external 443 directly to port 80 without terminating TLS, clients connecting directly bypass Cloudflare and encounter SSL_ERROR_RX_RECORD_TOO_LONG.
Fix: Set Cloudflare SSL/TLS encryption mode to Full (Strict) and ensure your origin server has a valid SSL certificate (such as Let's Encrypt or a Cloudflare Origin CA certificate) bound with listen 443 ssl; or SSLEngine on.
7. NAT Port-Forwarding & Docker Mapping Collisions
A frequent cause in self-hosted and containerized environments is misconfigured port forwarding on routers or firewalls:
Internet (WAN) 443 ββ[ Router NAT: 443 -> 80 ]ββ> Internal Server:80 (Plaintext HTTP)
- Verify Router NAT Rules: Confirm external TCP port 443 forwards strictly to internal TCP port 443.
- Verify Docker Port Mappings: In
docker-compose.yml, confirm you have mapped443:443rather than mapping host 443 to container 80:
# β BROKEN DOCKER PORT MAPPING
ports:
- "443:80"
# β
CORRECT DOCKER PORT MAPPING
ports:
- "80:80"
- "443:443"
8. Prevent SSL Protocol Failures with Automated Monitoring
Protocol collisions and certificate configuration bugs often surface during server reboots, container redeployments, or automated certificate renewals.
Proactive monitoring ensures your operations team receives instant alerts before users encounter broken handshake errors.
βββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ
β Metric / Check β Verification Purpose β
βββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ€
β TCP Port 443 Availability β Verifies socket accepts inbound TCP β
β TLS Handshake & SNI Validation β Confirms binary TLS ServerHello response β
β SSL Certificate Expiry Tracking β Alerts 30, 14, 7, and 1 day before expiry β
β Multi-Region Response Latency β Detects routing loops and proxy delays β
β Instant Alert Channels β Dispatches down alerts via WhatsApp/Slack β
βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββ
Pingzo Monitoring Plans & Features
| Feature / Plan | Free | Starter ($5/mo) | Pro ($12/mo) | Agency ($29/mo) |
|---|---|---|---|---|
| Monitors Included | 1 Monitor | 10 Monitors | Unlimited | Unlimited |
| Check Frequency | 15 Minutes | 5 Minutes | 2 Minutes | 1 Minute |
| TLS & SSL Certificate Checks | Included | Included | Included | Included |
| Alert Channels | WhatsApp, Slack, Telegram | WhatsApp, Slack, SMS | Multi-Team Routing | |
| SSL Expiry Countdown | 7 Days | 30 Days | 90 Days | 365 Days |
| Status Pages | Pingzo Branded | Branded | Custom Domain | White-Label |
Conclusion & Troubleshooting Checklist
When diagnosing SSL_ERROR_RX_RECORD_TOO_LONG, remember that the error is a wire-level protocol mismatch, not an invalid certificate:
- Run OpenSSL Diagnostic: Verify if
openssl s_client -connect yourdomain.com:443returns raw HTTP text or an OpenSSL certificate chain. - Check Nginx: Ensure
listen 443 ssl;has thesslparameter enabled. - Check Apache: Confirm
SSLEngine onis present inside<VirtualHost *:443>andmod_sslis enabled. - Audit Cloudflare: Set encryption mode to Full (Strict).
- Inspect NAT & Docker: Verify external port 443 routes to internal port 443.
π Monitor Your SSL Certificates & Handshakes with Pingzo β Set up multi-region SSL expiry and TLS handshake monitors with instant WhatsApp alerts in under 3 minutes.
Stop Finding Out About Outages from Angry Users
Get instant WhatsApp & Discord alerts the second your API, website, or server goes down. Setup in 30 seconds with 60-second checks.