fix(client/http): coalesce simple proxy response into a single Write#1554
Open
liuwuyu118 wants to merge 1 commit into
Open
fix(client/http): coalesce simple proxy response into a single Write#1554liuwuyu118 wants to merge 1 commit into
liuwuyu118 wants to merge 1 commit into
Conversation
Go's net/http.Response.Write emits the status line and the trailing CRLF
as separate Write calls. For headerless responses (CONNECT 200, 502, etc.)
this produces ≥2 underlying syscalls, and because *net.TCPConn defaults to
TCP_NODELAY=true, the bytes go onto the wire as separate TCP segments.
This breaks framing-sensitive HTTP/CONNECT clients that begin TLS handshake
immediately after seeing the status line + first CRLF without waiting for
the full \r\n\r\n. A concrete example is Bun's x86_64 native build (used in
Anthropic's Claude Code CLI 2.1.119): when the CONNECT 200 arrives in two
TCP segments ("HTTP/1.1 200 OK\r\n" then "\r\n"), the trailing two bytes
get consumed as TLS data, the TLS record framing is permanently misaligned,
and the HTTPS/2 stream hangs until external timeout. A 50-line Python
reproducer demonstrates 4/5 timeouts vs 5/5 OK depending only on whether
the proxy issues 1 or 2 sendalls for the response.
Same class of fix as apernet#1110 (ffmpeg compatibility): keep the wire bytes
identical, just buffer the small response and emit it in one conn.Write
so the kernel produces a single segment.
Verified end-to-end on Linux x86_64 (Ubuntu 24.04, kernel 6.8.0,
hysteria v2.8.1 + this patch) with Claude Code 2.1.119 behind hy2:
- before patch: 1/5 OK, 4/5 timeout
- after patch: 5/5 OK, 9-15s each
- tcpdump confirms the response goes out as a single 19-byte segment
(previously two segments of 17 + 2 bytes)
The same buffering is applied to sendProxyAuthRequired so that 407
responses with the Proxy-Authenticate header are also emitted as a
single segment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1553.
What
Make
sendSimpleResponseandsendProxyAuthRequiredserialize the response into abytes.Bufferand emit it with a singleconn.Write, instead of letting Go stdlibnet/http.Response.Writeissue separate writes for the status line and the trailing CRLF.The wire bytes are unchanged. Only the application-level syscall boundary moves.
Why
For a CONNECT 200 with
ContentLength=-1and an emptyHeader,http.Response.Writeemits two underlyingWritecalls (status line + final CRLF). Because Go setsTCP_NODELAY=trueon*net.TCPConnby default, the bytes go on the wire as two TCP segments (HTTP/1.1 200 OK\r\n+\r\n).Some real-world HTTP CONNECT clients begin TLS handshake the moment they see the status line + first CRLF and don't tolerate the trailing 2 bytes arriving in a separate segment. The most visible current example is Bun's x86_64 native build, which ships in Anthropic's Claude Code CLI: when the response is split, Bun consumes the trailing
\r\nas TLS data, the TLS record framing is permanently misaligned, and the HTTPS/2 fetch hangs until external timeout. Bun aarch64 is not affected. Full repro and cross-architecture data is in #1553 and in anthropics/claude-code#50252.This is the same class of fix as #1110 (ffmpeg compatibility): keep the wire bytes identical, just adjust how hy2 hands them to the kernel so a single TCP segment is produced.
Verification
End-to-end on Linux x86_64 (Ubuntu 24.04.3, kernel 6.8.0-107, hysteria v2.8.1 + this patch, Claude Code 2.1.119 behind hy2):
tcpdump -i loon the local hy2 listener:Flags [P.], length 17thenFlags [P.], length 2Flags [P.], length 19(single segment, exactlyHTTP/1.1 200 OK\r\n\r\n)The same change is applied to
sendProxyAuthRequiredso 407 responses with theProxy-Authenticateheader are also emitted as a single segment.Compatibility
bytesis already imported in this file; no new dependencies. The buffered response is at most a few hundred bytes (a CONNECT 200 is exactly 19 bytes), so the extra allocation is negligible.No behavior change for compliant clients: the wire bytes — including byte order, headers, and content — are byte-for-byte identical.