[BUG] v2.1.113+ on linux-x64 behind HTTP CONNECT proxy: Bun runtime fetch race condition + 3 cascading regressions

Status Closed — not planned
Reported on v2.1.112
Maintainer reply ✓ Yes — mhegazy
Activity 12 comments · opened Apr 18, 2026 · closed Jun 13, 2026
💡 Likely answer: A maintainer (mhegazy, contributor) responded on this thread — see the highlighted reply below.

[BUG] v2.1.113+ on linux-x64 behind HTTP CONNECT proxy: Bun runtime fetch race condition + 3 cascading regressions

TL;DR — root cause

Bun runtime's fetch() in the linux-x64 build hangs intermittently when used through an HTTP CONNECT proxy. This is a race condition: under strace (which slows syscalls via ptrace) it disappears completely. The arm64 build is unaffected. Same proxy works in 0.3s with curl and python urllib. There is no per-binary distribution issue — the official installer and the npm @anthropic-ai/claude-code-linux-x64 optional dep ship the identical ELF (sha256 below).

This single root cause produces several different user-visible failures, documented as Bug #1–#4 below. Bug #4 is the root; Bugs #1–#3 cascade from it (or compound it).

| | x86_64 Linux | arm64 Linux |
|---|---|---|
| 2.1.112 (bundled JS) | 10/10 ✅ | n/a |
| 2.1.114 (Bun native) | 2/10 ✅ (8 TIMEOUT) | 10/10 ✅ |

Same proxy, same ~/.claude/settings.json, same Ubuntu 24.04.3 + glibc 2.39. Only the architecture (and binary) changes.

Workaround: npm i -g @anthropic-ai/claude-code@2.1.112.

---

Environment

| | |
|---|---|
| Claude Code | 2.1.114 (also reproduced on 2.1.113) |
| Distribution | Bun single-file ELF (log paths show /$bunfs/root/src/entrypoints/cli.js) |
| OS | Ubuntu 24.04.3 LTS, kernel 6.8.0-107-generic, glibc 2.39, x86_64 |
| Network | Standard enterprise setup — all egress must traverse a local HTTP CONNECT forward proxy. No direct internet route. |
| Reference machine A | local x86_64 + Claude Code 2.1.112 (bundled JS) — works |
| Reference machine B | aarch64 + Claude Code 2.1.114 (Bun native) — works |

~/.claude/settings.json env block sets HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, NO_PROXY (each in upper- and lowercase), plus OTEL_SDK_DISABLED=true, OTEL_*_EXPORTER=none, DISABLE_AUTOUPDATER=1, CLAUDE_CODE_PROMPT_CACHE_TTL=1h. Verified loaded by debug log: settingsEnv keys: HTTPS_PROXY,HTTP_PROXY,ALL_PROXY,...,OTEL_SDK_DISABLED,....

---

Bug #4 (root cause) — linux-x64 Bun runtime fetch hangs through HTTP CONNECT proxy

Symptom

POST /v1/messages is sent successfully through the CONNECT tunnel. Then 30 seconds pass with zero stream chunks coming back, the client logs Slow first byte: no stream chunk 30.0s after request sent (attempt 1), and the request hangs until the wrapper kills it at 45s. Headless mode reproduces ~75% of the time. TUI hits the same code path during startup and intermittently during long sessions.

Hard evidence

1. Same binary across distribution channels — packaging is innocent:

npm optional dep @anthropic-ai/claude-code-linux-x64 :
  sha256 12bd4b0916deb06be17ffc7b2f0485e140bf00b2db3dcb78469d66723d73c27f
  BuildID 052ef6d8cef1bef39149a31808f3d579db450889
  size   236411520

