Empty HTTP 200 returned after CCR buffered-stream conversion (proxy never contacts upstream, logs no error)
Description
When CCR converts a stream:true client request into a buffered stream:false upstream request so it can
handle headroom_retrieve server-side, the next request on that session aborts inside the proxy between
outbound_headers and beta_header_merge, and the client receives HTTP 200 with an empty body. The proxy
never opens a connection to api.anthropic.com for that request.
There are two defects here:
- The abort itself. The request dies silently after the CCR buffered round-trip on the same session.
- The reporting. The failure emits no
ERROR line at all in proxy.log, and answers 200 rather
than a 5xx. A failure that returns success and logs nothing took several sessions to localise.
Correlation in a single proxy.log: 6 CCR buffered conversions, 6 empty-200 deaths, perfectly paired in
both directions across three separate client sessions. No conversion without a death, no death without a
conversion.
The trigger appears to be headroom_retrieve being present in the tool list at all, not any particular
prompt. Note the proxy itself injects that tool
(event=tool_injection_decision ... decision=inject_sticky_replay tool_definition_bytes_count=464), so
removing the headroom MCP server client-side does not avoid it.
To Reproduce
Steps to reproduce the behavior:
-
Install headroom: uv tool install headroom-ai (version 0.35.0).
-
Start the proxy standalone, default transforms, CCR enabled:
headroom proxy --port 8787
-
Run a Claude Code client through it:
(this injects ANTHROPIC_BASE_URL=http://127.0.0.1:8787 per-process)
-
Work a long session until the proxy logs a CCR buffered conversion:
CCR: stream:true request has headroom_retrieve available; using buffered stream:false upstream request for server-side retrieval handling
-
The next request on that session dies. The client prints the error below and the session must be
exited.
Observed on requests that are not small: five of the six deaths landed at msgs=35, the sixth at msgs=140
with content_length=284080. Never on small requests.
To localise a death in proxy.log, grep for the signature:
PERF model=\S+ msgs=\d+ tok_before=0 tok_after=0 .* transforms=none
That pattern combined with no event=outbound_request matched exactly the six deaths and nothing else.
Expected Behavior
Either the request is forwarded upstream normally, or — if the proxy cannot process it — it returns a 5xx
with a body, and logs an ERROR line naming the failure.
Actual Behavior
The proxy returns status=200 in 17 ms with an empty body, having never opened an upstream connection, and
writes no ERROR line. The client cannot parse the response and drops the connection.
Code Sample
No Python API involved — this is the headroom proxy CLI in front of a Claude Code client.
# Not applicable. Reproduced via CLI:
# headroom proxy --port 8787
# headroom wrap claude
# The failing request is generated by Claude Code (claude-cli/2.1.232), not by user code.
Error Output
Client side (Claude Code):
API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request
A subagent in the same session died with Agent terminated early due to an API error.
Proxy side — the entire log footprint of the failing request hr_1786680242_000147
(2026-08-13 23:04:02, proxy.log lines 6067–6071), client 127.0.0.1:61084, user-agent: claude-cli/2.1.232,
POST /v1/messages?beta=true, model=claude-opus-5, msgs=140, content_length=284080:
event=proxy_inbound_request ... content_length=284080
event=outbound_headers forwarder=anthropic_messages stripped_count=1
PERF msgs=140 tok_before=0 tok_after=0 tok_saved=0 opt_ms=13 total_ms=13 tok_out=0 ttfb_ms=0 transforms=none client=claude-code
STAGE_TIMINGS "compression_first_stage": null, "memory_context": null, "upstream_connect": null, "upstream_first_byte": null, "total_pre_upstream": 14.97
event=proxy_inbound_response status=200 duration_ms=17.31
A healthy request on the same proxy logs outbound_headers → beta_header_merge → pipeline →
tool_injection_decision → cache_breakpoints → outbound_request. This one stops after outbound_headers.
upstream_connect: null and compression_first_stage: null place the bail-out after read_request_json +
deep_copy and before compression — roughly 15 ms in. That is the step that reads per-session sticky
beta / sticky tool state.
The immediately preceding request hr_1786680217_000146 (23:03:37, total_ms=24796) succeeded, and is
the CCR conversion:
CCR: stream:true request has headroom_retrieve available; using buffered stream:false upstream request for server-side retrieval handling
CACHE-BUST: expected_cached=98,632 actual_read=0 tokens_lost=98,632 tokens_saved=718
CACHE-MISS-ATTRIBUTION: reason=prefix_change idle=25s ttl=300s prefix_changed=True ttl_exceeded=False
The whole log file contains zero ERROR lines across its entire life. The only WARNINGs are upstream
429s, the Kompress/Magika startup notices, and Could not parse output_tokens from SSE, estimating 28 from 1144 bytes.
Environment
- Headroom version: 0.35.0 (installed via
uv as headroom-ai)
- Python version: 3.14.6 (system interpreter; the
uv tool environment may pin its own)
- OS: Windows 11 Pro 10.0.26200
- LLM Provider: Anthropic (
api.anthropic.com, model=claude-opus-5)
- Client: Claude Code,
claude-cli/2.1.232
- Proxy invocation:
headroom proxy --port 8787, standalone, no config file under ~/.headroom,
default transforms
Additional Context
Paired line numbers, single proxy.log, three separate client sessions:
| CCR buffered conversion |
empty-200 death |
gap (lines) |
| 1838 |
1848 |
+10 |
| 1918 |
1927 |
+9 |
| 1946 |
1955 |
+9 |
| 2235 |
2275 |
+40 |
| 2554 |
2594 |
+40 |
| 6027 |
6069 |
+42 |
Other CCR: lines (workspace unresolved, Proactively expanded N context(s),
skipping proactive expansion append) are common and harmless — only the stream:true → buffered
stream:false conversion pairs with a death.
Ruled out on our side:
- Double-bound proxy port — verified single instance,
17432 (launcher) → 20032 → 24620 (python, owns port 8787).
- Version drift — installed
headroom-ai matches the reported 0.35.0.
- Client-side hooks — they never touch the HTTP path; a blocked hook becomes ordinary
tool_result content
client-side, before any API call.
Wrong levers, for the record, in case they help narrow the code path:
--no-ccr-proactive-expansion does not help — the conversion fires on headroom_retrieve being
available, not on expansion occurring.
--no-ccr avoids it but costs lossy compression with no recovery path.
--lossless looks like the correct workaround: no CCR retrieval marker is emitted, so no
headroom_retrieve is needed and the buffered stream:false path never runs. Not yet confirmed by a long
session at the time of writing.
Log excerpt available. proxy.log lines 5996–6106 verbatim with original line numbers, 47 KB — covers
the last three healthy requests, the CCR/cache-bust predecessor, the failure, and two minutes of following
silence. authorization / x-api-key / sk-ant-* redacted. Happy to attach on request.
Note for maintainers: proxy.log carries verbatim prompt and tool-call content elsewhere in the file, so
it cannot be shared wholesale.
Empty HTTP 200 returned after CCR buffered-stream conversion (proxy never contacts upstream, logs no error)
Description
When CCR converts a
stream:trueclient request into a bufferedstream:falseupstream request so it canhandle
headroom_retrieveserver-side, the next request on that session aborts inside the proxy betweenoutbound_headersandbeta_header_merge, and the client receives HTTP 200 with an empty body. The proxynever opens a connection to
api.anthropic.comfor that request.There are two defects here:
ERRORline at all inproxy.log, and answers200ratherthan a 5xx. A failure that returns success and logs nothing took several sessions to localise.
Correlation in a single
proxy.log: 6 CCR buffered conversions, 6 empty-200 deaths, perfectly paired inboth directions across three separate client sessions. No conversion without a death, no death without a
conversion.
The trigger appears to be
headroom_retrievebeing present in the tool list at all, not any particularprompt. Note the proxy itself injects that tool
(
event=tool_injection_decision ... decision=inject_sticky_replay tool_definition_bytes_count=464), soremoving the
headroomMCP server client-side does not avoid it.To Reproduce
Steps to reproduce the behavior:
Install headroom:
uv tool install headroom-ai(version 0.35.0).Start the proxy standalone, default transforms, CCR enabled:
Run a Claude Code client through it:
(this injects
ANTHROPIC_BASE_URL=http://127.0.0.1:8787per-process)Work a long session until the proxy logs a CCR buffered conversion:
The next request on that session dies. The client prints the error below and the session must be
exited.
Observed on requests that are not small: five of the six deaths landed at
msgs=35, the sixth atmsgs=140with
content_length=284080. Never on small requests.To localise a death in
proxy.log, grep for the signature:That pattern combined with no
event=outbound_requestmatched exactly the six deaths and nothing else.Expected Behavior
Either the request is forwarded upstream normally, or — if the proxy cannot process it — it returns a 5xx
with a body, and logs an
ERRORline naming the failure.Actual Behavior
The proxy returns
status=200in 17 ms with an empty body, having never opened an upstream connection, andwrites no
ERRORline. The client cannot parse the response and drops the connection.Code Sample
No Python API involved — this is the
headroom proxyCLI in front of a Claude Code client.Error Output
Client side (Claude Code):
A subagent in the same session died with
Agent terminated early due to an API error.Proxy side — the entire log footprint of the failing request
hr_1786680242_000147(2026-08-13 23:04:02,
proxy.loglines 6067–6071), client127.0.0.1:61084,user-agent: claude-cli/2.1.232,POST
/v1/messages?beta=true,model=claude-opus-5,msgs=140,content_length=284080:A healthy request on the same proxy logs
outbound_headers→beta_header_merge→ pipeline →tool_injection_decision→cache_breakpoints→outbound_request. This one stops afteroutbound_headers.upstream_connect: nullandcompression_first_stage: nullplace the bail-out afterread_request_json+deep_copyand before compression — roughly 15 ms in. That is the step that reads per-session stickybeta / sticky tool state.
The immediately preceding request
hr_1786680217_000146(23:03:37,total_ms=24796) succeeded, and isthe CCR conversion:
The whole log file contains zero
ERRORlines across its entire life. The onlyWARNINGs are upstream429s, the Kompress/Magika startup notices, and
Could not parse output_tokens from SSE, estimating 28 from 1144 bytes.Environment
uvasheadroom-ai)uvtool environment may pin its own)api.anthropic.com,model=claude-opus-5)claude-cli/2.1.232headroom proxy --port 8787, standalone, no config file under~/.headroom,default transforms
Additional Context
Paired line numbers, single
proxy.log, three separate client sessions:Other
CCR:lines (workspace unresolved,Proactively expanded N context(s),skipping proactive expansion append) are common and harmless — only thestream:true→ bufferedstream:falseconversion pairs with a death.Ruled out on our side:
17432 (launcher) → 20032 → 24620 (python, owns port 8787).headroom-aimatches the reported 0.35.0.tool_resultcontentclient-side, before any API call.
Wrong levers, for the record, in case they help narrow the code path:
--no-ccr-proactive-expansiondoes not help — the conversion fires onheadroom_retrievebeingavailable, not on expansion occurring.
--no-ccravoids it but costs lossy compression with no recovery path.--losslesslooks like the correct workaround: no CCR retrieval marker is emitted, so noheadroom_retrieveis needed and the bufferedstream:falsepath never runs. Not yet confirmed by a longsession at the time of writing.
Log excerpt available.
proxy.loglines 5996–6106 verbatim with original line numbers, 47 KB — coversthe last three healthy requests, the CCR/cache-bust predecessor, the failure, and two minutes of following
silence.
authorization/x-api-key/sk-ant-*redacted. Happy to attach on request.Note for maintainers:
proxy.logcarries verbatim prompt and tool-call content elsewhere in the file, soit cannot be shared wholesale.