SSL EOF occurred in violation of protocol behind a proxy: was the request sent?

[SSL: UNEXPECTED_EOF_WHILE_READING], SSLEOFError or curl error 0A000126 through a proxy: was the request sent, can you retry, and which side should you check?

On this page

A failure in the TLS handshake means that attempt was not sent, unless you turned on TLS early data. A failure after the handshake means the outcome is unknown, and the target may already have your request. Either way your last error line describes one attempt, not the whole job. The string says the connection ended while TLS still expected data, and it doesn't say who ended it.

No proxy that you know of? Check that first: many public reports of these strings involve none.

Earlier attempts are a separate question. A mounted Retry or max_retries, urllib3 used directly, an SDK with built-in retries or a followed redirect can put a POST at the target even when the last line reads as a handshake EOF. Behind an HTTP proxy those retry settings can re-send a POST by themselves, including Retry(total=3, allowed_methods={"GET"}).

Your last lineThis attempt
SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)')), <urlopen error [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)>, or that text with no prefix, with a low _ssl.c number (we saw 1000 to 1147)Not sent
httpx.ConnectError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)Not sent
ProxyError('Unable to connect to proxy', SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)')))Not sent. If your proxy URL says https:// and the port speaks plain HTTP, write http://
curl: (35) TLS connect error: error:0A000126:SSL routines::unexpected eof while reading, curl: (35) Send failure: Broken pipe or curl: (35) Recv failure: Connection reset by peerNot sent. With --tls-earlydata, unknown
curl: (56) OpenSSL SSL_read: OpenSSL/3.5.5: error:0A000126:SSL routines::unexpected eof while reading, errno 0Unknown
ProxyError('Unable to connect to proxy', RemoteDisconnected('Remote end closed connection without response'))Unknown
SSLEOFError with a high _ssl.c number (we saw 2406 to 3042), or [SSL: UNEXPECTED_EOF_WHILE_READING] unexpected eof while reading (_ssl.c:2711)Unknown

These results are synthetic: our own scripted proxies and target on one machine (the fixture) and named proxy software on loopback.

  • Not sent, on a first attempt with no redirect: a retry can't duplicate anything.
  • Not sent after retries or a redirect, or Unknown: look for the result at the target before you send a POST again. Give it time: on the fixture a relay that held the request delivered it after the client had printed its error (scripted timing), so an empty lookup straight after the failure proves nothing. With no lookup or idempotency key at the target, the outcome stays unknown: ask its operator, or decide whether a duplicate is worse than a lost request.

A followed redirect counts as an earlier attempt: the POST may already sit at the first URL, and the last line can't rule that out. Requests and urllib3 follow them by default and print the next hop, which can be the address you posted to. urllib.request, aiohttp, HTTPX's follow_redirects=True and curl -L print no path. If this endpoint can redirect, treat the POST as unknown, or send it with redirects off.

For a safe GET or HEAD a duplicate shouldn't change anything at the target, so go straight to the comparison below. No client-side check can always name the side that closed; the comparison narrows it, and the table by path gives the next action.

pip ends on ERROR: No matching distribution found, so read the WARNING: Retrying lines above it. Its There was a problem confirming the ssl certificate line blames the certificate, but no certificate was involved. A package download has nothing to duplicate, so a retry is safe, but pip has already retried: next, check which proxy pip picks up.

The _ssl.c numbers change between builds, but CPython's source has kept handshake lines at or below 1176 and read or write lines at or above 1844 from 3.5 to main. PyPy prints no number and can swap the wording, so treat its line as unknown. The prefix alone doesn't show the stage: [SSL: UNEXPECTED_EOF_WHILE_READING] also printed on a cut upload on Linux, with the headers at the target.

Turning off certificate checks doesn't help: verify=False, curl --insecure and pip's --trusted-host printed the same line on the fixture, and ssl.OP_IGNORE_UNEXPECTED_EOF, where the build has it, only changed it to SSLZeroReturnError.

Is a proxy in your path at all?

