Sandbox HTTP proxy drops part of large HTTPS response bodies for Python clients (curl unaffected, origin logs a complete 200)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.220)
What's Wrong?
The sandbox HTTP proxy intermittently loses part of an HTTPS response body when the
client is Python. http.client raisesIncompleteRead(<n> bytes read, <m> more expected). Rate is roughly 25-40%, and it
drifts noticeably between bursts.
It is the proxy, not the containment. Both arms below ran outside the
sandbox with the same Python client, interleaved per round — arm A pointed at the
sandbox proxy via HTTPS_PROXY, arm B direct:
| both outside the sandbox | truncated |
|---|---|
| via the sandbox proxy | 3 / 10 |
| direct, no proxy | 0 / 10 |
No connection errors in arm A, so the proxy served those requests normally
otherwise. This also means the sandbox is not required to reproduce — the proxy
plus a Python client is enough.
curl through the same sandbox proxy, same URL, interleaved in the same rounds,
is not affected. Neither is any client outside the sandbox.
Interleaved, 8 rounds, one call per client per round, same URL, 310 359-byte
response:
| client | truncated |
|---|---|
| curl, HTTP/2 | 0 / 8 |
| curl, HTTP/1.1 | 0 / 8 |
| Python urllib (3.14.3) | 2 / 8 |
Both curl runs go through the sandbox proxy (CONNECT tunnel: HTTP/1.1 to
negotiatedlocalhost:<proxy-port>), so this is not a proxy-bypass artifact.
HTTP version is not the factor — curl forced to HTTP/1.1, the same version Python
uses, is clean.
Not a CPython regression either; interleaved, 8 rounds:
| interpreter | truncated |
|---|---|
| Python 3.14.3 | 2 / 8 |
| Python 3.12.12 | 4 / 8 |
The origin completed every response. Its nginx access log shows HTTP 200 with
the full body length for all six requests in one test window, including the five
the sandboxed Python client could not finish reading — no 499 (what nginx logs
when the client disappears mid-response), and an empty vhost error log.$body_bytes_sent only counts what nginx handed to the socket, so it is not proof
of delivery, but nginx saw neither a write error nor a client reset.
Reproduces against two unrelated self-hosted services with different backends (a
Python/gunicorn app, ~115 kB POST response; and a Bitbucket Data Center
instance, 310 kB GET response), so it is not application-specific.
What Should Happen?
A sandboxed process should receive the complete response body, or fail with an
error that identifies the sandbox as the source. Silent tail loss is the worst
case: IncompleteRead reads as a server-side fault, so the natural reaction — ours,
twice, on two separate days — is to go debug the origin. We only found it by
pulling the origin's access log, and only identified the client-dependence by
testing curl side by side.
Error Messages/Logs
Client, six consecutive sandboxed Python calls against the first service
(timestamps UTC):
12:42:22-28 IncompleteRead, partial = 77824 bytes
12:42:28-36 IncompleteRead, partial = 77824 bytes
12:42:36-42 IncompleteRead, partial = 77824 bytes
12:42:42-48 ok
12:42:48-53 IncompleteRead, partial = 77676 bytes
12:42:53-59 IncompleteRead, partial = 106496 bytes
Origin nginx access log, the same six requests
($request_time/$upstream_response_time … $status $body_bytes_sent; client IP and
host redacted):
<ip> 5.825/5.773 - [25/Jul/2026:12:42:28 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
<ip> 7.836/7.795 - [25/Jul/2026:12:42:36 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
<ip> 5.965/5.839 - [25/Jul/2026:12:42:42 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
<ip> 5.587/5.547 - [25/Jul/2026:12:42:48 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
<ip> 5.203/5.160 - [25/Jul/2026:12:42:53 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
<ip> 5.839/5.791 - [25/Jul/2026:12:43:00 +0000] "POST /… HTTP/1.1" 200 114690 "-" "Python-urllib/3.14"
Truncation offsets repeat in streaks — three byte-identical cuts in a row above,
then two different values — and the streak value differs per endpoint (77824 on
one service, 302462 on the other). Not a single global constant, which is why we
suspect per-connection or per-worker state rather than a fixed buffer or a clock.
We found no sandbox-proxy log to inspect; ~/.claude/debug is empty on this
install. If proxy-side logging can be enabled, we will re-run with it on and
attach the output.
Steps to Reproduce
We could not reduce this to a public repro. Public endpoints pass — see the
ruled-out list. What reproduces for us:
- Sandbox enabled,
network.allowedDomains: ["*"], nostrictAllowlist, no
tlsTerminate.
- From a sandboxed Bash command,
urllib.requestGET/POST against an internal
HTTPS origin behind nginx returning 100 kB+ with Transfer-Encoding: chunked.
- Repeat 8-20 times, counting
http.client.IncompleteRead. - In the same loop, interleave a
curlcall to the identical URL as a control.
The shortest form, which needs no sandbox at all: read the proxy URL out of a
sandboxed shell's HTTPS_PROXY, then from a normal shell run the same Python
request in a loop with HTTPS_PROXY set to it, alongside a direct-connection arm.
That is the attribution test in the first table.
Ruled out:
- Client / HTTP version / interpreter — see the two tables above (interleaved).
- Response size, or a threshold to stay under — interleaved A/B with and
without Accept-Encoding: gzip: 4/10 truncated each. Compression worked (the
observed failure offsets dropped from ~100 KiB to ~40 kB) but the rate did not
move. Caveat: the two arms were two different builds of the same client, not one
build with a header toggle.
- Chunked encoding as such — public control inside the sandbox,
httpbingo.org/bytes/200000 (Content-Length) vs /stream-bytes/200000
(chunked), 15 rounds each, interleaved: 15/15 OK for both.
- Slow bodies as such —
httpbingo.org/drip?duration=10&numbytes=50000inside
the sandbox: 49 s wall-clock, intact.
- Origin front-end buffering — flipped the first origin from
proxy_buffering off to on: 11/20 vs 8/20 truncated. No large effect
detected; note this comparison is inherently sequential and underpowered
(Fisher p = 0.53), so it does not exclude a small effect.
- The origin server — access log above; and outside the sandbox the same
Python client was clean in a 10-call run.
Honest limits of the measurement, since the rate drifts between bursts: the
sandboxed-vs-unsandboxed comparison (8/20 vs 0/10, Fisher two-sided p = 0.029)
ran as two sequential batches, not interleaved, and the sandboxed arm pooled two
client builds. The client comparisons in the two tables above are interleaved
per round, and those are the ones we would lean on.
We are happy to test candidate builds; the measurement is scripted and takes a few
minutes per run.
Environment
- Claude Code 2.1.220
- macOS 26.5.2, arm64
- Clients: Python 3.14.3 and 3.12.12
urllib.request(HTTP/1.1); curl (HTTP/2 and
forced HTTP/1.1) as the unaffected control
- Sandbox:
enabled: true,autoAllowBashIfSandboxed: true,
network.allowedDomains: ["*"], allowLocalBinding: true, no strictAllowlist,
no tlsTerminate
- Origins are internal hosts reached over a VPN; the public endpoints we tested as
controls are not, which is the one dimension we could not disentangle.
Is this a regression?
Unknown — older Claude Code versions not tested. First observed 2026-07-23 on
2.1.2xx.
Workaround
{ "sandbox": { "excludedCommands": ["mytool *"] } }
Reliable, but it drops filesystem containment for that command too, which is more
than we want to give up. Switching the affected tool to curl also avoids it.