Repository: go-resty/resty (go)

Issue:
Client: Add ability to override any default value from resty http transport
Such as timeout, max idle connections, max idle connection per host, disable keep alives, read & write buffer size, etc.

Pull request:
feat(enhancement): add ability to override default value and provide transport settings on resty client #814
Closes #814

Patch:
diff --git a/client.go b/client.go
index 20292a7..a55fb6b 100644
--- a/client.go
+++ b/client.go
@@ -89,25 +89,58 @@ type (
 	SuccessHook func(*Client, *Response)
 )
 
-// ClientTimeoutSetting struct is used to define custom dialer and transport timeout
-// values for the Resty client initialization. The default dialer and transport
-// timeout values are -
-//
-//	defaultClientTimeout := &ClientTimeoutSetting{
-//		DialerTimeout:                  30 * time.Second,
-//		DialerKeepAlive:                30 * time.Second,
-//		TransportIdleConnTimeout:       90 * time.Second,
-//		TransportTLSHandshakeTimeout:   10 * time.Second,
-//		TransportExpectContinueTimeout: 1 * time.Second,
-//	}
+// TransportSettings struct is used to define custom dialer and transport
+// values for the Resty client. Please refer to individual
+// struct fields to know the default values.
 //
-// Since v3.0.0
-type ClientTimeoutSetting struct {
-	DialerTimeout                  time.Duration
-	DialerKeepAlive                time.Duration
-	TransportIdleConnTimeout       time.Duration
-	TransportTLSHandshakeTimeout   time.Duration
-	TransportExpectContinueTimeout time.Duration
+// Also, refer to https://pkg.go.dev/net/http#Transport for more details.
+type TransportSettings struct {
+	// DialerTimeout, default value is `30` seconds.
+	DialerTimeout time.Duration
+
+	// DialerKeepAlive, default value is `30` seconds.
+	DialerKeepAlive time.Duration
+
+	// IdleConnTimeout, default value is `90` seconds.
+	IdleConnTimeout time.Duration
+
+	// TLSHandshakeTimeout, default value is `10` seconds.
+	TLSHandshakeTimeout time.Duration
+
+	// ExpectContinueTimeout, default value is `1` seconds.
+	ExpectContinueTimeout time.Duration
+
+	// ResponseHeaderTimeout, added to provide ability to
+	// set value. No default value in Resty, the Go
+	// HTTP client default value applies.
+	ResponseHeaderTimeout time.Duration
+
+	// MaxIdleConns, default value is `100`.
+	MaxIdleConns int
+
+	// MaxIdleConnsPerHost, default value is `runtime.GOMAXPROCS(0) + 1`.
+	MaxIdleConnsPerHost int
+
+	// DisableKeepAlives, default value is `false`.
+	DisableKeepAlives bool
+
+	// DisableCompression, default value is `false`.
+	DisableCompression bool
+
+	// MaxResponseHeaderBytes, added to provide ability to
+	// set value. No default value in Resty, the Go
+	// HTTP client default value applies.
+	MaxResponseHeaderBytes int64
+
+	// WriteBufferSize, added to provide ability to
+	// set value. No default value in Resty, the Go
+	// HTTP client default value applies.
+	WriteBufferSize int
+
+	// ReadBufferSize, added to provide ability to
+	// set value. No default value in Resty, the Go
+	// HTTP client default value applies.
+	ReadBufferSize int
 }
 
 // Client struct is used to create Resty client with client level settings,
diff --git a/resty.go b/resty.go
index 7ceaba8..0abbbd0 100644
--- a/resty.go
+++ b/resty.go
@@ -23,33 +23,15 @@ import (
 // Version # of resty
 const Version = "3.0.0-dev"
 
-var (
-	defaultClientTimeout = &ClientTimeoutSetting{
-		DialerTimeout:                  30 * time.Second,
-		DialerKeepAlive:                30 * time.Second,
-		TransportIdleConnTimeout:       90 * time.Second,
-		TransportTLSHandshakeTimeout:   10 * time.Second,
-		TransportExpectContinueTimeout: 1 * time.Second,
-	}
-)
-
 // New method creates a new Resty client.
 func New() *Client {
-	return NewWithTimeout(defaultClientTimeout)
+	return NewWithTransportSettings(nil)
 }
 
-// NewWithTimeout method creates a new Resty client with provided
+// NewWithTransportSettings method creates a new Resty client with provided
 // timeout values.
-//
-// Since v3.0.0
-func NewWithTimeout(timeoutSetting *ClientTimeoutSetting) *Client {
-	return createClient(&http.Client{
-		Jar: createCookieJar(),
-		Transport: createTransport(
-			createDialer(nil, timeoutSetting),
-			timeoutSetting,
-		),
-	})
+func NewWithTransportSettings(transportSettings *TransportSettings) *Client {
+	return NewWithDialerAndTransportSettings(nil, transportSettings)
 }
 
 // NewWithClient method creates a new Resty client with given `http.Client`.
@@ -59,32 +41,25 @@ func NewWithClient(hc *http.Client) *Client {
 
 // NewWithDialer method creates a new Resty client with given Local Address
 // to dial from.
-//
-// Since v3.0.0
 func NewWithDialer(dialer *net.Dialer) *Client {
-	return NewWithDialerAndTimeout(dialer, defaultClientTimeout)
+	return NewWithDialerAndTransportSettings(dialer, nil)
 }
 
-// NewWithDialer method creates a new Resty client with given Local Address
+// NewWithLocalAddr method creates a new Resty client with given Local Address
 // to dial from.
-//
-// Since v3.0.0
-func NewWithDialerAndTimeout(dialer *net.Dialer, timeoutSetting *ClientTimeoutSetting) *Client {
-	return createClient(&http.Client{
-		Jar:       createCookieJar(),
-		Transport: createTransport(dialer, timeoutSetting),
-	})
+func NewWithLocalAddr(localAddr net.Addr) *Client {
+	return NewWithDialerAndTransportSettings(
+		&net.Dialer{LocalAddr: localAddr},
+		nil,
+	)
 }
 
-// NewWithLocalAddr method creates a new Resty client with given Local Address
+// NewWithDialerAndTransportSettings method creates a new Resty client with given Local Address
 // to dial from.
-func NewWithLocalAddr(localAddr net.Addr) *Client {
+func NewWithDialerAndTransportSettings(dialer *net.Dialer, transportSettings *TransportSettings) *Client {
 	return createClient(&http.Client{
-		Jar: createCookieJar(),
-		Transport: createTransport(
-			createDialer(localAddr, defaultClientTimeout),
-			defaultClientTimeout,
-		),
+		Jar:       createCookieJar(),
+		Transport: createTransport(dialer, transportSettings),
 	})
 }
 
@@ -92,28 +67,91 @@ func NewWithLocalAddr(localAddr net.Addr) *Client {
 // Unexported methods
 //_______________________________________________________________________
 
-func createDialer(localAddr net.Addr, timeoutSetting *ClientTimeoutSetting) *net.Dialer {
-	dialer := &net.Dialer{
-		Timeout:   timeoutSetting.DialerTimeout,
-		KeepAlive: timeoutSetting.DialerKeepAlive,
+func createTransport(dialer *net.Dialer, transportSettings *TransportSettings) *http.Transport {
+	if transportSettings == nil {
+		transportSettings = &TransportSettings{}
+	}
+
+	// Dialer
+
+	if dialer == nil {
+		dialer = &net.Dialer{}
 	}
-	if localAddr != nil {
-		dialer.LocalAddr = localAddr
+
+	if transportSettings.DialerTimeout > 0 {
+		dialer.Timeout = transportSettings.DialerTimeout
+	} else {
+		dialer.Timeout = 30 * time.Second
+	}
+
+	if transportSettings.DialerKeepAlive > 0 {
+		dialer.KeepAlive = transportSettings.DialerKeepAlive
+	} else {
+		dialer.KeepAlive = 30 * time.Second
 	}
-	return dialer
-}
 
-func createTransport(dialer *net.Dialer, timeoutSetting *ClientTimeoutSetting) *http.Transport {
-	return &http.Transport{
-		Proxy:                 http.ProxyFromEnvironment,
-		DialContext:           transportDialContext(dialer),
-		ForceAttemptHTTP2:     true,
-		MaxIdleConns:          100,
-		IdleConnTimeout:       timeoutSetting.TransportIdleConnTimeout,
-		TLSHandshakeTimeout:   timeoutSetting.TransportTLSHandshakeTimeout,
-		ExpectContinueTimeout: timeoutSetting.TransportExpectContinueTimeout,
-		MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 1,
+	// Transport
+	t := &http.Transport{
+		Proxy:              http.ProxyFromEnvironment,
+		DialContext:        transportDialContext(dialer),
+		DisableKeepAlives:  transportSettings.DisableKeepAlives,
+		DisableCompression: transportSettings.DisableCompression,
+		ForceAttemptHTTP2:  true,
 	}
+
+	if transportSettings.IdleConnTimeout > 0 {
+		t.IdleConnTimeout = transportSettings.IdleConnTimeout
+	} else {
+		t.IdleConnTimeout = 90 * time.Second
+	}
+
+	if transportSettings.TLSHandshakeTimeout > 0 {
+		t.TLSHandshakeTimeout = transportSettings.TLSHandshakeTimeout
+	} else {
+		t.TLSHandshakeTimeout = 10 * time.Second
+	}
+
+	if transportSettings.ExpectContinueTimeout > 0 {
+		t.ExpectContinueTimeout = transportSettings.ExpectContinueTimeout
+	} else {
+		t.ExpectContinueTimeout = 1 * time.Second
+	}
+
+	if transportSettings.MaxIdleConns > 0 {
+		t.MaxIdleConns = transportSettings.MaxIdleConns
+	} else {
+		t.MaxIdleConns = 100
+	}
+
+	if transportSettings.MaxIdleConnsPerHost > 0 {
+		t.MaxIdleConnsPerHost = transportSettings.MaxIdleConnsPerHost
+	} else {
+		t.MaxIdleConnsPerHost = runtime.GOMAXPROCS(0) + 1
+	}
+
+	//
+	// No default value in Resty for following settings, added to
+	// provide ability to set value otherwise the Go HTTP client
+	// default value applies.
+	//
+
+	if transportSettings.ResponseHeaderTimeout > 0 {
+		t.ResponseHeaderTimeout = transportSettings.ResponseHeaderTimeout
+	}
+
+	if transportSettings.MaxResponseHeaderBytes > 0 {
+		t.MaxResponseHeaderBytes = transportSettings.MaxResponseHeaderBytes
+	}
+
+	if transportSettings.WriteBufferSize > 0 {
+		t.WriteBufferSize = transportSettings.WriteBufferSize
+	}
+
+	if transportSettings.ReadBufferSize > 0 {
+		t.ReadBufferSize = transportSettings.ReadBufferSize
+	}
+
+	return t
 }
 
 func createCookieJar() *cookiejar.Jar {


Test patch:
diff --git a/client_test.go b/client_test.go
index faa2710..90789e0 100644
--- a/client_test.go
+++ b/client_test.go
@@ -791,18 +791,24 @@ func TestDebugLogSimultaneously(t *testing.T) {
 	}
 }
 
-func TestNewWithTimeout(t *testing.T) {
+func TestCustomTransportSettings(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
 
-	customTimeout := &ClientTimeoutSetting{
-		DialerTimeout:                  30 * time.Second,
-		DialerKeepAlive:                15 * time.Second,
-		TransportIdleConnTimeout:       120 * time.Second,
-		TransportTLSHandshakeTimeout:   20 * time.Second,
-		TransportExpectContinueTimeout: 1 * time.Second,
+	customTransportSettings := &TransportSettings{
+		DialerTimeout:          30 * time.Second,
+		DialerKeepAlive:        15 * time.Second,
+		IdleConnTimeout:        120 * time.Second,
+		TLSHandshakeTimeout:    20 * time.Second,
+		ExpectContinueTimeout:  1 * time.Second,
+		MaxIdleConns:           50,
+		MaxIdleConnsPerHost:    3,
+		ResponseHeaderTimeout:  10 * time.Second,
+		MaxResponseHeaderBytes: 1 << 10,
+		WriteBufferSize:        2 << 10,
+		ReadBufferSize:         2 << 10,
 	}
-	client := NewWithTimeout(customTimeout)
+	client := NewWithTransportSettings(customTransportSettings)
 	client.SetBaseURL(ts.URL)
 
 	resp, err := client.R().Get("/")
@@ -810,6 +816,29 @@ func TestNewWithTimeout(t *testing.T) {
 	assertEqual(t, resp.String(), "TestGet: text response")
 }
 
+func TestDefaultDialerTransportSettings(t *testing.T) {
+	ts := createGetServer(t)
+	defer ts.Close()
+
+	t.Run("transport-default", func(t *testing.T) {
+		client := NewWithTransportSettings(nil)
+		client.SetBaseURL(ts.URL)
+
+		resp, err := client.R().Get("/")
+		assertNil(t, err)
+		assertEqual(t, resp.String(), "TestGet: text response")
+	})
+
+	t.Run("dialer-transport-default", func(t *testing.T) {
+		client := NewWithDialerAndTransportSettings(nil, nil)
+		client.SetBaseURL(ts.URL)
+
+		resp, err := client.R().Get("/")
+		assertNil(t, err)
+		assertEqual(t, resp.String(), "TestGet: text response")
+	})
+}
+
 func TestNewWithDialer(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()