sh
env | grep -i _proxy
python3 -c "import urllib.request; print(urllib.request.getproxies())"
pip config list

The output can hold the proxy password, so don't paste it anywhere. If a proxy shows up that you didn't expect and don't need, remove it (unset HTTPS_PROXY, pip config --user unset global.proxy), then try a safe request again. The second line also reads the Windows registry and a macOS system proxy. The third covers pip.

If nothing is set and you run no local proxy client, this page isn't your main path. Many public reports of these strings involve no proxy. Look here instead:

Check the next attempt with a HEAD or GET, never the failed POST

A probe describes only the attempt it makes, and it can pass on an intermittent fault. Your last line stays the evidence for the request that failed.

Set PROXY_URL to the proxy your failing job uses and TARGET_URL to the HTTPS address that failed. An empty PROXY_URL would turn the proxy off, so these commands stop with an error instead.

sh
curl --silent --show-error --head --output /dev/null --max-time 20 \
  --noproxy '' --proxy "${PROXY_URL:?}" \
  --write-out 'connect=%{http_connect} exit=%{exitcode} appconnect=%{time_appconnect} http=%{http_code}\n' \
  "${TARGET_URL:?}"

connect is the status of the proxy's reply to CONNECT (000 if curl read none). appconnect is the seconds until the TLS handshake with the target finished (through a TLS-inspecting proxy, with that proxy). exit is curl's exit code: %{exitcode} needs curl 7.75 or later; before that, read $?.

Read appconnect before the exit code. On a new connection to an https:// target with TLS early data off (curl's default), appconnect 0 with a non-zero exit meant the attempt that printed it was not sent, on every curl build we ran. A reused connection also reports 0, even with the POST delivered, so use this only on a one-URL run like the one above. Exit 35 on a one-URL HTTP/1.1 run showed 0 unless -L or auth negotiation came first. Exit 56 occurred with 0 receipts and with 1 or more (a receipt is one complete request the target read).

Write-outMeaningNext
connect=200 exit=35, appconnect 0Tunnel opened, handshake through it failed. Not sentCompare
connect=200 exit=28, appconnect 0Tunnel opened, handshake went silent. Not sentTimeouts
connect=000 exit=35, https:// proxy URLTLS to the proxy failed. Not sentCheck the scheme
connect=000 exit=7Proxy port closed. Not sentHost and port
connect=000 exit=56, appconnect 0, curl: (56) Proxy CONNECT aborted or Recv failure: Connection reset by peerProxy never answered CONNECT. Not sentProxy's log or provider
connect=000 exit=97, SOCKS5Proxy refused or couldn't reach the target. Not sentProxy's log or provider
connect=502 exit=7 (exit 56 on curl 8.7.1)Proxy refused the tunnel. Not sentTunnel 502, other codes
appconnect above 0, any non-zero exit (we saw 16, 18, 28, 52, 55, 56, 92)Handshake done, then it ended. UnknownCompare
connect=200 exit=0, appconnect above 0, any httpThis attempt got a response, and http is its status. It says nothing about the attempt that failedRepeat it

Through SOCKS5 connect is always 000, even with the request delivered (120 of 120 calls on loopback), so go by appconnect there.

No curl? ssl_eof_probe.py needs only Python and says in words how far one HEAD got: HTTPS_PROXY="$PROXY_URL" python3 ssl_eof_probe.py "$TARGET_URL". With no proxy set it stops with an error. We ran it on Linux and macOS, not Windows, and it doesn't test SOCKS5.

A wrong scheme can print this string too: an https:// proxy URL whose handshake closes gives the ProxyError with SSLError row in the first table. On loopback, 6 of 15 proxy ports with no TLS did this at connect=000 exit=35 (rows). If your port has no TLS, write http:// in the proxy URL, or socks5h:// if it's a SOCKS5 port.

