Python Requests SSLError: UNEXPECTED_EOF_WHILE_READING on specific HTTPS hosts

Root Cause Diagnosis

The error [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol means the peer closed the TCP connection without sending a TLS close_notify alert, or without completing the handshake. OpenSSL 3.0+ treats this as a fatal error, while OpenSSL 1.1.1 silently tolerated it. Python 3.10+ builds linked against OpenSSL 3.x therefore fail only on hosts whose servers, load balancers, WAFs, or middleboxes close sockets abruptly or reject the client’s TLS parameters.

Quick Fix

Make the TLS context ignore unexpected EOF (requires Python 3.10+ built against OpenSSL 3.0+):

import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context

class EOFTolerantAdapter(HTTPAdapter):
    def _ctx(self):
        ctx = create_urllib3_context()
        # ssl.OP_IGNORE_UNEXPECTED_EOF == 0x80
        ctx.options |= getattr(ssl, "OP_IGNORE_UNEXPECTED_EOF", 0x80)
        return ctx

    def init_poolmanager(self, *args, **kwargs):
        kwargs["ssl_context"] = self._ctx()
        super().init_poolmanager(*args, **kwargs)

    def proxy_manager_for(self, *args, **kwargs):
        kwargs["ssl_context"] = self._ctx()
        return super().proxy_manager_for(*args, **kwargs)

session = requests.Session()
session.mount("https://", EOFTolerantAdapter())
r = session.get("https://example.com", timeout=15)

Mount the adapter only on the affected host (session.mount("https://affected.example.com", ...)) to avoid relaxing truncation detection globally.

Step-by-Step Resolution

1. Record the runtime versions.

python -c "import ssl, sys; print(sys.version); print(ssl.OPENSSL_VERSION)"
python -m pip list 2>/dev/null | grep -iE "^(requests|urllib3|certifi|pyopenssl) "

An OpenSSL 3.x string confirms the strict EOF handling is active.

2. Determine where the EOF occurs. Test the handshake outside Python:

openssl s_client -connect HOST:443 -servername HOST -tls1_2 </dev/null
openssl s_client -connect HOST:443 -servername HOST -tls1_3 </dev/null
curl -v --tlsv1.2 https://HOST/
ResultMeaningAction
Handshake completes, data transfers, then abrupt closeServer omits close_notifyQuick Fix above
Handshake fails with unexpected eof or connection reset before certificate outputServer or middlebox rejects the ClientHelloSteps 3 and 4
Works with -tls1_2 only or -tls1_3 onlyProtocol version mismatchStep 3

3. Pin or relax TLS parameters for the affected host.

ctx = create_urllib3_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.maximum_version = ssl.TLSVersion.TLSv1_2   # force TLS 1.2 if TLS 1.3 handshakes are dropped
ctx.set_ciphers("DEFAULT:@SECLEVEL=1")         # only for legacy servers; weakens security
ctx.options |= 0x80

Reduce the security level only for servers you control or trust. Note that @SECLEVEL=1 is a different failure class (DH_KEY_TOO_SMALL, UNSAFE_LEGACY_RENEGOTIATION_DISABLED). Apply it only if openssl s_client shows a weak-parameter negotiation.

See also  Cloudflare Turnstile Infinite Loop on Automated Chromium: Headless Bypass Flags

4. Rule out WAF and bot-filter resets. Some CDNs and WAFs reset the connection based on the ClientHello fingerprint or a missing User-Agent:

session.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept": "*/*",
})

If curl succeeds and Python still fails after this, the block is likely fingerprint-based (JA3/JA4). Use a client with a browser-like TLS profile (e.g. curl_cffi) or request an allowlist from the host operator.

5. Bypass or fix the proxy path.

session.trust_env = False                      # ignore HTTP(S)_PROXY / NO_PROXY env vars
r = session.get(url, proxies={"https": None})  # or test explicitly without a proxy

If the request works without the proxy, the proxy is terminating TLS or closing tunnels abruptly. Check HTTPS_PROXY, ALL_PROXY, and REQUESTS_CA_BUNDLE.

6. Handle stale pooled connections. If the failure appears only on the second or later request in a session, the server closed an idle keep-alive socket:

from urllib3.util.retry import Retry

adapter = EOFTolerantAdapter(max_retries=Retry(total=3, backoff_factor=0.5,
                                               allowed_methods=None))
session.mount("https://", adapter)

Alternatively, send Connection: close or create a new Session per host.

7. Update the dependencies.

python -m pip install -U requests urllib3 certifi

Do not pin urllib3<2 as a fix. It only changes the code path and does not change the OpenSSL behavior.

Why This Fails for Some Users

  • OpenSSL version split: Ubuntu 20.04 (OpenSSL 1.1.1) and older Python builds never raise this error. Ubuntu 22.04+, Debian 12, current python:3.x Docker images, and Python.org macOS and Windows installers (bundled OpenSSL 3.x) do. The same script behaves differently across machines for this reason.
  • OP_IGNORE_UNEXPECTED_EOF unavailable: Python 3.9 and earlier, or builds on OpenSSL 1.1.1, do not have it. Those builds do not raise this error in the first place.
  • Handshake-stage EOF is not fixed by the flag: The option only suppresses EOF after the handshake. If the server drops the ClientHello (unsupported version or cipher, missing SNI, fingerprint block), you must change the TLS parameters or the network path.
  • Large ClientHello and post-quantum key shares: OpenSSL 3.5+ sends hybrid post-quantum key shares by default (e.g. X25519MLKEM768), enlarging the ClientHello. Faulty middleboxes and old servers may drop it. Test by restricting groups, e.g. ctx.set_ecdh_curve("prime256v1"), or openssl s_client -groups X25519.
  • TLS-inspecting corporate proxies and antivirus: These re-sign and reset connections inconsistently. Python uses the certifi bundle, not the OS trust store, so also set REQUESTS_CA_BUNDLE to the corporate CA file if certificate errors accompany the EOF.
  • MTU and IPv6 path issues: Fragmented packets dropped on the path appear as EOF during the handshake. Test with curl -4 versus curl -6, and lower the interface MTU (e.g. ip link set dev eth0 mtu 1400) for diagnosis.
  • Rate limiting and connection caps: Servers that cap concurrent connections per IP close new sockets without TLS shutdown. Reduce concurrency and reuse a single Session.
  • Security tradeoff: OP_IGNORE_UNEXPECTED_EOF weakens detection of TLS truncation attacks. Never combine it with verify=False, and scope it to specific hosts.
See also  Fixing urllib3 ConnectionResetError(104, 'Connection reset by peer') during bulk API requests

Leave a Comment