From 9b3b586006080987de69444f7cabe1172b3e67c6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 13 Jul 2026 08:45:30 +0700 Subject: [PATCH] fix(network): harden audit findings in transport, uTLS pool, and cover cache - sharedTransport ResponseHeaderTimeout 45s -> 120s: downloads ride this transport and some providers prepare files server-side before the first byte; the stall watchdog still bounds dead transfers - uTLS pool: a request that fails on a pooled (possibly silently dead) connection now re-dials once when the body is repeatable, matching the old dial-per-request reliability - cover singleflight completes via defer with a default error so a panicking fetch can never strand waiters in (nil, nil) or leave a permanent in-flight entry --- go_backend/cover.go | 16 ++++++++++------ go_backend/httputil.go | 16 ++++++++++------ go_backend/httputil_utls.go | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/go_backend/cover.go b/go_backend/cover.go index 78397f9c..c274bedc 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -108,21 +108,25 @@ func fetchCoverCached(downloadURL string) ([]byte, error) { return call.data, call.err } call := &coverInflightCall{} + // Default error so a panicking fetch never strands waiters with a + // (nil, nil) "success"; overwritten on normal completion. + call.err = fmt.Errorf("cover fetch aborted") call.wg.Add(1) coverInflight[downloadURL] = call coverMu.Unlock() + defer func() { + call.wg.Done() + coverMu.Lock() + delete(coverInflight, downloadURL) + coverMu.Unlock() + }() + data, err := coverFetch(downloadURL) call.data, call.err = data, err if err == nil { coverCachePut(downloadURL, data) } - call.wg.Done() - - coverMu.Lock() - delete(coverInflight, downloadURL) - coverMu.Unlock() - return data, err } diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 15e95b20..9554d68c 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -68,12 +68,16 @@ var sharedTransport = &http.Transport{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - MaxConnsPerHost: 20, - IdleConnTimeout: 60 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ResponseHeaderTimeout: 45 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + MaxConnsPerHost: 20, + IdleConnTimeout: 60 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + // Downloads ride this transport; some extension providers prepare the + // file server-side before the first byte, so give TTFB more headroom + // than the API/metadata transports. The 60s stall watchdog still bounds + // dead transfers. + ResponseHeaderTimeout: 120 * time.Second, ExpectContinueTimeout: 1 * time.Second, DisableKeepAlives: false, ForceAttemptHTTP2: true, diff --git a/go_backend/httputil_utls.go b/go_backend/httputil_utls.go index 3a62fcfc..346857a1 100644 --- a/go_backend/httputil_utls.go +++ b/go_backend/httputil_utls.go @@ -49,10 +49,19 @@ func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { if cc := t.cachedConn(addr); cc != nil { resp, err := cc.RoundTrip(req) - if err != nil { - t.invalidate(addr, cc) + if err == nil { + return resp, nil } - return resp, err + // A pooled conn can be silently dead after a network switch. Drop it + // and, when the request is safely repeatable, fall through to a fresh + // dial instead of failing where the old dial-per-request code would + // have succeeded. + t.invalidate(addr, cc) + retryReq, ok := rewindRequestBody(req) + if !ok { + return nil, err + } + req = retryReq } tlsConn, proto, err := t.dial(req.Context(), host, addr) @@ -80,6 +89,25 @@ func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { return cc.RoundTrip(req) } +// rewindRequestBody returns a request whose body can be sent again after a +// failed attempt on a pooled connection: bodyless requests as-is, requests +// with GetBody with a rebuilt body, anything else not ok. +func rewindRequestBody(req *http.Request) (*http.Request, bool) { + if req.Body == nil { + return req, true + } + if req.GetBody == nil { + return nil, false + } + body, err := req.GetBody() + if err != nil { + return nil, false + } + retryReq := req.Clone(req.Context()) + retryReq.Body = body + return retryReq, true +} + // dial opens a TCP connection and completes the Chrome-fingerprint TLS handshake, // returning the connection and negotiated ALPN protocol. func (t *utlsTransport) dial(ctx context.Context, host, addr string) (*utls.UConn, string, error) {