Share the write-out line, not a verbose trace: curl -v prints a Proxy-Authorization: Basic token that decodes to the password, and --proxy shows the password in the process list while curl runs. aiohttp's ClientHttpProxyError prints url='http://USERNAME:PASSWORD@...' in str(). With a well-formed proxy URL, Requests, urllib3, HTTPX and curl's error lines showed no password. A proxy URL a client can't parse is different: with an unencoded / in the password, curl printed the whole URL, password included, and Requests, urllib3 and HTTPX printed part or all of it. Requests and urllib3 also print the query string, in the error and in the logging below, so a token in the URL lands in your log.

ProxyError with RemoteDisconnected('Remote end closed connection without response'): the request may have been sent

This is the line you see when a tunnel ends with no response after the handshake. The label Unable to connect to proxy can be false: the tunnel had connected, and across 540 single-attempt calls on loopback the target held the complete request in 309, headers only in 179, and nothing in 52. Those count failures we provoked, not rates. urllib3 1.26.20 prints Cannot connect to proxy. for this drop and also for a proxy that closes before any tunnel opens, so that line is unknown too.

These read the same wayThis attempt
ProxyError('Unable to connect to proxy', ConnectionResetError(104, 'Connection reset by peer'))Unknown
('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))Unknown
httpx.RemoteProtocolError: Server disconnected without sending a response.Unknown
httpx.ReadErrorUnknown
Read timed out. (read timeout=3.0)Unknown. If your connect and read timeouts differ and the number is the connect one, that case was not sent
curl: (52) Empty reply from serverUnknown
curl: (56) Recv failure: Connection reset by peerUnknown, unless the write-out shows appconnect 0

Treat this line and SSLEOFError as one incident. On the fixture, a job sending GETs while the proxy's exit IP changed saw the in-flight request fail with this ProxyError, then SSLEOFError on each new tunnel, then 200 (3 of 3 runs on Linux and on macOS). So when a log shows SSLEOFError, look one line up. An earlier ProxyError is the request that may have landed. To see which side closed it, run the comparison below.

Retries can hide that line, and Max retries exceeded doesn't show them: it printed after 1 CONNECT on a default session and after 4 on max_retries=3. Add this before the job's next run:

python
import logging

logging.basicConfig(level=logging.WARNING)

Then look for a line like this:

code
WARNING:urllib3.connectionpool:Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'RemoteDisconnected('Remote end closed connection without response')': /drop-fin

On the fixture a plain script printed no Retrying lines and logging printed 3, the first naming RemoteDisconnected, and both runs ended on SSLEOFError with 1 receipt at the target. Treat any RemoteDisconnected or reset line as unknown, whatever the last line says. For a run that already happened with retries on and logging off, treat the earlier attempts as unknown. curl's --retry-all-errors re-sends the same way: --show-error prints every attempt, and --silent alone prints none.

What each retry setting re-sends

Receipts at the target for a POST delivered and then dropped with no response, recorded on the fixture on 5 November 2026, 3 calls per cell. The curl and HTTPX rows come from the named-software run on 4 November (curl flag rows: 18, 12 and 2 calls, left to right).

SettingHTTP proxySOCKS5 (socks5h://)No proxy
Requests default session111
HTTPAdapter(max_retries=3)411
Retry(total=3)411
Retry(total=3, allowed_methods={"GET"})411
Retry(total=3, status_forcelist=[429,500,502,503,504])411
urllib3 with no retries argument411
Retry(connect=3) or Retry(read=3)1111
Retry(total=3, allowed_methods={"GET", "POST"}), or allowed_methods=[]444
Retry(total=3, other=0)111
HandshakeEOFRetry(total=3, other=0), below111
HTTPX default or HTTPTransport(retries=3)111
aiohttp default1not run1
curl, with or without --retry 3111
curl --retry 3 --retry-all-errors444

Retry(connect=3) leaves total at its default of 10, and this drop spends total. These counts need a close or reset with no response bytes. Other settings and failures:

  • Zeroing the rest doesn't help: Retry(total=3, connect=0, read=0, redirect=0, status=0) still went 4 times through the HTTP proxy.
  • With total=None no retry counter limits it: Retry(total=None, connect=3, read=3) was still re-sending there after 15 s, at 374 to 641 receipts, when we stopped it.
  • A read timeout or a partial status line stayed at 1 on every path. A 100 Continue and then a close still went 4 times through the HTTP proxy.
  • A generator body can't be rewound, so its 3 re-sends were empty POSTs.
  • A delivered PUT or DELETE went 4 times on every path, by design.
  • With POST in allowed_methods, other=0 stops only the HTTP proxy re-send: through SOCKS5 and with no proxy it still went 4 times.
  • A loop of your own that catches requests.exceptions.ConnectionError re-sends after a drop like this, because ProxyError and SSLError are subclasses of it.

Versions: Requests 2.34.2, urllib3 2.8.0, HTTPX 0.28.1, aiohttp 3.14.5 and curl 8.18.0 on Python 3.14.4 with OpenSSL 3.5.5. urllib3 1.26.20 and main at commit c5347bb gave the same urllib3 counts, and every release from 1.25.1 to 2.8.0 gave 4 in a separate sweep (other needs 1.26). HTTPX passes retries to its direct connection pool only, so it does nothing through a proxy.

On loopback (tables), tinyproxy, Squid, gost, proxy.py, mitmproxy in tunnel mode, mihomo, sing-box and Xray gave 4 receipts in 351 of 351 calls across HTTPAdapter(max_retries=3), Retry(total=3) and Retry(total=3, allowed_methods={"GET"}). Dante and every SOCKS5 port gave 1. When the first attempt was delivered and each retry died in its handshake, those eight ended on SSLEOFError with 1 receipt (78 of 78). A retry can also succeed and duplicate: with the proxy killed after delivery and restarted, Retry(total=3, allowed_methods={"GET"}, backoff_factor=1) returned HTTP 200 while the target held the POST twice (23 of 23, on those eight plus TLS-inspecting mitmproxy). None of it says how often this happens on a real path, or whether any commercial service behaves this way.

Whether upstream intends this is open, so plan for what the installed code does. Requests documents that max_retries "applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server", and these counts contradict that. urllib3 warns of its other counter: "These errors might be raised after the request was sent to the server, so the request might have side-effects."

The mechanism: behind an HTTP proxy this drop arrives as ProxyError, which counts as other, and among errors urllib3 checks the method only for read errors, so allowed_methods is never consulted. Through SOCKS5 or with no proxy the same drop is a ProtocolError, a read error, and POST stops. On urllib3 2.x ProxyError appears because http.client closes the connection first and close() clears the flag urllib3 uses to tell proxy errors from tunnel errors: keeping the flag in a local patch took this POST from 4 receipts to 1. urllib3 1.26 wraps any socket error behind a proxy as ProxyError. On 5 November 2026 we found no maintainer ruling in either project's tracker. A related thread is urllib3 #1744, where maintainers called a ProxyError hiding a connect failure from Retry a bug and fixed the connect side.

curl re-sends a POST in other ways too:

  • Two URLs in one invocation, with the POST lost on the reused connection: 2 receipts with no retry flag on loopback (52 of 52 calls). TLS-inspecting mitmproxy stayed at 1, and so did a single HTTP/1.1 URL in a fresh process with no --retry. If the fresh connection then dies in the handshake, the last line is curl: (35) with the POST already at the target.
  • A long-lived libcurl client: a curl_cffi 0.17.0 session with no retry option did the same on the fixture, on all three paths.
  • --retry alone on a timeout or a transient status: --retry 2 on exit 28 after delivery gave 3 receipts on the fixture and --retry 3 on 408, 429, 500, 502, 503 or 504 gave 4 with exit 0. A TLS-inspecting proxy can turn the drop into that 502.

Fixes and what they cost

If you can't look up or de-duplicate the POST at the target, use no retries or Retry(total=3, other=0), and add HandshakeEOFRetry only if you need retries through an outage.

python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(total=3, other=0)))

With urllib3 used directly, pass retries=False or retries=Retry(total=3, other=0) to the request. retries=False also stops it following redirects.

FixWhat it stopsCost
No automatic retry on non-idempotent calls: keep them on a session with no mounted retries, as RFC 9110 section 9.2.2 asksEvery re-send except a followed 307 or 308, which also needs allow_redirects=FalseYou handle each failure yourself
Retry(total=3, other=0)The POST re-send after a drop or a corrupted record, on every pathAlso stops retrying a handshake EOF (1 CONNECT and a failure, where Retry(total=3) took 3 and got a 200) and a delivered GET behind an HTTP proxy. Through SOCKS5 or with no proxy that GET still went 4 times
Idempotency keysThe effect of a duplicateWorks only where the endpoint implements the key. See what a key does and doesn't do
HandshakeEOFRetry(..., other=0)Every POST re-send except after a handshake that died or a connect that failed, and a followed 307 or 308, which also needs allow_redirects=False. If you add POST to allowed_methods, it re-sends through SOCKS5 and with no proxyDoesn't retry a stalled handshake or a CONNECT the proxy refuses or drops. It relies on a private urllib3 method: if a later version stops calling it, handshakes stop being retried with no warning, and no duplicates follow. Rerun the fixture when you upgrade

retry_handshake_eof.py tests where the error was raised (do_handshake), not what it says. Recorded on 5 November 2026 in 15 pairings of OpenSSL 1.1.1s to 4.1.0, LibreSSL 2.8.3 and urllib3 1.26.20 to main, it recovered from handshake failures and its retries never re-sent a POST after delivery.

