diff --git a/internal/proxy/engine.go b/internal/proxy/engine.go index ba63e78..2f5ca30 100644 --- a/internal/proxy/engine.go +++ b/internal/proxy/engine.go @@ -154,7 +154,7 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa } } - resp, err := http.DefaultClient.Do(req) + resp, err := upstreamClient.Do(req) if err != nil { return nil, &UpstreamError{Err: err} } @@ -170,7 +170,7 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa req2.Header.Set("Accept", accept) } } - resp, err = http.DefaultClient.Do(req2) + resp, err = upstreamClient.Do(req2) if err != nil { return nil, &UpstreamError{Err: err} } @@ -302,7 +302,7 @@ func (e *Engine) checkUpstream(ctx context.Context, remote models.Remote, path, } } - resp, err := http.DefaultClient.Do(req) + resp, err := upstreamClient.Do(req) if err != nil { return false, &UpstreamError{Err: err} } @@ -392,7 +392,7 @@ func fetchBearerToken(ctx context.Context, wwwAuth string, remote models.Remote) req.SetBasicAuth(remote.Username, remote.Password) } - resp, err := http.DefaultClient.Do(req) + resp, err := upstreamClient.Do(req) if err != nil { return "", err } diff --git a/internal/proxy/httpclient.go b/internal/proxy/httpclient.go new file mode 100644 index 0000000..0c2433d --- /dev/null +++ b/internal/proxy/httpclient.go @@ -0,0 +1,30 @@ +package proxy + +import ( + "net" + "net/http" + "time" +) + +// upstreamClient is the shared HTTP client for all upstream requests. +// +// It deliberately sets no overall Client.Timeout: the proxy streams +// arbitrarily large artifacts and the total time is bounded by the request +// context instead. Instead it constrains the phases that must never hang — +// connect, TLS handshake, and time-to-first-response-header — so a slow or +// wedged upstream cannot pin a goroutine and connection indefinitely. +var upstreamClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + }, +}