Fix ERR_SSL_PROTOCOL_ERROR on a Django Site
Fix the browser error ERR_SSL_PROTOCOL_ERROR or SSL_ERROR_RX_RECORD_TOO_LONG for a Django site: find which layer answers HTTPS with plain HTTP, fix listen 443 ssl in Nginx, port mappings, proxy upstream schemes, and cached HSTS for development servers.
Problem statement
Chrome shows "This site can’t provide a secure connection" with ERR_SSL_PROTOCOL_ERROR. Firefox says SSL_ERROR_RX_RECORD_TOO_LONG. curl reports:
curl: (35) error:0A00010B:SSL routines::wrong version number
All three mean the browser started a TLS handshake and got something that is not TLS back, almost always a plain HTTP response from a server listening on port 443 without TLS, or from a port that should never have been reached over HTTPS. The fix is to find which layer answered and make it speak TLS, or to stop sending HTTPS to it.
- Test what the port speaks:
curl -v http://example.com:443/. If that returns HTML, port 443 serves plain HTTP. - In Nginx, the listener needs
ssl:listen 443 ssl;plus certificate directives. - Check port mappings: Docker
"443:80"maps HTTPS traffic to a plain HTTP port. - If only
localhost:8000fails, the browser is forcing HTTPS on the development server; see the runserver fix. - Behind Cloudflare or a load balancer, make sure the origin port and scheme match the proxy's SSL mode.
Step-by-step solution
1. Ask the port which protocol it speaks
curl -sv http://example.com:443/ -o /dev/null 2>&1 | grep -E "^< HTTP|Connected"
openssl s_client -connect example.com:443 -servername example.com </dev/null | head -5
If plain http:// on port 443 returns an HTTP status, the listener has no TLS. If openssl s_client shows a certificate chain, TLS works on the server and the problem is elsewhere, such as a proxy in between or a client issue.
2. Fix the Nginx listener
A common mistake is listen 443; without ssl, or certificate lines in a different server block:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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;
location / { proxy_pass http://app; }
}
Also check for a second server block, perhaps a default site, that has listen 443 without ssl. Nginx applies the ssl flag per address and port, and conflicting definitions give confusing results. sudo nginx -T | grep -n "listen" shows every listener.
3. Check container port mappings
In Docker Compose, ports: ["443:80"] publishes host port 443 but sends traffic to the container's plain HTTP port 80. Browsers then get HTTP on 443. Map 443 to the port where the proxy container terminates TLS:
proxy:
image: caddy:2
ports: ["80:80", "443:443"]
Gunicorn itself should not be exposed on 443 unless it is configured with certificates, which is unusual.
4. Check proxy upstream schemes
A proxy that forwards with proxy_pass https://127.0.0.1:8000 makes Nginx start TLS to Gunicorn. Nginx logs an upstream SSL error and the browser may see a 502 rather than a protocol error, but load balancers configured with an HTTPS target group pointing at a plain HTTP port produce the same mismatch one hop later. Use plain HTTP to app servers on a private network and terminate TLS at the edge.
5. Development servers
If the error appears only for localhost:8000, the browser upgraded to HTTPS because of cached HSTS or a redirect. Django's runserver speaks only HTTP. Follow Fix "You’re Accessing the Development Server Over HTTPS".
Explanation
A TLS connection begins with the client sending a ClientHello. A plain HTTP server reading those bytes either ignores them or replies with an HTTP error page in plain text. The browser then reads HTTP/1.1 400 Bad Request where it expected a TLS record header, sees an impossible record length or version, and aborts with ERR_SSL_PROTOCOL_ERROR or SSL_ERROR_RX_RECORD_TOO_LONG. The error therefore always points to a listener that speaks the wrong protocol for the URL.
Edge cases / notes
- Antivirus and proxies. Some endpoint security tools intercept TLS and break it for certain sites; test from another network to rule them out.
- Old TLS versions. A server configured with only very old or very new protocol versions can give a handshake failure instead; that is a different error, usually
ERR_SSL_VERSION_OR_CIPHER_MISMATCH. - IPv6. A missing
listen [::]:443 ssl;makes IPv6 clients hit a different server block or nothing.
Next steps
- For HTTPS that never connects at all, see Fix HTTPS Not Working for Django With Nginx.
- Set up certificates correctly with Configure HTTPS With Let’s Encrypt for Django.
- Behind Cloudflare, check Fix Cloudflare 525 and 526 SSL Errors.
FAQ
What does SSL_ERROR_RX_RECORD_TOO_LONG mean in Firefox?
The same as Chrome's ERR_SSL_PROTOCOL_ERROR: the server answered a TLS handshake with plain HTTP.
Is my certificate wrong?
Usually not. Certificate problems produce certificate errors. This error means TLS was not spoken at all.
Can Gunicorn serve HTTPS directly?
It can with --certfile and --keyfile, but terminating TLS in Nginx, Caddy, or a load balancer is the common and simpler setup.
Why does it work with http:// but not https://?
The server listens for HTTP only. Add a TLS listener with certificates.