In your own retry loop, died_in_handshake(error) from the same file tests the last attempt only, so call it with no mounted retries and allow_redirects=False. On a default session a 303 or 307 whose next hop died in the handshake made it return True with the POST at the first URL, and a loop that re-sent on True put it there 3 times on the fixture.

Simpler tests fail. Matching the SSLEOFError class sent a cut upload 4 times on OpenSSL 1.1.1s and 3.0.13 and on PyPy. Matching UNEXPECTED_EOF never retried on LibreSSL, OpenSSL 1.1.1s or Python 3.9.25.

SOCKS5 isn't a way out. Clean closes and resets stayed at 1, but a corrupted TLS record after a complete POST went 4 times with Retry(total=3), through SOCKS5 and with no proxy, because it counts as other. A cut upload also reached the target more than once on some builds, as headers with no complete body. Delivery stays unknown through SOCKS5 too, and not every proxy offers it.

A retry that waits

On the fixture Retry(total=3) spent its 4 attempts in under 10 ms, all inside one outage. This version uses the helper above and waits:

python
import requests
from requests.adapters import HTTPAdapter
from retry_handshake_eof import HandshakeEOFRetry

proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
session = requests.Session()
session.trust_env = False
session.proxies = {"http": proxy_url, "https": proxy_url}
retries = HandshakeEOFRetry(total=6, other=0, backoff_factor=1)
session.mount("https://", HTTPAdapter(max_retries=retries))

response = session.post("https://example.com/price-checks", json={"sku": "A-100"}, timeout=(10, 20))
print(response.status_code)

trust_env = False stops an HTTPS_PROXY variable overriding session.proxies, and it also ignores REQUESTS_CA_BUNDLE. Against a 20 s outage on the fixture this attempted at about 0, 0, 2, 6, 14 and 30 s, and the last one got a 200. A GET can use plain Retry(total=6, backoff_factor=1).

From a shell, --retry alone did not retry exit 35. To retry it, wrap curl in a loop that waits and re-sends only on exit 35 with appconnect 0. Run one URL, put -q first so ~/.curlrc can't add flags, and use no redirect flag, --retry or --tls-earlydata: with any of those, exit 35 can follow a delivered POST.

How long to wait depends on the proxy's route out, so measure your own path or ask your provider.

Target, known-good, direct: which side to look at

Set KNOWN_GOOD_URL to a small, unrelated HTTPS page you may fetch and the proxy allows. The block sends the same HEAD to the target through the proxy, to that page through the proxy, and to the target from your own address: the second line shows whether every tunnel fails, the third whether direct clients fail too.

