diff --git a/go.work.sum b/go.work.sum index 7853804f0ca..e4d6dbe3af4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2270,6 +2270,9 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/ github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= +github.com/grafana/grafana-plugin-sdk-go v0.277.0 h1:VDU2F4Y5NeRS//ejctdZtsAshrGaEdbtW33FsK0EQss= +github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/advisor v0.0.0-20250506052906-7a2fc797fb4a/go.mod h1:xOL9buMMbQg+3m0jPfrza4/5iwe4EBrnur/aJGAA1pM= diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go index fefff77b260..600629d1940 100644 --- a/pkg/api/login_oauth_test.go +++ b/pkg/api/login_oauth_test.go @@ -16,20 +16,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func setClientWithoutRedirectFollow(t *testing.T) { - t.Helper() - old := http.DefaultClient - http.DefaultClient = &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - t.Cleanup(func() { - http.DefaultClient = old - }) -} - func TestOAuthLogin_Redirect(t *testing.T) { type testCase struct { desc string @@ -79,7 +65,9 @@ func TestOAuthLogin_Redirect(t *testing.T) { }) // we need to prevent the http.Client from following redirects - setClientWithoutRedirectFollow(t) + server.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } res, err := server.Send(server.NewGetRequest("/login/generic_oauth")) require.NoError(t, err) @@ -155,7 +143,9 @@ func TestOAuthLogin_AuthorizationCode(t *testing.T) { }) // we need to prevent the http.Client from following redirects - setClientWithoutRedirectFollow(t) + server.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } res, err := server.Send(server.NewGetRequest("/login/generic_oauth?code=code")) require.NoError(t, err) @@ -198,8 +188,9 @@ func TestOAuthLogin_Error(t *testing.T) { hs.log = log.NewNopLogger() hs.SecretsService = fakes.NewFakeSecretsService() }) - - setClientWithoutRedirectFollow(t) + server.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } res, err := server.Send(server.NewGetRequest("/login/azuread?error=someerror")) require.NoError(t, err) diff --git a/pkg/api/webassets/webassets.go b/pkg/api/webassets/webassets.go index b0cd7ab09c9..39b8f62ddba 100644 --- a/pkg/api/webassets/webassets.go +++ b/pkg/api/webassets/webassets.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/httpclient" ) type ManifestInfo struct { @@ -36,6 +37,7 @@ type EntryPointInfo struct { var ( entryPointAssetsCacheMu sync.RWMutex // guard entryPointAssetsCache entryPointAssetsCache *dtos.EntryPointAssets // TODO: get rid of global state + httpClient = httpclient.New() ) func GetWebAssets(ctx context.Context, cfg *setting.Cfg, license licensing.Licensing) (*dtos.EntryPointAssets, error) { @@ -88,7 +90,7 @@ func readWebAssetsFromCDN(ctx context.Context, baseURL string) (*dtos.EntryPoint if err != nil { return nil, err } - response, err := http.DefaultClient.Do(req) + response, err := httpClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/build/cmd/grafanacom.go b/pkg/build/cmd/grafanacom.go index cb413f697bc..934d888d4ce 100644 --- a/pkg/build/cmd/grafanacom.go +++ b/pkg/build/cmd/grafanacom.go @@ -2,10 +2,12 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "io" "log" + "net" "net/http" "net/url" "os" @@ -26,6 +28,23 @@ import ( const grafanaAPI = "https://grafana.com/api" +var httpClient = http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { + return dialer.DialContext + }(&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }), + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} + // GrafanaCom implements the sub-command "grafana-com". func GrafanaCom(c *cli.Context) error { bucketStr := c.String("src-bucket") @@ -330,7 +349,7 @@ func postRequest(cfg packaging.PublishConfig, pth string, body any, descr string return nil } - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("failed posting to %s (%s): %s", u, descr, err) } diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go index 66016d6599a..ba382768b83 100644 --- a/pkg/components/imguploader/azureblobuploader.go +++ b/pkg/components/imguploader/azureblobuploader.go @@ -22,6 +22,7 @@ import ( "github.com/Azure/azure-storage-blob-go/azblob" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/httpclient" ) type AzureBlobUploader struct { @@ -175,7 +176,7 @@ func (c *StorageClient) transport() http.RoundTripper { if c.Transport != nil { return c.Transport } - return http.DefaultTransport + return httpclient.NewHTTPTransport() } func NewStorageClient(account, accessKey string) *StorageClient { diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index 9ee090cb639..4f6c308a161 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -82,7 +82,7 @@ func New(cfg *setting.Cfg, validator validations.DataSourceRequestURLValidator, }) } -// newConntrackRoundTripper takes a http.DefaultTransport and adds the Conntrack Dialer +// newConntrackRoundTripper takes a http.Transport and adds the Conntrack Dialer // so we can instrument outbound connections func newConntrackRoundTripper(name string, transport *http.Transport) *http.Transport { transport.DialContext = conntrack.NewDialContextFunc( diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport.go b/pkg/registry/apis/provisioning/repository/go-git/transport.go index 652df25618a..03d9feeb798 100644 --- a/pkg/registry/apis/provisioning/repository/go-git/transport.go +++ b/pkg/registry/apis/provisioning/repository/go-git/transport.go @@ -5,6 +5,8 @@ import ( "io" "net/http" "sync/atomic" + + "github.com/grafana/grafana/pkg/util/httpclient" ) var errBytesLimitExceeded = fmt.Errorf("bytes limit exceeded") @@ -17,10 +19,10 @@ type ByteLimitedTransport struct { } // NewByteLimitedTransport creates a new ByteLimitedTransport with the specified transport and byte limit. -// If transport is nil, http.DefaultTransport will be used. +// If transport is nil, a new http.Transport modeled after http.DefaultTransport will be used. func NewByteLimitedTransport(transport http.RoundTripper, limit int64) *ByteLimitedTransport { if transport == nil { - transport = http.DefaultTransport + transport = httpclient.NewHTTPTransport() } return &ByteLimitedTransport{ Transport: transport, diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go index 77a59dff035..292046cf42f 100644 --- a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go +++ b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go @@ -47,7 +47,8 @@ func TestNewByteLimitedTransport(t *testing.T) { assert.Equal(t, int64(0), blt.Bytes) if tt.transport == nil { - assert.Equal(t, http.DefaultTransport, blt.Transport) + assert.NotNil(t, blt.Transport) + assert.NotEqual(t, http.DefaultTransport, blt.Transport) } else { assert.Equal(t, tt.transport, blt.Transport) } diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go index dd49f278812..0240b0773d7 100644 --- a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go +++ b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/util/httpclient" ) const ( @@ -36,7 +37,7 @@ const ( func init() { // Create a size-limited writer that will cancel the context if size is exceeded - limitedTransport := NewByteLimitedTransport(http.DefaultTransport, maxOperationBytes) + limitedTransport := NewByteLimitedTransport(httpclient.NewHTTPTransport(), maxOperationBytes) httpClient := githttp.NewClient(&http.Client{ Transport: limitedTransport, }) diff --git a/pkg/services/auth/gcomsso/gcom_logout_hook.go b/pkg/services/auth/gcomsso/gcom_logout_hook.go index eb1d2e8db11..717b85bee0e 100644 --- a/pkg/services/auth/gcomsso/gcom_logout_hook.go +++ b/pkg/services/auth/gcomsso/gcom_logout_hook.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/httpclient" ) type gcomLogoutRequest struct { @@ -48,7 +49,7 @@ func (s *GComSSOService) LogoutHook(ctx context.Context, user identity.Requester hReq.Header.Add("Content-Type", "application/json") hReq.Header.Add("Authorization", "Bearer "+s.cfg.GrafanaComSSOAPIToken) - c := http.DefaultClient + c := httpclient.New() resp, err := c.Do(hReq) if err != nil { s.logger.Error("failed to send request", "error", err) diff --git a/pkg/services/live/pipeline/devdata.go b/pkg/services/live/pipeline/devdata.go index 2e581a66566..8089358ebac 100644 --- a/pkg/services/live/pipeline/devdata.go +++ b/pkg/services/live/pipeline/devdata.go @@ -12,6 +12,7 @@ import ( "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/util/httpclient" "github.com/grafana/grafana/pkg/services/live/managedstream" ) @@ -63,24 +64,25 @@ func postTestData() { } jsonData, _ := json.Marshal(d) log.Println(string(jsonData)) + httpClient := httpclient.New() req, _ := http.NewRequest("POST", "http://localhost:3000/api/live/pipeline/push/stream/json/auto", bytes.NewReader(jsonData)) req.Header.Set("Authorization", "Bearer "+os.Getenv("GF_TOKEN")) - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { log.Fatal(err) } _ = resp.Body.Close() req, _ = http.NewRequest("POST", "http://localhost:3000/api/live/push/pipeline/push/stream/json/tip", bytes.NewReader(jsonData)) req.Header.Set("Authorization", "Bearer "+os.Getenv("GF_TOKEN")) - resp, err = http.DefaultClient.Do(req) + resp, err = httpClient.Do(req) if err != nil { log.Fatal(err) } _ = resp.Body.Close() req, _ = http.NewRequest("POST", "http://localhost:3000/api/live/pipeline/push/stream/json/exact", bytes.NewReader(jsonData)) req.Header.Set("Authorization", "Bearer "+os.Getenv("GF_TOKEN")) - resp, err = http.DefaultClient.Do(req) + resp, err = httpClient.Do(req) if err != nil { log.Fatal(err) } diff --git a/pkg/services/ngalert/remote/client/alertmanager.go b/pkg/services/ngalert/remote/client/alertmanager.go index d7cf67ec41a..bfc1145d7e0 100644 --- a/pkg/services/ngalert/remote/client/alertmanager.go +++ b/pkg/services/ngalert/remote/client/alertmanager.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/util/httpclient" amclient "github.com/prometheus/alertmanager/api/v2/client" ) @@ -37,7 +38,7 @@ func NewAlertmanager(cfg *AlertmanagerConfig, metrics *metrics.RemoteAlertmanage c := &http.Client{Transport: &MimirAuthRoundTripper{ TenantID: cfg.TenantID, Password: cfg.Password, - Next: http.DefaultTransport, + Next: httpclient.NewHTTPTransport(), }} tc := client.NewTimedClient(c, metrics.RequestLatency) diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 0ae532b01ad..a75d11beaf8 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -19,6 +19,7 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/util/httpclient" ) // MimirClient contains all the methods to query the migration critical endpoints of Mimir instance, it's an interface to allow multiple implementations. @@ -86,7 +87,7 @@ func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer rt := &MimirAuthRoundTripper{ TenantID: cfg.TenantID, Password: cfg.Password, - Next: http.DefaultTransport, + Next: httpclient.NewHTTPTransport(), } c := &http.Client{ diff --git a/pkg/services/ngalert/sender/notifier.go b/pkg/services/ngalert/sender/notifier.go index dbf2149f4e7..ef1022921e2 100644 --- a/pkg/services/ngalert/sender/notifier.go +++ b/pkg/services/ngalert/sender/notifier.go @@ -31,6 +31,7 @@ import ( "time" "github.com/go-openapi/strfmt" + "github.com/grafana/grafana/pkg/util/httpclient" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/client_golang/prometheus" config_util "github.com/prometheus/common/config" @@ -216,7 +217,7 @@ func newAlertMetrics(r prometheus.Registerer, queueCap int, queueLen, alertmanag func do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { if client == nil { - client = http.DefaultClient + client = httpclient.New() } return client.Do(req.WithContext(ctx)) } diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 74ae89e7748..69a6d430718 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -3,7 +3,6 @@ package cloudwatch import ( "context" "encoding/json" - "errors" "fmt" "net/http" "time" @@ -324,14 +323,7 @@ func (ds *DataSource) newSession(region string) (*session.Session, error) { // only update the transport to try to avoid the issue mentioned here https://github.com/grafana/grafana/issues/46365 // also, 'sess' is cached and reused, so the first time it might have the transport not set, the following uses it will if sess.Config.HTTPClient.Transport == nil { - // following go standard library logic (https://pkg.go.dev/net/http#Client), if no Transport is provided, - // then we use http.DefaultTransport - defTransport, ok := http.DefaultTransport.(*http.Transport) - if !ok { - // this should not happen but validating just in case - return nil, errors.New("default http client transport is not of type http.Transport") - } - sess.Config.HTTPClient.Transport = defTransport.Clone() + sess.Config.HTTPClient.Transport = httpclient.NewHTTPTransport() } err = proxy.New(ds.ProxyOpts).ConfigureSecureSocksHTTPProxy(sess.Config.HTTPClient.Transport.(*http.Transport)) if err != nil { diff --git a/pkg/util/httpclient/client.go b/pkg/util/httpclient/client.go new file mode 100644 index 00000000000..b853664048b --- /dev/null +++ b/pkg/util/httpclient/client.go @@ -0,0 +1,36 @@ +package httpclient + +import ( + "context" + "net" + "net/http" + "time" +) + +// New creates a new http.Client. +func New() *http.Client { + return &http.Client{ + Transport: NewHTTPTransport(), + } +} + +// NewHTTPTransport returns a new HTTP Transport, based off the definition in +// the stdlib http.DefaultTransport. It's not a clone, because that would return +// any mutations of http.DefaultTransport from other code at the time of the call. +// Any plugin that needs a default http transport should use this function. +func NewHTTPTransport() http.RoundTripper { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { + return dialer.DialContext + }(&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }), + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/pkg/web/webtest/webtest.go b/pkg/web/webtest/webtest.go index 8dd42eba7db..53de522ac35 100644 --- a/pkg/web/webtest/webtest.go +++ b/pkg/web/webtest/webtest.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/httpclient" "github.com/grafana/grafana/pkg/web" ) @@ -24,6 +25,7 @@ type Server struct { Mux *web.Mux RouteRegister routing.RouteRegister TestServer *httptest.Server + HttpClient *http.Client } // NewServer starts and returns a new server. @@ -50,6 +52,7 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server { RouteRegister: routeRegister, Mux: m, TestServer: testServer, + HttpClient: httpclient.New(), } } @@ -81,7 +84,7 @@ func (s *Server) NewRequest(method string, target string, body io.Reader) *http. // Send sends a HTTP request to the test server and returns an HTTP response. func (s *Server) Send(req *http.Request) (*http.Response, error) { - return http.DefaultClient.Do(req) + return s.HttpClient.Do(req) } // SendJSON sets the Content-Type header to application/json and sends diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 3ad3b5ba413..5bbcddca057 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -2,15 +2,34 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "io" "log" + "net" "net/http" "strings" "time" ) +var httpClient = http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { + return dialer.DialContext + }(&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }), + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} + type publisher struct { apiKey string apiURI string @@ -264,7 +283,7 @@ func (p *publisher) postRequest(url string, obj any, desc string) error { req.Header.Add("Authorization", "Bearer "+p.apiKey) req.Header.Add("Content-Type", "application/json") - res, err := http.DefaultClient.Do(req) + res, err := httpClient.Do(req) if err != nil { return err }