Official installer  (https://claude.ai/install.sh -- 2.1.114) :
  sha256 12bd4b0916deb06be17ffc7b2f0485e140bf00b2db3dcb78469d66723d73c27f
  BuildID 052ef6d8cef1bef39149a31808f3d579db450889
  size   236411520

2. Network/proxy/upstream are healthy:

| Tool | api.anthropic.com | statsigapi.net | docs.claude.com |
|---|---|---|---|
| curl 8.5.0 (same env, same proxy) | 0.258 / 0.270 / 0.273 s (404) | 0.340 / 0.343 / 0.339 s (403) | 0.501 / 0.598 / 0.420 s (200) |
| Python urllib 3.13 (same env, same proxy) | 0.418 / 0.415 / 0.421 s | 0.333 / 0.337 / 0.338 s | 0.282 / 0.273 / 0.369 s |
| Bun runtime via Claude Code | 30s+ stalls, intermittent |||

tcpdump during a TIMEOUT trial captures a clean CONNECT api.anthropic.com:443 HTTP/1.1\r\nProxy-Connection: Keep-Alive\r\n\r\n request (so this is not issue #14165 "Bun ignores HTTP_PROXY" — it does use the proxy). A successful trial under strace shows TLS ClientHello → ServerHello → application data flowing both directions in the CONNECT tunnel.

3. Race condition (the smoking gun):

Without strace : 2 / 10 OK
Under  strace  : 5 / 5 OK   (ptrace serializes syscalls and adds ~100µs/call)

ptrace overhead reliably masks the bug. Bun uses io_uring on Linux (anon_inode:[io_uring] fds visible in the strace) — the suspected location is the io_uring submission/completion path or the TLS state machine on the CONNECT-tunneled socket.

4. Architecture-selective, not network-selective:

Same proxy, same settings.json, same Ubuntu 24.04.3 + glibc 2.39. The only difference between the working machine and the broken machine is linux-arm64 vs linux-x64 Bun build.

5. Also reproduces on a different multi-core x86_64 box — not a single-vCPU artifact, not specific to the low-power Intel N150 reference machine.

6. No user-side workaround works (each tested 5×, early-aborted on 2 TIMEOUTs):

| Env var | Effect |
|---|---|
| BUN_FEATURE_FLAG_DISABLE_IO_POOL=1 | 2 TIMEOUT (no help) |
| BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK=1 | 2 TIMEOUT (no help) |
| BUN_FEATURE_FLAG_FORCE_WAITER_THREAD=1 | 2 OK / 2 TIMEOUT (no change) |
| BUN_FEATURE_FLAG_DISABLE_IPV6=1 | 2 TIMEOUT (no help) |
| BUN_FEATURE_FLAG_DISABLE_DNS_CACHE=1 | 1 OK / 2 TIMEOUT (no help) |
| BUN_CONFIG_MAX_HTTP_REQUESTS=1 | 2 TIMEOUT (no help) |
| BUN_CONFIG_HTTP_RETRY_COUNT=0 | 1 OK / 1 TIMEOUT (no change) |

7. No bundled-JS escape hatch in 2.1.113+ — the npm package's cli-wrapper.cjs only spawnSyncs the same native binary; there is no JS code path and no CLAUDE_CODE_USE_JS_BUILD flag.

Suggested fix direction

  1. Diff the linux-x64 vs linux-arm64 Bun build pipelines for fetch / proxy tunneling
  2. Bisect Bun versions between the last known-good Claude Code release and the linux-x64 native build
  3. Provide an CLAUDE_CODE_USE_JS_BUILD=1 (or equivalent) escape hatch for affected users until the Bun fix lands

Repro

# On any linux-x64 host behind an HTTP CONNECT proxy:
curl -fsSL https://claude.ai/install.sh | bash -s -- 2.1.114
# (or: npm i -g @anthropic-ai/claude-code@2.1.114)

# Configure proxy in ~/.claude/settings.json `env` block (or just export HTTPS_PROXY).

for i in 1 2 3 4 5 6 7 8 9 10; do
  timeout 45 claude -p --dangerously-skip-permissions --output-format stream-json \
    --model claude-haiku-4-5-20251001 'Respond with only the word ok' \
    > /dev/null 2>&1 && echo "$i OK" || echo "$i TIMEOUT"
done

Expect ~75 % TIMEOUT on linux-x64. On linux-arm64 or 2.1.112 expect 0% TIMEOUT.

---

Bug #1 (cascading) — settings.json.env strips HTTP(S)_PROXY and NO_PROXY from spawned subprocess env

Symptom

In ~/.claude/settings.json's env block, set 22 variables. From a Claude session, run Bash tool: printenv. The subprocess inherits 19 of the 22. The 3 missing keys, all in upper- and lowercase = 6 strings, are exactly:

HTTPS_PROXY, HTTP_PROXY, NO_PROXY,
https_proxy, http_proxy, no_proxy

ALL_PROXY / all_proxy are passed through normally. So is every non-proxy variable (CLAUDE_*, ANTHROPIC_*, OTEL_*, COLORTERM, BASH_DEFAULT_TIMEOUT_MS, DISABLE_AUTOUPDATER, etc.).

Evidence

The keys ARE loaded by Claude:

CA certs: Config fallback - globalEnv keys: ,
  settingsEnv keys: ...,HTTPS_PROXY,HTTP_PROXY,ALL_PROXY,
  https_proxy,http_proxy,all_proxy,NO_PROXY,no_proxy,...

But by the time a Bash tool subprocess is spawned, the 6 proxy keys (HTTPS/HTTP/NO, both cases) are gone, while ALL_PROXY survives. Workaround: export the 6 keys in the launcher shell — they then propagate normally — confirming the strip happens specifically in the settings.json → spawn env path, not in the runtime's general inheritance logic.

Suggested fix direction

Audit the env-merge logic between settings.json loading and child_process.spawn. Likely a leftover proxy-agent allow/denylist that filters HTTP/HTTPS/NO_PROXY but not ALL_PROXY. Either remove the filter or document it.

Minimal repro

// ~/.claude/settings.json
{
  "env": {
    "HTTPS_PROXY": "http://example:8080",
    "HTTP_PROXY":  "http://example:8080",
    "NO_PROXY":    "localhost",
    "ALL_PROXY":   "http://example:8080",
    "FOO_BAR":     "visible"
  }
}

In the REPL, ask Claude to run:

printenv | grep -iE '^(https?_proxy|no_proxy|all_proxy|foo_bar)='

Expected: 5 lines. Actual: only ALL_PROXY / all_proxy / FOO_BAR.

---

Bug #2 (cascading) — OTEL_SDK_DISABLED=true does not stop OTEL exporter from opening connections

Symptom

settings.json.env.OTEL_SDK_DISABLED = "true" is loaded (visible in settingsEnv keys). Claude logs [3P telemetry] isTelemetryEnabled=false. But the OTEL exporter still opens TCP connections and times out:

[ERROR] [3P telemetry] OTEL diag error: {
  "message":"Operation timed out.",
  "sourceURL":"/$bunfs/root/src/entrypoints/cli.js",
  "line":"421","column":"107499"
}

Repeats every ~40 s.

ss -tnp while the process is hung shows ≥3 ESTAB TCP connections from the claude pid to the proxy — the OTEL SDK isn't disabled at the network-init layer, only at the application-emit layer.

Suggested fix direction

Short-circuit OTEL exporter creation entirely when OTEL_SDK_DISABLED=true is set, before any socket open. (Currently it appears to create the exporter and only the emit path is gated.)

---

Bug #3 (cascading) — showSetupScreens() blocks for 400+ seconds on non-essential network calls

Symptom

UI sits frozen at the login / setup screen with no feedback for 6 minutes 40 seconds. The --debug log proves it's not a deadlock — it's a await on non-essential outbound HTTPS that times out and is internally retried.

Hard evidence (full log timestamps from one session)

01:31:21.521 [DEBUG] [STARTUP] Running showSetupScreens()...
01:31:21.582 [DEBUG] [Perfetto] initializePerfettoTracing called
01:31:21.582 [DEBUG] [3P telemetry] isTelemetryEnabled=false
01:31:21.588 [DEBUG] DECSTBM: gated
                                                       ← UI frozen here
01:32:01.523 [ERROR] [3P telemetry] OTEL diag error: Operation timed out
                                                       ← still frozen
01:38:02.117 [DEBUG] [STARTUP] showSetupScreens() completed in 400595ms
01:38:02.119 [DEBUG] [API:auth] OAuth token check starting
01:38:42.721 [ERROR] [3P telemetry] OTEL diag error: Operation timed out  ← still retrying after REPL is up

Process state during the hang:

/proc/<pid>/status:    State: S (sleeping), Threads: 6
/proc/<pid>/wchan:     ep_poll
ss -tnp -- pid filter:
  ESTAB ... -> proxy:49998  fd=18
  ESTAB ... -> proxy:49998  fd=17
  ESTAB ... -> proxy:49998  fd=19

3 TCP connections established (so this is not a SYN failure — TCP handshake completed) but the upper layer is waiting on responses that never arrive within the implicit timeout, and the wait is effectively unbounded.

Suggested fix direction

  1. showSetupScreens() must not await non-essential network. Render the UI immediately; let telemetry / Statsig / autoupdate run fire-and-forget in the background.
  2. Cap any startup-path non-essential outbound at a tight (≤ 5 s) timeout.
  3. Provide a single kill switch env var that disables all non-essential outbound at startup, properly honored at the network-init layer (see Bug #2).

---

Why the four bugs likely share one root

  • Bug #4 is the underlying race in Bun's linux-x64 fetch through HTTP CONNECT.
  • Bug #3's intermittency matches Bug #4's intermittency exactly — sometimes the startup await returns fast (TLS race wins), sometimes it doesn't (and you wait 40 s ×N).
  • Bug #2 is the "isTelemetryEnabled=false but exporter still talks" mismatch — a flag layered above the network init, which only ever matters because Bug #4 makes that network init slow.
  • Bug #1 is most plausibly an artifact of Bun's internal "proxy agent injection" — it consumes HTTPS_PROXY/HTTP_PROXY/NO_PROXY from the env, applies them to its own internal HTTP agent, and forgets to put them back when spawning child processes. ALL_PROXY (which Bun's fetch arguably should also honor — see #14165) survives because nothing consumes it.

Fixing Bug #4 would dramatically improve user experience even if the other three remain; fixing all four restores parity with the 2.1.112 / arm64 baseline.

---

Workaround

npm i -g @anthropic-ai/claude-code@2.1.112

The bundled-JS 2.1.112 is 10/10 reliable on the same broken host. That's what we've fallen back to. Affected population: any x86_64 Linux user behind an HTTP CONNECT proxy (very common in enterprise deployments) on Claude Code ≥ 2.1.113.

---

Happy to attach raw strace logs, pcap captures, full --debug logs (showing the 400-second showSetupScreens hang and the 30-second Slow first byte warnings), and /proc/<pid>/{status,wchan,task} snapshots on request.

View original on GitHub ↗

11 Comments

liuwuyu118 · 4 months ago

Follow-up: this looks like part of a wider pattern — settings.json env block (and other env inputs) are not consistently honored after the v2.1.113 native-runtime switch.

A few related reports that, taken together, suggest the env-propagation path was not fully ported when the runtime changed from Node.js to the new native binary:

  • #50115Network and env-var docs still describe Claude Code as running on a Node.js runtime after v2.1.113. Documentation hasn't caught up with the runtime change, which is consistent with env semantics having shifted under the hood.
  • #50037ANTHROPIC_DEFAULT_*_MODEL from settings.json silently stripped under Desktop's PROVIDER_MANAGED_BY_HOST. Same shape: env field set in settings.json, runtime ignores it.
  • #50200model setting in settings.json not respected at session start. Another settings.json field not being honored on 2.1.113.

On our side (the original report), we tried adding the following to ~/.claude/settings.json env to rule out telemetry/non-essential traffic as the proxy hang trigger:

"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1",
"DISABLE_BUG_COMMAND": "1",
"DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1"

None of them changed observable behavior on 2.1.113 — startup hangs and WebFetch/WebSearch truncations continued at the same rate. On 2.1.112 (the last bundled-JS build) the same env block works as documented.

This makes us suspect the native binary's env-loading path from settings.json.env is incomplete — at minimum these DISABLE_* switches and HTTPS_PROXY/HTTP_PROXY aren't reaching whichever HTTP client the binary uses for the affected code paths.

Concrete ask: please verify the settings.jsonenv → runtime path end-to-end on the new native binary, with particular attention to:

  1. Proxy env vars (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, NO_PROXY and lowercase variants)
  2. The DISABLE_* / CLAUDE_CODE_DISABLE_* family
  3. Any helper / telemetry / startup-probe HTTP clients that may not share the main SDK's proxy configuration

Thanks!

mhegazy contributor · 4 months ago

Thanks for reporting. one note. for future issues please do not include multiple issues in the same report even if you believe they are originating from the same source. it makes state tracking much harder.

mhegazy contributor · 4 months ago

I have a fix landing for Bug #2.

for the others, i will need more info. Please use /feedback and share the link, this would give me the full debug-log timeline (Bug #3), the exact Bash tool input/output (Bug #1), and the API request timing (Bug #4).

liuwuyu118 · 4 months ago

@mhegazy Thanks for the triage.

Feedback ID: 5c1290c0-5456-4a59-aaf9-9b3df06a6c21

  • cc_version: 2.1.118.a9d
  • OS: Ubuntu 24.04.3, kernel 6.14.0-1018-oem, x86_64, glibc 2.39
  • Network: behind HTTP CONNECT proxy to api.anthropic.com:443

This single session captures three symptoms of what we believe is the same Bun fetch/socket race:

1. Startup blocks 400s inside showSetupScreens()

From the --debug log, contiguous lines 48–67 (nothing in between):

L48  22:45:03.498Z [STARTUP] Running showSetupScreens()...
L49-66  22:45:03.504-542Z  CA certs load, keybindings init, terminal caps probe
<<< 400.5 seconds of complete log silence — zero DEBUG lines >>>
L67  22:51:44.074Z [STARTUP] showSetupScreens() completed in 400576ms

Post-startup, OTEL diag fires at a consistent native frame:

22:52:24.209Z [ERROR] [3P telemetry] OTEL diag error: Operation timed out.
               stack: /$bunfs/root/src/entrypoints/cli.js:421:108938
22:54:10.341Z [ERROR] (same frame 421:108938)

OTEL_SDK_DISABLED=true is set; [3P telemetry] isTelemetryEnabled=false confirms the flag parses — yet something on that path still opens a socket and blocks for ~400s.

2. WebFetch: one of N parallel calls stalls while siblings succeed

User fired 4 parallel WebFetch calls. 3 returned in 2–3s. One hung for 44.7s until Esc:

L406  22:53:40.861Z [Stall] tool_dispatch_start tool=WebFetch toolUseId=toolu_01UpGD...
  (siblings complete in 2446ms / 2725ms / 3161ms)
L501  22:54:25.556Z [DEBUG] [onCancel] focusedInputDialog=undefined streamMode=tool-use
L502  22:54:25.559Z [WARN]  [Stall] tool_dispatch_end WebFetch toolu_01UpGD...
                    outcome=error durationMs=44698

[onCancel] arrives 3ms before tool_dispatch_end error — the cancel path released the socket, not a natural fetch completion/timeout.

3. /feedback submission itself hit the same race

Right after #2, while uploading this report:

23:01:46.602Z [DEBUG] [API REQUEST] /v1/messages source=feedback
23:02:16.604Z [WARN]  Slow first byte: no stream chunk 30.0s after request sent (attempt 1)

It eventually recovered and produced the Feedback ID above — but a 30s+ stall on a third, unrelated code path (feedback upload vs tool-use vs OTEL exporter) reinforces the common factor: Bun's fetch/socket layer on linux-x64 behind HTTP CONNECT, not any specific feature.

See the follow-up comment for a stricter reproduction of symptom #2 (cancel truly not honored) from a second machine.

liuwuyu118 · 4 months ago

Follow-up on a stricter form of WebFetch cancel-not-honored, from a second machine:

Feedback ID: bd8b02c5-8222-428c-a26e-20ff9995f4ea

  • cc_version: 2.1.118.c75 (different 2.1.118 build hash — same symptom)
  • OS: Ubuntu 24.04.3, kernel 6.8.0-107-generic, x86_64, glibc 2.39
  • Network: same HTTP CONNECT proxy

User pressed Esc three times across three different streamMode values. The underlying Bun fetch was never torn down — log ends without a dispatch_end for that toolUseId:

L1866  22:23:37.983Z [Stall] tool_dispatch_start tool=WebFetch toolUseId=toolu_01QPz68K...
L1890  22:25:07.188Z [DEBUG] [onCancel] streamMode=tool-use       ← 1st Esc, 89s in
L1952  22:25:25.837Z [DEBUG] [onCancel] streamMode=thinking       ← 2nd Esc, 108s in
L1986  22:26:32.237Z [WARN]  Slow first byte: no stream chunk 30.0s after request sent
L1987  22:28:03.538Z [DEBUG] [onCancel] streamMode=requesting     ← 3rd Esc, 266s in
  ... (log continues 11 more minutes, no tool_dispatch_end for toolu_01QPz68K ever)
L2156  22:34:52.792Z [API REQUEST] /v1/messages source=feedback   ← /feedback filed while WebFetch still in-flight

Three different [onCancel] streamMode= values confirm the UI cancel reached the orchestrator in all three UI states (tool-usethinkingrequesting), but the underlying fetch/socket never unblocked. This session had to file /feedback with the dead WebFetch still hanging in the background.

Together with the primary comment: two different 2.1.118 build hashes (a9d and c75), two different kernels (6.14.0-oem and 6.8.0-generic), identical symptoms — rules out build-specific or kernel-specific causes, leaves the Bun runtime + linux-x64 + HTTP CONNECT intersection.

Already ruled out (per OP): network (curl/tcpdump OK via same proxy), packaging (identical BuildID across distribution channels), ARM64 (10/10 OK same settings), CPU count (8-core reporter identical), strace (5/5 OK under strace, classic async I/O race cure), 7 Bun tuning env flags (no effect).

Happy to run targeted traces if an instrumented build is cut.

liuwuyu118 · 4 months ago

Update — minimal repro isolated to a TCP-level pattern in CONNECT 200 response handling.

We narrowed this down to a single behavior: Bun-x64 fetch hangs when the upstream HTTP CONNECT proxy delivers HTTP/1.1 200 OK\r\n\r\n across two TCP segments instead of one.

No exotic transport involved — reproduced with a Mac on the same LAN running a 50-line Python socket proxy. Two run modes, only difference is one sendall vs two consecutive sendalls for the CONNECT 200 line.

Repro setup

  • Linux x86_64 (Ubuntu 24.04.3, kernel 6.8.0-107-generic, glibc 2.39) running claude 2.1.119 (Bun-x64 native build, cc_version: 2.1.118.c75 series)
  • LAN-local Mac running Python 3 with the proxy script below, listening on a Tailscale IP, port 49996
  • ubr → CONNECT → Mac:49996 → upstream api.anthropic.com:443
  • Test command: claude -p --dangerously-skip-permissions --verbose --output-format stream-json --model claude-haiku-4-5-20251001 'Respond with only the word ok', with a 45s timeout wrapper, repeated 5×.

Python CONNECT proxy (drop in and run)

#!/usr/bin/env python3
"""Minimal CONNECT proxy. --mode normal sends the 200 line in one sendall;
   --mode split sends "HTTP/1.1 200 OK\\r\\n" then "\\r\\n" as two sendalls."""
import argparse, socket, threading

def relay(src, dst):
    try:
        while True:
            data = src.recv(65536)
            if not data: break
            dst.sendall(data)
    except Exception: pass
    finally:
        try: src.shutdown(socket.SHUT_RD)
        except Exception: pass
        try: dst.shutdown(socket.SHUT_WR)
        except Exception: pass

def handle(client, mode):
    try:
        buf = b""
        while b"\r\n\r\n" not in buf:
            chunk = client.recv(4096)
            if not chunk: return
            buf += chunk
        first = buf.split(b"\r\n", 1)[0].decode("ascii", errors="replace")
        if not first.upper().startswith("CONNECT "):
            client.sendall(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n"); return
        host, _, port = first.split(" ")[1].partition(":")
        upstream = socket.create_connection((host, int(port or 443)), timeout=10)
        if mode == "split":
            client.sendall(b"HTTP/1.1 200 OK\r\n")
            client.sendall(b"\r\n")
        else:
            client.sendall(b"HTTP/1.1 200 OK\r\n\r\n")
        t1 = threading.Thread(target=relay, args=(client, upstream), daemon=True)
        t2 = threading.Thread(target=relay, args=(upstream, client), daemon=True)
        t1.start(); t2.start(); t1.join(); t2.join()
    finally:
        try: client.close()
        except Exception: pass

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--listen", default="0.0.0.0:49996")
    ap.add_argument("--mode", choices=["normal", "split"], default="normal")
    a = ap.parse_args()
    h, _, p = a.listen.partition(":")
    s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((h, int(p))); s.listen(64)
    print(f"[*] listening {h}:{p} mode={a.mode}", flush=True)
    while True:
        c, _ = s.accept()
        threading.Thread(target=handle, args=(c, a.mode), daemon=True).start()

if __name__ == "__main__": main()

Results (5 trials each, exact same network, exact same destination, exact same Bun binary)

| Proxy mode | CONNECT 200 wire format | Outcome |
|---|---|---|
| --mode normal | one TCP segment: HTTP/1.1 200 OK\r\n\r\n (19 bytes) | 5/5 OK, 10-15s each |
| --mode split | two TCP segments: HTTP/1.1 200 OK\r\n (17 bytes) + \r\n (2 bytes) | 1/5 OK, 4/5 TIMEOUT at 45s; the one OK trial took 31s |

Hypothesis

Bun-x64's HTTP CONNECT response parser appears to commit to "tunnel established → switch to TLS passthrough" as soon as it sees the status line + first CRLF, without waiting for the empty-line terminator (the second \r\n). When the second \r\n arrives in a separate TCP read, those 2 bytes are consumed as if they were the start of the TLS ServerHello, the TLS record framing is then permanently misaligned by 2 bytes, and the HTTPS/2 stream can never reach END_STREAM / GOAWAY. The fetch hangs until external timeout/cancel.

This explains all of the previously-reported symptoms in this issue, including the WebFetch "cancel not honored" cases — once the TLS framing is corrupted, even an AbortSignal / socket.destroy() on Bun's side can't reach a coherent "done" state, because the application layer is still waiting for bytes that will never parse.

Why it appears intermittent on real proxies

Most production HTTP CONNECT proxies (squid, tinyproxy, Tailscale's built-in CONNECT proxy, etc.) write the entire response with a single write(2) and the kernel sends one TCP segment, so they never trigger this. Some proxy implementations — particularly transport-converting proxies that bridge TCP↔QUIC or TCP↔WebSocket — naturally split the response across packet boundaries, so they trigger it ~100% of the time. ARM64 Bun on the same network does not appear to hit this, suggesting the parser bug is specific to the x86_64 native build (or its IO read sizing).

Note

If a follow-up trace under an instrumented Bun build would help, happy to run it on the same setup. The repro takes < 1 minute end-to-end.

liuwuyu118 · 4 months ago

Cross-architecture control: aarch64 is unaffected.

Ran the same --mode split Python CONNECT proxy from the previous comment, against an aarch64 host on the same LAN, talking to the same destination over the same Tailscale link. The only variable changed is the architecture of the claude binary on the client side.

| Host | Arch | claude version | Same --mode split proxy | Result |
|---|---|---|---|---|
| Linux x86_64, kernel 6.8.0-107-generic | x86_64 | 2.1.119 | (same Python script, same Tailscale link, same upstream) | 1/5 OK, 4/5 TIMEOUT at 45s |
| Linux aarch64, kernel 6.14.0-1015-nvidia | aarch64 | 2.1.118 | same | 5/5 OK, 5-7s each |

Same 5-trial loop, same prompt, same model (claude-haiku-4-5-20251001), same --output-format stream-json --verbose. Only the architecture of the local claude binary differs.

This narrows it further: the buggy CONNECT-200 read path is in the x86_64 native build specifically, not in shared cross-platform code. Likely candidates would be x86_64-only fast paths in Bun's HTTP/socket reader (e.g. SIMD scan over the response head, or x86_64-tuned recv buffer sizing that takes a different branch when the first read returns ≤ a threshold of bytes).

Hopefully helpful for narrowing the search; happy to run any further targeted comparisons on either build.

liuwuyu118 · 4 months ago

Confirmed root cause + working sender-side workaround.

Filed an upstream issue and PR on the proxy side that triggered this in our environment:

  • Issue: apernet/hysteria#1553 — client/http: CONNECT 200 reply is split across 2 TCP segments, breaking framing-sensitive clients (e.g. Bun x86_64 / Claude Code)
  • PR: apernet/hysteria#1554 — 12-line patch buffering the CONNECT 200 reply into a single conn.Write so the kernel emits a single TCP segment

The hysteria-side trigger turns out to be Go stdlib net/http.Response.Write — for a ContentLength=-1 headerless response it issues two separate underlying writes (status line, then trailing CRLF), and Go defaults *net.TCPConn to TCP_NODELAY=true, so the bytes leave as two TCP segments. This matches the wire pattern the Python --mode split reproducer in comment 4319520748 was simulating.

End-to-end verification on the same machine (Ubuntu 24.04.3 x86_64, kernel 6.8.0-107, Claude Code 2.1.119):

| hysteria build | claude -p 5-trial result |
|---|---|
| v2.8.1 stock | 1/5 OK, 4/5 timeout at 45s |
| v2.8.1 + apernet/hysteria#1554 | 5/5 OK, 9–15s each |

tcpdump -i lo on the local proxy port confirms the 200 response is now a single 19-byte TCP segment (was 17 + 2 before).

This is a sender-side workaround that unblocks anyone hitting this through hysteria2; it does not address the underlying Bun-x86_64 CONNECT/TLS framing fragility, which still affects any other CONNECT proxy that happens to split the response (transport-converting proxies, some QUIC/WebSocket bridges, etc.). Posting in case it helps narrow the search on the Bun side: the fragile path is "first segment ends exactly at status-line \r\n, second segment is just the empty-line \r\n" — Bun-aarch64 handles this fine, only Bun-x86_64 misframes.

Happy to run any further targeted comparisons against either build.

jrfeenst · 3 months ago

I'm not sure if this is related but I'm not seeing my HTTP_PROXY and HTTPS_PROXY env vars reliably passed through claude into Bash subprocesses like I used to on windows. I tried setting 'settings.enableWeakerNetworkIsolation' but that only let HTTP_PROXY pass through, not HTTPS_PROXY. I can't use settings.json since my port changes per claude instance.

PhiFever · 3 months ago

Workaround confirmed on aarch64 / claude-code 2.1.131 — rebuild hysteria with apernet/hysteria#1554

Adding a data point that the architecture profile has shifted on recent claude-code releases:

| host | claude-code | local proxy | result with stock hysteria |
|---|---|---|---|
| Raspberry Pi 4 Model B Rev 1.5, aarch64, Ubuntu 24.04.4 LTS, kernel 6.8.0-1053-raspi | 2.1.131 (Bun native) | hysteria2 client v2.9.1, HTTP listener | TLS handshake hang / fetch timeout — same symptom previously reported only on x86_64 |

So the earlier "Bun-aarch64 is not affected" cross-arch result from the 2.1.118/2.1.119 era no longer holds on 2.1.131. Whether the cause is a Bun upgrade or something else in the native build, I haven't dug in — but anyone on ARM seeing the same hangs should know the workaround tree is now the same as for x86_64.

Sender-side fix that I just verified works end-to-end (system has Go, native build on the Pi):

git clone https://github.com/apernet/hysteria.git
cd hysteria
git checkout app/v2.9.1
curl -sSL https://github.com/apernet/hysteria/pull/1554.diff | git apply --3way
python3 -m pip install -r requirements.txt
python3 hyperbole.py build
# replace the system binary with ./build/hysteria-linux-arm64

After swap-in, claude operations through the same proxy went from intermittent hangs to clean. Until either the Bun-side parser fix lands here or apernet/hysteria#1554 merges upstream, this is the cleanest workaround for ARM users behind hysteria2.

github-actions[bot] · 2 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

Showing cached comments. Read the full discussion on GitHub ↗