sh
probe() {
  curl --silent --show-error --head --output /dev/null --max-time 20 \
    --write-out 'connect=%{http_connect} exit=%{exitcode} appconnect=%{time_appconnect} http=%{http_code}\n' "$@"
}
compare() {
  probe --noproxy '' --proxy "${PROXY_URL:?}" "${TARGET_URL:?}"
  probe --noproxy '' --proxy "${PROXY_URL:?}" "${KNOWN_GOOD_URL:?}"
  probe --noproxy '*' "${TARGET_URL:?}"
}
compare

The third line connects from your own address, so the target sees that address; skip the line if it shouldn't. Each cell is exit/connect/http, so connect=200 exit=35 appconnect=0.000000 http=000 is 35/200/000. All are from the fixture on three curl builds.

Target via proxyKnown-good via proxyTarget directPoints at
35/200/00035/200/0000/000/200The proxy or its exit IP, or your known-good page. Open that page directly and try a second one: a dead known-good through a proxy that answers 200 first prints this row too
35/200/0000/200/20035/000/000The target closes your address and the proxy's exit IP, perhaps everyone
35/200/0000/200/2007/000/000The target is down, or your own route to it is blocked, and this proxy answers 200 before dialling
35/200/0000/200/2000/000/200Unresolved: the target refuses the proxy's exit IP, or the route from that IP to the target fails

