diff --git a/pkg/platformclient/app.go b/pkg/platformclient/app.go index b8fb56581..a4a81427e 100644 --- a/pkg/platformclient/app.go +++ b/pkg/platformclient/app.go @@ -52,7 +52,7 @@ func (c *HTTPClient) DeleteApp(id string) error { return err } req.Header.Add("Authorization", c.apiKey) - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("DeleteApp (%s %s): %w", req.Method, endpoint, err) } diff --git a/pkg/platformclient/channel.go b/pkg/platformclient/channel.go index 81ecdb847..3338dc142 100644 --- a/pkg/platformclient/channel.go +++ b/pkg/platformclient/channel.go @@ -60,7 +60,7 @@ func (c *HTTPClient) ArchiveChannel(appID, channelID string) error { return err } req.Header.Add("Authorization", c.apiKey) - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("ArchiveChannel (%s %s): %w", req.Method, endpoint, err) } diff --git a/pkg/platformclient/client.go b/pkg/platformclient/client.go index 55b32dae0..db3f5c0cf 100644 --- a/pkg/platformclient/client.go +++ b/pkg/platformclient/client.go @@ -7,8 +7,11 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "os" + "strings" + "time" "github.com/pkg/errors" "github.com/replicatedhq/replicated/pkg/version" @@ -20,6 +23,44 @@ var ( ErrForbidden = errors.New("the action is not allowed for the current user or team") ) +// httpClient is a custom HTTP client that handles .localhost domains properly. +// Go's default DNS resolver doesn't handle .localhost domains like browsers do +// (per RFC 6761), so we need custom logic to resolve them to 127.0.0.1. +// This is a singleton that's reused for all requests to avoid leaking connections. +var httpClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // Check if the address is a .localhost domain + host, port, err := net.SplitHostPort(addr) + if err != nil { + // If there's no port, just use the address as host + host = addr + port = "" + } + + // If the host ends with .localhost, replace it with 127.0.0.1 + if strings.HasSuffix(host, ".localhost") { + if port != "" { + addr = net.JoinHostPort("127.0.0.1", port) + } else { + addr = "127.0.0.1" + } + } + + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + return dialer.DialContext(ctx, network, addr) + }, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} + type APIError struct { Method string Endpoint string @@ -83,7 +124,7 @@ func (c *HTTPClient) DoJSONWithoutUnmarshal(method string, path string, reqBody req.Header.Set("Authorization", c.apiKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return nil, err } @@ -133,7 +174,7 @@ func (c *HTTPClient) DoJSON(ctx context.Context, method string, path string, suc return errors.Wrap(err, "add github actions headers") } - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return err } @@ -222,7 +263,7 @@ func (c *HTTPClient) HTTPGet(path string, successStatus int) ([]byte, error) { } req.Header.Set("Authorization", c.apiKey) - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/platformclient/release.go b/pkg/platformclient/release.go index 35e25734f..4e8655f8e 100644 --- a/pkg/platformclient/release.go +++ b/pkg/platformclient/release.go @@ -49,7 +49,7 @@ func (c *HTTPClient) UpdateRelease(appID string, sequence int64, yaml string) er } req.Header.Set("Authorization", c.apiKey) req.Header.Set("Content-Type", "application/yaml") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("UpdateRelease: %w", err) }