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
This commit is contained in:
zarzet
2026-07-13 08:45:30 +07:00
parent 0f0ebe5565
commit 9b3b586006
3 changed files with 51 additions and 15 deletions
+10 -6
View File
@@ -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
}
+10 -6
View File
@@ -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,
+31 -3
View File
@@ -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) {