Row four's two causes print the same three lines. The third line counts only if this machine may connect directly. In a sandbox or behind a corporate proxy a blocked direct route fails whatever the target does, and exit 7 there looks like row three. If you can't connect directly, or you skip the line, read the first two columns alone: known-good failing is row one, known-good passing is row four. A local client in TUN mode sets no proxy variable, and --noproxy '*' still goes through it, so stop the client for the third line (we didn't run TUN mode). All three at exit 35 has no row here: look near you first, at a firewall or your client. Through SOCKS5 read 000 for 200, and a dead target prints exit 97. If connect on the first line isn't 200 through an HTTP proxy, the tunnel never opened, so read that line in the write-out table.

A first column of 52/200/000 or 56/200/000 means the handshake finished and then the connection ended. Read the same rows with that code in place of 35. On the fixture a drop after the handshake printed 52 on curl 8.7.1 and 56 on curl 8.18.0 and 8.23.0, in the patterns of rows one, two and four.

After row one or four, go to Next step by path. After row two the proxy is cleared, so use the no-proxy list above. After row three, try the target from another network, then wait for it.

Any HTTP status counts as a response here, except that a TLS-inspecting proxy can answer for a target it couldn't reach: mitmproxy answered connect=200 exit=0 http=502 with the target's handshake dead. If all three lines pass, curl's HEAD works now, and either the fault comes and goes or your job differs from this HEAD. If the job fails every time, suspect the difference: its TLS library, its proxy setting or its request. Run ssl_eof_probe.py with the interpreter your job uses: HTTPS_PROXY="$PROXY_URL" /path/to/job/python ssl_eof_probe.py "$TARGET_URL". If that fails where curl passed, read the error it prints: a certificate error is the trust store, and an EOF points at the TLS build, as in the middlebox row below. If it passes too, look at what your job does that this HEAD doesn't: its proxy URL, its body or a long response.

If the job fails only sometimes, repeat the first two lines on a slow timer while the job runs, since a connection limit may show only under its load:

sh
while :; do
  date -u +%FT%TZ
  probe --noproxy '' --proxy "${PROXY_URL:?}" "${TARGET_URL:?}"
  probe --noproxy '' --proxy "${PROXY_URL:?}" "${KNOWN_GOOD_URL:?}"
  sleep 30
done

When a line fails, stop the loop, run the third line once and read that row. Send your provider the failing line and its timestamp. Until you catch one, keep logging on so the next failure shows its earlier attempts, and retry only what wasn't sent, with a wait.

A HEAD has no body in either direction, so it passes where only uploads or long responses die: on the fixture, through a proxy that cut uploads, it gave exit=0 http=200 while a 5 MB POST failed. If your failing request or its response is large, look for a size limit at the proxy or the target.

Next step by path

PathNext actionTested here
Sandbox or corporate egressCheck the host allow-list, the proxy's connection limit and its log: a denied host can print SSLEOFError, and a 16-connection cap printed both strings on hosts that were allowedNo. Public reports only
Local proxy client with rulesRead which rule matched, then try the other route: one curl line appeared on a host a rule sent direct. Then check port, scheme and stale settingsmihomo 1.19.33 on loopback: a MATCH,REJECT rule or a dead upstream node printed the string with a healthy target (128 of 128). No other client's rules
TLS-inspecting proxyThis string doesn't point at the root CA: a missing root prints a certificate error (CERTIFICATE_VERIFY_FAILED, curl exit 60). Ask the network teammitmproxy only: HTTP 502 or a timeout when the upstream closed first. The string only when mitmproxy was killed, or on curl when a transfer was cut. Not ssl-bump or corporate middleboxes
Middlebox and a large ClientHelloAdd --curves X25519 to the first curl block on a curl built with OpenSSL 3.5 or later. If only that passes, OpenSSL's larger ClientHello is tripping a device, which OpenSSL warns can happen in rare cases. A developer calls this a device bug, so raise it with whoever runs it, or its vendorFixture only: a proxy that closed on a ClientHello over 1,000 bytes. One report suspects this behind SSLEOFError
Exit IP changing, for example a mobile proxy rotatingLive tunnels can die and new ones can fail until the path settles, as the fixture scripted it. Pause, then retry only what wasn't sent, with a waitFixture only. No timing here applies to a real network
Target refuses your exit IPStop, or ask the site. It isn't a fault to work around: one service did this to a client's IP until its staff lifted the blockFixture only; that report had no proxy
Commercial proxyOn a rotating pool each line can leave from a different exit, so use a sticky session if your provider offers one, or repeat the comparison before you trust a row. If failures rise with concurrency, lower it and compare again. Then ask your provider, with a failing write-out line and its timestampNothing here shows what a service does

The wording depends on the build

BuildHandshake EOF prints
Python on OpenSSL 3.0.13, 3.5.5, 3.5.9, 3.6.5 and 4.1.0[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol, low number
Python on OpenSSL 1.1.1s and LibreSSL 2.8.3EOF occurred in violation of protocol, low number
Python 3.9.25 (OpenSSL 3.5.4)SSLZeroReturnError(6, 'TLS/SSL connection has been closed (EOF) (_ssl.c:1147)')
curl 8.7.1 (LibreSSL 3.3.6)curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to, then the host
pip 26.3There was a problem confirming the ssl certificate: [SSL: UNEXPECTED_EOF_WHILE_READING], then the EOF text

An untrusted chain prints CERTIFICATE_VERIFY_FAILED on Linux and certificate is not trusted on macOS instead of pip's certificate line. aiohttp 3.14.5 prints Cannot connect to host with no EOF text, and sends a delivered GET, HEAD, OPTIONS, PUT or DELETE twice and a POST or PATCH once. Node 24.22.1's fetch, in one run, printed TypeError: fetch failed with cause ECONNRESET: see Node.js fetch failed behind a proxy. Playwright wasn't run: see Fix ERR_TUNNEL_CONNECTION_FAILED.

How this was tested

ipvolt sells dedicated mobile proxies, and its Terms say rotation can briefly interrupt traffic. The tables come from a loopback fixture and from named open-source proxy software on loopback, with default settings apart from the changes its README lists. None of it shows what a commercial service runs or does.

ipvolt ran the fixture on 4 and 5 November 2026 on Ubuntu 26.04 and macOS 15.7, and the named software on 4 November on Ubuntu 26.04 (11,816 client calls against an HTTP/1.1 target). These runs leave out Windows, HTTP/3, load, a real proxy's open tunnel going silent with no FIN or RST, and a real proxy that requires a login.

Fixture files: archive, README and recorded results. Named-software files: archive and the facts behind its counts.

Pricing lists ipvolt's mobile proxies by country and carrier.

Sources & further reading

Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.