fix: Repeater 一次性传输取消阶段超时并回收空闲连接,错误时不重复显示(Codex gpt-6.1-sol)(#42)

This commit is contained in:
RuoJi6 committed 2026-10-25 00:02:43 +08:00
1 parent 91d2fe456c
commit 32d732bdfa
3 files changed
+79 -1

No files matched your search

+6
View File
@@ -167,6 +167,9 @@ func (s *Server) sendRepeater(w http.ResponseWriter, r *http.Request) {
return nil return nil
}, },
} }
// Each send owns its transport. Registered before Body.Close so idle
// connections are released after the response body, including on errors.
defer client.CloseIdleConnections()
started := time.Now() started := time.Now()
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -235,6 +238,9 @@ func (s *Server) repeaterTransport(useRecordingProxy, insecure bool) http.RoundT
// Keep response bytes faithful and make body_bytes match what was received; // Keep response bytes faithful and make body_bytes match what was received;
// transparent gzip decompression would otherwise obscure both values. // transparent gzip decompression would otherwise obscure both values.
transport.DisableCompression = true transport.DisableCompression = true
// Use the request's timeout_ms budget instead of inherited phase deadlines.
transport.TLSHandshakeTimeout = 0
transport.ExpectContinueTimeout = 0
if !proxied { if !proxied {
// Clear a custom TLS dialer before installing the guarded dialer; when // Clear a custom TLS dialer before installing the guarded dialer; when
// DialTLSContext is set, net/http bypasses DialContext entirely. // DialTLSContext is set, net/http bypasses DialContext entirely.
+72
View File
@@ -161,6 +161,78 @@ func TestRepeaterDirectHTTP2(t *testing.T) {
} }
} }
func TestRepeaterTLSHandshakeUsesRequestTimeout(t *testing.T) {
t.Setenv(repeaterAllowLocalEnv, "1")
// Scale down the inherited timeout so a slow handshake can be tested
// without waiting for the default transport's ten-second deadline.
base := http.DefaultTransport
short := base.(*http.Transport).Clone()
short.TLSHandshakeTimeout = 10 * time.Millisecond
http.DefaultTransport = short
defer func() { http.DefaultTransport = base }()
defer short.CloseIdleConnections()
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, "handshake completed")
}))
srv.TLS = &tls.Config{GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
time.Sleep(100 * time.Millisecond)
return nil, nil
}}
srv.StartTLS()
defer srv.Close()
code, out, raw := repeaterTestSend(t, &Server{}, map[string]any{
"url": srv.URL, "use_recording_proxy": false, "insecure_skip_verify": true, "timeout_ms": 1000,
})
if code != http.StatusOK || out.Error != "" || out.Status != http.StatusOK || out.Body != "handshake completed" {
t.Fatalf("handshake within request budget failed: status=%d raw=%s", code, raw)
}
}
func TestRepeaterClosesIdleConnections(t *testing.T) {
t.Setenv(repeaterAllowLocalEnv, "1")
for _, mode := range []string{"direct", "http2", "global proxy", "recording proxy fallback"} {
t.Run(mode, func(t *testing.T) {
closed := make(chan struct{}, 1)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, "response")
}))
srv.Config.ConnState = func(_ net.Conn, state http.ConnState) {
if state == http.StateClosed {
closed <- struct{}{}
}
}
if mode == "http2" {
srv.EnableHTTP2 = true
srv.StartTLS()
} else {
srv.Start()
}
defer srv.Close()
s := &Server{}
payload := map[string]any{"url": srv.URL, "use_recording_proxy": false, "insecure_skip_verify": true}
if mode == "global proxy" || mode == "recording proxy fallback" {
s.m = &Manager{globalProxy: srv.URL}
payload["url"] = "http://target.example/"
if mode == "recording proxy fallback" {
delete(payload, "use_recording_proxy")
}
}
code, out, raw := repeaterTestSend(t, s, payload)
if code != http.StatusOK || out.Error != "" || out.Status != http.StatusOK || out.Body != "response" {
t.Fatalf("status=%d raw=%s", code, raw)
}
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("connection still open after sendRepeater returned")
}
})
}
}
func TestRepeaterRoundTripHeadersAndBody(t *testing.T) { func TestRepeaterRoundTripHeadersAndBody(t *testing.T) {
t.Setenv(repeaterAllowLocalEnv, "1") t.Setenv(repeaterAllowLocalEnv, "1")
var got http.Request var got http.Request
@@ -67,7 +67,7 @@ export function formatHeaders(headers: Record<string, string[]> | Record<string,
} }
export function buildRawResponse(response: RepeaterSendResponse): string { export function buildRawResponse(response: RepeaterSendResponse): string {
if (!response.status && response.error) return `错误: ${response.error}`; if (!response.status) return "";
const statusLine = `${response.proto || "HTTP/1.1"} ${response.status_text || response.status}`.trim(); const statusLine = `${response.proto || "HTTP/1.1"} ${response.status_text || response.status}`.trim();
const headers = formatHeaders(response.headers); const headers = formatHeaders(response.headers);
return `${statusLine}\n${headers}\n\n${response.body}`; return `${statusLine}\n${headers}\n\n${response.body}`;