diff --git a/pkg/tools/mcp/main_test.go b/pkg/tools/mcp/main_test.go index 034d3e4ae..b72a70339 100644 --- a/pkg/tools/mcp/main_test.go +++ b/pkg/tools/mcp/main_test.go @@ -3,15 +3,15 @@ package mcp import ( "os" "testing" - "time" - - "github.com/docker/docker-agent/pkg/httpclient" ) // TestMain swaps the OAuth helpers' SSRF-safe HTTP client for the // loopback-allowing variant so tests can hit httptest.NewServer (which -// binds to 127.0.0.1). Production code keeps the safe client. +// binds to 127.0.0.1). Production code keeps the safe client. The variant +// carries its own connection pool, so parallel tests closing httptest +// servers (which prunes http.DefaultTransport's pool) can't break its +// in-flight requests. func TestMain(m *testing.M) { - oauthHTTPClient = httpclient.NewSafeClient(30*time.Second, true) + oauthHTTPClient = oauthHTTPClientForAllowPrivateIPs(true) os.Exit(m.Run()) } diff --git a/pkg/tools/mcp/oauth_helpers.go b/pkg/tools/mcp/oauth_helpers.go index 31fb0f213..90cd8d832 100644 --- a/pkg/tools/mcp/oauth_helpers.go +++ b/pkg/tools/mcp/oauth_helpers.go @@ -37,7 +37,14 @@ var oauthHTTPClient = httpclient.NewSafeClient(30*time.Second, false) func oauthHTTPClientForAllowPrivateIPs(allowPrivateIPs bool) *http.Client { if allowPrivateIPs { - return &http.Client{Timeout: 30 * time.Second} + // Clone keeps the default proxy/HTTP2/timeout behavior but gives the + // client its own connection pool: a nil Transport would share + // http.DefaultTransport's pool, which third parties may prune via + // CloseIdleConnections (httptest.Server.Close does). + return &http.Client{ + Timeout: 30 * time.Second, + Transport: http.DefaultTransport.(*http.Transport).Clone(), + } } return oauthHTTPClient } diff --git a/pkg/tools/mcp/oauth_server_test.go b/pkg/tools/mcp/oauth_server_test.go index 8f662d9f5..0b7427fd1 100644 --- a/pkg/tools/mcp/oauth_server_test.go +++ b/pkg/tools/mcp/oauth_server_test.go @@ -99,7 +99,7 @@ func TestCallbackServer_DuplicateCallbacksDoNotBlock(t *testing.T) { cs.SetExpectedState("expected-state") callbackURL := cs.GetRedirectURI() + "?code=authcode&state=expected-state" - client := &http.Client{Timeout: 2 * time.Second} + client := &http.Client{Timeout: 2 * time.Second, Transport: newTestTransport(t)} // Fire several callbacks back-to-back. Each one must complete (so the // handler goroutine isn't stuck) regardless of whether anyone is diff --git a/pkg/tools/mcp/oauth_test.go b/pkg/tools/mcp/oauth_test.go index 5f42056ef..5af3e00e2 100644 --- a/pkg/tools/mcp/oauth_test.go +++ b/pkg/tools/mcp/oauth_test.go @@ -28,7 +28,7 @@ import ( // test's in-flight request ("transport connection broken"). func newTestTransport(t *testing.T) *http.Transport { t.Helper() - tr := &http.Transport{} + tr := http.DefaultTransport.(*http.Transport).Clone() t.Cleanup(tr.CloseIdleConnections) return tr }