fix: split connection and download timeouts in httpcache

ConnTimeout (default 5s) caps TCP connect + TLS handshake via net.Dialer
and Transport.TLSHandshakeTimeout. Timeout (default 5m) caps the overall
request including body read. Previously a single 30s timeout covered both,
which was too short for large downloads and too long for connection failures.
This commit is contained in:
AJ ONeal 2026-04-20 09:56:24 -06:00
parent 86ffa2fb23
commit d0a5e0a9d2
No known key found for this signature in database

View File

@ -3,13 +3,17 @@ package httpcache
import ( import (
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"os" "os"
"sync" "sync"
"time" "time"
) )
const defaultTimeout = 30 * time.Second const (
defaultConnTimeout = 5 * time.Second // TCP connect + TLS handshake
defaultTimeout = 5 * time.Minute // overall including body read
)
// Syncer is implemented by any value that can fetch a remote resource and // Syncer is implemented by any value that can fetch a remote resource and
// report whether it changed. Both *Cacher and *gitshallow.Repo satisfy this. // report whether it changed. Both *Cacher and *gitshallow.Repo satisfy this.
@ -43,7 +47,8 @@ func (NopSyncer) Fetch() (bool, error) { return false, nil }
type Cacher struct { type Cacher struct {
URL string URL string
Path string Path string
Timeout time.Duration // 0 uses 30s ConnTimeout time.Duration // 0 uses 5s; caps TCP connect + TLS handshake
Timeout time.Duration // 0 uses 5m; caps overall request including body read
MaxAge time.Duration // 0 disables; skip HTTP if file mtime is within this MaxAge time.Duration // 0 disables; skip HTTP if file mtime is within this
MinInterval time.Duration // 0 disables; skip HTTP if last Fetch attempt was within this MinInterval time.Duration // 0 disables; skip HTTP if last Fetch attempt was within this
Username string // Basic Auth — not forwarded on redirects Username string // Basic Auth — not forwarded on redirects
@ -86,6 +91,10 @@ func (c *Cacher) Fetch() (updated bool, err error) {
} }
c.lastChecked = time.Now() c.lastChecked = time.Now()
connTimeout := c.ConnTimeout
if connTimeout == 0 {
connTimeout = defaultConnTimeout
}
timeout := c.Timeout timeout := c.Timeout
if timeout == 0 { if timeout == 0 {
timeout = defaultTimeout timeout = defaultTimeout
@ -102,20 +111,26 @@ func (c *Cacher) Fetch() (updated bool, err error) {
req.Header.Set("If-Modified-Since", c.lastMod) req.Header.Set("If-Modified-Since", c.lastMod)
} }
transport := &http.Transport{
DialContext: (&net.Dialer{Timeout: connTimeout}).DialContext,
TLSHandshakeTimeout: connTimeout,
}
var client *http.Client var client *http.Client
if c.Username != "" { if c.Username != "" {
req.SetBasicAuth(c.Username, c.Password) req.SetBasicAuth(c.Username, c.Password)
// Strip auth before following any redirect — presigned URLs (e.g. R2) // Strip auth before following any redirect — presigned URLs (e.g. R2)
// must not receive our credentials. // must not receive our credentials.
client = &http.Client{ client = &http.Client{
Timeout: timeout, Timeout: timeout,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
req.Header.Del("Authorization") req.Header.Del("Authorization")
return nil return nil
}, },
} }
} else { } else {
client = &http.Client{Timeout: timeout} client = &http.Client{Timeout: timeout, Transport: transport}
} }
resp, err := client.Do(req) resp, err := client.Do(req)