fix: use a timeout-configured HTTP client for upstream requests
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

All upstream GET/HEAD and bearer-token requests used http.DefaultClient,
which has no timeouts, so a slow or wedged upstream could pin a goroutine
and connection indefinitely. Introduce a shared upstreamClient with
dial, TLS-handshake and response-header timeouts (no overall Client
timeout, so large artifact bodies can still stream, bounded by the
request context).

Refs #67
This commit is contained in:
2026-07-02 00:26:36 +10:00
parent 8d9bc1c422
commit 1476120c7b
2 changed files with 34 additions and 4 deletions
+4 -4
View File
@@ -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
}
+30
View File
@@ -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,
},
}