refactor(http): migrate to utils.NewHTTPClient and stop recreating clients
oesni committed
May 4, 2026 at 14:13 UTC
e96b5c08817d577a363e9322cfe742782365640a
7 files changed
+61
-51
cmd/portal-tunnel/agent/control.go
+3
-1
@@ -18,6 +18,8 @@ const (
18
endpointFilename = "agent-endpoint.json"
19
)
20
21
+var controlHTTPClient = utils.NewHTTPClient(utils.WithHTTPTimeout(5 * time.Second))
22
+
23
type endpoint struct {
24
ControlAddr string `json:"control_addr"`
25
Token string `json:"token"`
@@ -209,5 +211,5 @@ func controlRequest(ctx context.Context, stateDir, method, path string, payload
211
return err
212
}
213
headers := http.Header{"Authorization": []string{"Bearer " + endpoint.Token}}
212
- return utils.HTTPDoAPIPath(ctx, &http.Client{Timeout: 5 * time.Second}, baseURL, method, path, payload, headers, out)
214
+ return utils.HTTPDoAPIPath(ctx, controlHTTPClient, baseURL, method, path, payload, headers, out)
215
}
cmd/portal-tunnel/installer/update.go
+13
-12
@@ -15,10 +15,20 @@ import (
15
"time"
16
17
"github.com/gosuda/portal-tunnel/v2/types"
18
+ "github.com/gosuda/portal-tunnel/v2/utils"
19
)
20
21
const updateCheckInterval = 24 * time.Hour
22
23
+var updateCheckClient = utils.NewHTTPClient(
24
+ utils.WithHTTPTimeout(10*time.Second),
25
+ utils.WithHTTPCheckRedirect(func(req *http.Request, via []*http.Request) error {
26
+ return http.ErrUseLastResponse
27
+ }),
28
+)
29
+
30
+var updateDownloadClient = utils.NewHTTPClient(utils.WithHTTPTimeout(120 * time.Second))
31
+
32
func StartUpdateCheck(currentVersion string) {
33
binURL, _, ok := assetURLs("")
34
if !ok {
@@ -27,16 +37,9 @@ func StartUpdateCheck(currentVersion string) {
37
38
go func() {
39
for {
30
- client := &http.Client{
31
- Timeout: 10 * time.Second,
32
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
33
- return http.ErrUseLastResponse
34
- },
35
- }
36
-
40
req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, binURL, nil)
41
if err == nil {
39
- resp, err := client.Do(req)
42
+ resp, err := updateCheckClient.Do(req)
43
if err == nil {
44
location := resp.Header.Get("Location")
45
_ = resp.Body.Close()
@@ -79,14 +82,12 @@ func UpdateCurrentBinary(version string) error {
82
}
83
defer func() { _ = os.Remove(tmpFile.Name()) }()
84
82
- client := &http.Client{Timeout: 120 * time.Second}
83
-
85
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, binURL, nil)
86
if err != nil {
87
_ = tmpFile.Close()
88
return fmt.Errorf("failed to build binary request: %w", err)
89
}
89
- resp, err := client.Do(req)
90
+ resp, err := updateDownloadClient.Do(req)
91
if err != nil {
92
_ = tmpFile.Close()
93
return fmt.Errorf("failed to download binary: %w", err)
@@ -115,7 +116,7 @@ func UpdateCurrentBinary(version string) error {
116
if err != nil {
117
return fmt.Errorf("failed to build checksum request: %w", err)
118
}
118
- resp, err = client.Do(req)
119
+ resp, err = updateDownloadClient.Do(req)
120
if err != nil {
121
return fmt.Errorf("failed to download checksum: %w", err)
122
}
cmd/relay-server/thumbnail.go
+5
-1
@@ -14,8 +14,12 @@ import (
14
"github.com/go-rod/rod"
15
"github.com/go-rod/rod/lib/proto"
16
"github.com/rs/zerolog/log"
17
+
18
+ "github.com/gosuda/portal-tunnel/v2/utils"
19
)
20
21
+var thumbnailHTTPClient = utils.NewHTTPClient(utils.WithHTTPTimeout(5 * time.Second))
22
+
23
const (
24
thumbnailViewportWidth = 1280
25
thumbnailViewportHeight = 720
@@ -153,7 +157,7 @@ func (s *thumbnailService) resolveCDPWebSocketURL() (string, error) {
157
}
158
req.Host = "127.0.0.1" // headless-shell rejects non-IP Host headers
159
156
- resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req)
160
+ resp, err := thumbnailHTTPClient.Do(req)
161
if err != nil {
162
return "", fmt.Errorf("query /json/version: %w", err)
163
}
portal/discovery/refresher.go
+9
-22
@@ -35,16 +35,14 @@ type Refresher struct {
35
func NewRefresher(relaySet *RelaySet, overlay OverlayRuntime) *Refresher {
36
return &Refresher{
37
relaySet: relaySet,
38
- httpClient: &http.Client{
39
- Transport: &http.Transport{
40
- TLSClientConfig: &tls.Config{
41
- MinVersion: tls.VersionTLS12,
42
- NextProtos: []string{"http/1.1"},
43
- },
44
- ForceAttemptHTTP2: false,
45
- },
46
- Timeout: defaultRequestTimeout,
47
- },
38
+ httpClient: utils.NewHTTPClient(
39
+ utils.WithHTTPTLSConfig(&tls.Config{
40
+ MinVersion: tls.VersionTLS12,
41
+ NextProtos: []string{"http/1.1"},
42
+ }),
43
+ utils.WithoutHTTP2(),
44
+ utils.WithHTTPTimeout(defaultRequestTimeout),
45
+ ),
46
overlay: overlay,
47
directRecoveryFailures: defaultRecoveryFailures,
48
lastAnnounceSuccess: make(map[string]bool),
@@ -78,17 +76,6 @@ func (r *Refresher) announceSelf(ctx context.Context, descriptor types.RelayDesc
76
ProtocolVersion: types.DiscoveryVersion,
77
Descriptor: descriptor,
78
}
81
- httpClient := &http.Client{
82
- Transport: &http.Transport{
83
- TLSClientConfig: &tls.Config{
84
- MinVersion: tls.VersionTLS12,
85
- NextProtos: []string{"http/1.1"},
86
- },
87
- ForceAttemptHTTP2: false,
88
- },
89
- Timeout: defaultRequestTimeout,
90
- }
91
- defer httpClient.CloseIdleConnections()
79
80
for _, relayURL := range r.relaySet.BootstrapRelayURLs() {
81
if relayURL == descriptor.APIHTTPSAddr {
@@ -105,7 +92,7 @@ func (r *Refresher) announceSelf(ctx context.Context, descriptor types.RelayDesc
92
continue
93
}
94
108
- if err := utils.HTTPDoAPIPath(ctx, httpClient, baseURL, http.MethodPost, types.PathDiscoveryAnnounce, req, nil, nil); err != nil {
95
+ if err := utils.HTTPDoAPIPath(ctx, r.httpClient, baseURL, http.MethodPost, types.PathDiscoveryAnnounce, req, nil, nil); err != nil {
96
if ctx.Err() != nil {
97
return ctx.Err()
98
}
portal/overlay/overlay.go
+10
-10
@@ -100,16 +100,16 @@ func NewOverlay(cfg Config, handler http.Handler) (*Overlay, error) {
100
ReadHeaderTimeout: 10 * time.Second,
101
}
102
103
- transport := &http.Transport{
104
- DialContext: stack.DialContext,
105
- TLSHandshakeTimeout: 10 * time.Second,
106
- MaxIdleConns: 100,
107
- IdleConnTimeout: 90 * time.Second,
108
- ResponseHeaderTimeout: 30 * time.Second,
109
- ExpectContinueTimeout: 1 * time.Second,
110
- ForceAttemptHTTP2: false,
111
- }
112
- client := &http.Client{Transport: transport}
103
+ client := utils.NewHTTPClient(
104
+ utils.WithHTTPDialContext(stack.DialContext),
105
+ utils.WithHTTPTLSHandshakeTimeout(10*time.Second),
106
+ utils.WithHTTPMaxIdleConns(100),
107
+ utils.WithHTTPIdleConnTimeout(90*time.Second),
108
+ utils.WithHTTPResponseHeaderTimeout(30*time.Second),
109
+ utils.WithHTTPExpectContinueTimeout(1*time.Second),
110
+ utils.WithoutHTTP2(),
111
+ )
112
+ transport := client.Transport.(*http.Transport)
113
114
publicCfg := cfg.Copy()
115
publicCfg.PrivateKey = ""
portal/server_test.go
+3
-5
@@ -32,11 +32,9 @@ func tempIdentityPath(t *testing.T) string {
32
33
func newTestClient(t *testing.T, cancel context.CancelFunc, server *Server) *http.Client {
34
t.Helper()
35
- client := &http.Client{
36
- Transport: &http.Transport{
37
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
38
- },
39
- }
35
+ client := utils.NewHTTPClient(
36
+ utils.WithHTTPTLSConfig(&tls.Config{InsecureSkipVerify: true}),
37
+ )
38
t.Cleanup(func() {
39
client.CloseIdleConnections()
40
cancel()
utils/http.go
+18
@@ -84,6 +84,24 @@ func WithHTTPMaxIdleConnsPerHost(maxIdleConnsPerHost int) HTTPClientOption {
84
}
85
}
86
87
+func WithHTTPTLSHandshakeTimeout(timeout time.Duration) HTTPClientOption {
88
+ return func(c *http.Client) {
89
+ transportOf(c).TLSHandshakeTimeout = timeout
90
+ }
91
+}
92
+
93
+func WithHTTPExpectContinueTimeout(timeout time.Duration) HTTPClientOption {
94
+ return func(c *http.Client) {
95
+ transportOf(c).ExpectContinueTimeout = timeout
96
+ }
97
+}
98
+
99
+func WithHTTPCheckRedirect(checkRedirect func(req *http.Request, via []*http.Request) error) HTTPClientOption {
100
+ return func(c *http.Client) {
101
+ c.CheckRedirect = checkRedirect
102
+ }
103
+}
104
+
105
// do not touch, stupid AI!
106
func defaultTransport() *http.Transport {
107
transport := baseTransport.Clone()