Skip to content

client/http: CONNECT 200 reply is split across 2 TCP segments, breaking framing-sensitive clients (e.g. Bun x86_64 / Claude Code) #1553

Description

@liuwuyu118

Logs

2026-04-25T20:20:11+08:00        INFO        client mode
2026-04-25T20:20:11+08:00        INFO        connected to server        {"udpEnabled": true, "tx": 4750000, "count": 1}
2026-04-25T20:20:11+08:00        INFO        HTTP proxy server listening        {"addr": "0.0.0.0:49998"}

(Server logs are uneventful — no errors. The client is healthy; the issue is purely in how the client writes the CONNECT 200 response back to the local browser/CLI.)

Summary

When a client speaks HTTP CONNECT to http.listen on hysteria2 client, the HTTP/1.1 200 OK\r\n\r\n reply is emitted on the wire as two TCP segments (17 bytes HTTP/1.1 200 OK\r\n then 2 bytes \r\n). For framing-sensitive downstream HTTP CONNECT parsers — most notably Bun's x86_64 native build (used by Anthropic's Claude Code CLI, anthropics/claude-code#50252) — the 2 trailing bytes are consumed as TLS handshake data, the TLS record framing is permanently misaligned, and the HTTPS/2 fetch hangs until external timeout.

Bun-aarch64 is not affected; Bun-x86_64 is. The bug is in Bun's parser, but the trigger is hy2's split write, and we control that side.

This is the same class of issue as #1109 / #1110 (ffmpeg compatibility): a real-world client doesn't tolerate the exact byte boundaries that Go's net/http.Response.Write produces, even though the wire bytes are technically valid.

Root cause (verified in source)

app/internal/http/server.go::sendSimpleResponse (current master, line ~283) ends with:

return resp.Write(conn)

where conn is the raw *net.TCPConn accepted from the local listener.

Go stdlib net/http.Response.Write writes the response in pieces:

  • response.go:259fmt.Fprintf(w, "HTTP/%d.%d %03d %s\r\n", ...) (one Write, 17 bytes for "HTTP/1.1 200 OK\r\n")
  • response.go:322io.WriteString(w, "\r\n") (one Write, 2 bytes)

For a CONNECT 200 with ContentLength=-1 and an empty Header, no other bytes are written between these two. Because Go sets TCP_NODELAY=true on *net.TCPConn by default (net/tcpsock.go:222), each application-level Write becomes its own TCP segment.

Reproduce

A 50-line Python proxy reproduces the exact wire pattern with no hy2 involved:

# normal mode → 1 sendall ("HTTP/1.1 200 OK\r\n\r\n")  → 5/5 OK in Claude Code
# split  mode → 2 sendalls ("HTTP/1.1 200 OK\r\n" then "\r\n") → 4/5 timeout

Full reproducer + cross-architecture data is in anthropics/claude-code#50252 (comment) and anthropics/claude-code#50252 (comment).

Verified fix (12-line patch on master)

Buffer the simple response into a bytes.Buffer and emit it in one conn.Write. bytes is already imported. Wire bytes are unchanged; only the syscall boundary moves.

// app/internal/http/server.go
func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error {
    resp := &http.Response{ /* unchanged */ }
    resp.ContentLength = -1
    resp.Close = false
    resp.Uncompressed = true
    var buf bytes.Buffer
    if err := resp.Write(&buf); err != nil {
        return err
    }
    _, err := conn.Write(buf.Bytes())
    return err
}
// same change applied to sendProxyAuthRequired

End-to-end results on Linux x86_64 (Ubuntu 24.04, kernel 6.8.0, hysteria v2.8.1 + this patch, Claude Code 2.1.119):

Build Trials Latency
v2.8.1 stock 1/5 OK, 4/5 TIMEOUT at 45s timeout
v2.8.1 + patch 5/5 OK 9–15s

tcpdump -i lo confirms the 200 response is now a single 19-byte TCP segment (previously 17 + 2).

Versions

  • hysteria: v2.8.1 (also reproduces on v2.7.1 and master HEAD; app/internal/http/server.go::sendSimpleResponse is byte-identical across all three)
  • OS: Ubuntu 24.04.3, kernel 6.8.0-107-generic
  • Affected downstream client: Bun x86_64 native build, as used by Claude Code 2.1.119 (Anthropic). Bun aarch64 is not affected.

PR coming next, modeled on #1110.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions