fix(handlers): write raw HTTP response after Hijack to bypass buffered writer
Root cause of the 2m8s hang (which matched ResponseHeaderTimeout=180s): httptest's Hijack() discards the buffered writer, losing any bytes written via w.WriteHeader/w.Write that weren't already flushed to the raw TCP conn. The HTTP client therefore never receives response headers, blocking on ResponseHeaderTimeout (3 min). Fix: write the raw HTTP response directly to the raw conn AFTER Hijack(), completely bypassing httptest's buffered writer. This ensures: - Response headers reach the client immediately (not lost to buffered writer) - Client starts reading the response body - conn.Close() fires while client is mid-read → Read() returns EOF/error - executeDelegation completes in seconds, not minutes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
18355375fe
commit
56fd24d339
@ -136,41 +136,43 @@ func readDelegationRow(t *testing.T, conn *sql.DB) (status, preview, errorDetail
|
||||
}
|
||||
|
||||
// mockAgentWithPartialBody creates an httptest.Server that:
|
||||
// - Sends HTTP status + headers with Content-Length > actual body size.
|
||||
// - Writes a partial body and Flush()es via http.Flusher (sends to client).
|
||||
// - Immediately Hijack()s and Close()s the raw TCP connection.
|
||||
// - Hijacks the raw TCP connection after the HTTP parser consumes request
|
||||
// headers (so r.Body is still in-flight from the client).
|
||||
// - Writes raw HTTP response bytes directly to the raw conn so they bypass
|
||||
// httptest's buffered writer (which Hijack() discards, losing any unflushed
|
||||
// data that was written via w.WriteHeader/w.Write).
|
||||
// - Closes the raw conn while the client is mid-read on the body.
|
||||
//
|
||||
// Key insight (from a2a_proxy_test.go's TestProxyA2A_BodyReadFailure):
|
||||
// We do NOT read or close r.Body. Closing r.Body triggers the Go HTTP server's
|
||||
// pipe mechanism to signal an EOF to the body reader — which on the CLIENT side
|
||||
// causes the request-body writer to fail with "read from closed pipe". This hangs
|
||||
// the request indefinitely. The fix: just Hijack() and return without touching
|
||||
// r.Body. The HTTP server manages r.Body cleanup when the handler returns.
|
||||
// Critical insight: httptest's Hijack() discards the buffered writer, which
|
||||
// contains any bytes written via w.WriteHeader/w.Write that weren't flushed to
|
||||
// the raw TCP conn. This means the HTTP client NEVER receives the response
|
||||
// headers (blocked by ResponseHeaderTimeout = 3 minutes).
|
||||
// Fix: write the raw HTTP response directly to the raw conn AFTER Hijack(),
|
||||
// bypassing the buffered writer entirely.
|
||||
func mockAgentWithPartialBody(t *testing.T, statusCode int, declaredLength int, actualBody string) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", declaredLength))
|
||||
w.WriteHeader(statusCode)
|
||||
// Write partial body and flush so the client receives it.
|
||||
w.Write([]byte(actualBody)) //nolint:errcheck
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// Hijack: flushes the buffered writer, returns raw conn.
|
||||
// Close: sends FIN/RST to client → client's next Read() errors.
|
||||
// Do NOT touch r.Body — matches a2a_proxy_test.go pattern exactly.
|
||||
// Do NOT touch r.Body — server manages it.
|
||||
if hj, ok := w.(http.Hijacker); ok {
|
||||
conn, bufWriter, _ := hj.Hijack()
|
||||
if conn != nil {
|
||||
// Close the buffered writer (no-op on already-flushed data).
|
||||
if bw, ok := bufWriter.(io.Closer); ok {
|
||||
bw.Close()
|
||||
}
|
||||
conn.Close()
|
||||
// Write raw HTTP response bytes DIRECTLY to the raw conn.
|
||||
// Bypasses httptest's buffered writer so Hijack() can't lose them.
|
||||
// Content-Length > actualBody: server announces more bytes than it
|
||||
// sends before closing — simulates a mid-stream connection drop.
|
||||
header := fmt.Sprintf("HTTP/1.1 %d %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n",
|
||||
statusCode, http.StatusText(statusCode), declaredLength)
|
||||
conn.Write([]byte(header)) //nolint:errcheck
|
||||
conn.Write([]byte(actualBody)) //nolint:errcheck
|
||||
// Brief delay so client reads headers + partial body before close.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
conn.Close() // FIN/RST → client's next Read() errors
|
||||
}
|
||||
}
|
||||
// Return WITHOUT touching r.Body. The HTTP server manages it.
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user