Skip to content

Commit

Permalink
add ability to provide custom dialer and transport timeout, add NewWi…
Browse files Browse the repository at this point in the history
…thTimeout and NewWithDialer method, set go min version to 1.18, few cleanup #795
  • Loading branch information
jeevatkm committed May 11, 2024
1 parent 8181ea3 commit a5d485c
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 149 deletions.
2 changes: 0 additions & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ go_library(
"trace.go",
"transport_js.go",
"transport_other.go",
"transport.go",
"transport112.go",
"util.go",
],
importpath = "github.com/go-resty/resty/v2",
Expand Down
104 changes: 34 additions & 70 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@ import (
"compress/gzip"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -92,6 +89,27 @@ 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,
// }
//
// Since v3.0.0
type ClientTimeoutSetting struct {
DialerTimeout time.Duration
DialerKeepAlive time.Duration
TransportIdleConnTimeout time.Duration
TransportTLSHandshakeTimeout time.Duration
TransportExpectContinueTimeout time.Duration
}

// Client struct is used to create Resty client with client level settings,
// these settings are applicable to all the request raised from the client.
//
Expand Down Expand Up @@ -1121,10 +1139,23 @@ func (c *Client) IsProxySet() bool {
}

// GetClient method returns the current `http.Client` used by the resty client.
//
// Since v1.1.0
func (c *Client) GetClient() *http.Client {
return c.httpClient
}

// Transport method returns `*http.Transport` currently in use or error
// in case currently used `transport` is not a `*http.Transport`.
//
// Since v2.8.0 become exported method.
func (c *Client) Transport() (*http.Transport, error) {
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
return transport, nil
}
return nil, errors.New("current transport is not an *http.Transport instance")
}

// Clone returns a clone of the original client.
//
// Be careful when using this function:
Expand Down Expand Up @@ -1263,17 +1294,6 @@ func (c *Client) tlsConfig() (*tls.Config, error) {
return transport.TLSClientConfig, nil
}

// Transport method returns `*http.Transport` currently in use or error
// in case currently used `transport` is not a `*http.Transport`.
//
// Since v2.8.0 become exported method.
func (c *Client) Transport() (*http.Transport, error) {
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
return transport, nil
}
return nil, errors.New("current transport is not an *http.Transport instance")
}

// just an internal helper method
func (c *Client) outputLogTo(w io.Writer) *Client {
c.log.(*logger).l.SetOutput(w)
Expand Down Expand Up @@ -1354,59 +1374,3 @@ type MultipartField struct {
ContentType string
io.Reader
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Unexported package methods
//_______________________________________________________________________

func createClient(hc *http.Client) *Client {
if hc.Transport == nil {
hc.Transport = createTransport(nil)
}

c := &Client{ // not setting lang default values
QueryParam: url.Values{},
FormData: url.Values{},
Header: http.Header{},
Cookies: make([]*http.Cookie, 0),
RetryWaitTime: defaultWaitTime,
RetryMaxWaitTime: defaultMaxWaitTime,
PathParams: make(map[string]string),
RawPathParams: make(map[string]string),
JSONMarshal: json.Marshal,
JSONUnmarshal: json.Unmarshal,
XMLMarshal: xml.Marshal,
XMLUnmarshal: xml.Unmarshal,
HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"),

jsonEscapeHTML: true,
httpClient: hc,
debugBodySizeLimit: math.MaxInt32,
udBeforeRequestLock: &sync.RWMutex{},
afterResponseLock: &sync.RWMutex{},
}

// Logger
c.SetLogger(createLogger())

// default before request middlewares
c.beforeRequest = []RequestMiddleware{
parseRequestURL,
parseRequestHeader,
parseRequestBody,
createHTTPRequest,
addCredentials,
}

// user defined request middlewares
c.udBeforeRequest = []RequestMiddleware{}

// default after response middlewares
c.afterResponse = []ResponseMiddleware{
responseLogger,
parseResponseBody,
saveResponseIntoFile,
}

return c
}
35 changes: 35 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,41 @@ func TestDebugLogSimultaneously(t *testing.T) {
}
}

func TestNewWithTimeout(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,
}
client := NewWithTimeout(customTimeout)
client.SetHostURL(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()

dialer := &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
}
client := NewWithDialer(dialer)
client.SetHostURL(ts.URL)

resp, err := client.R().Get("/")
assertNil(t, err)
assertEqual(t, resp.String(), "TestGet: text response")
}

func TestNewWithLocalAddr(t *testing.T) {
ts := createGetServer(t)
defer ts.Close()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/go-resty/resty/v3

go 1.20
go 1.18

require (
golang.org/x/net v0.25.0
Expand Down
132 changes: 127 additions & 5 deletions resty.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,49 @@
package resty

import (
"encoding/json"
"encoding/xml"
"math"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
"runtime"
"sync"
"time"

"golang.org/x/net/publicsuffix"
)

// 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 {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return NewWithTimeout(defaultClientTimeout)
}

// NewWithTimeout method creates a new Resty client with provided
// timeout values.
//
// Since v3.0.0
func NewWithTimeout(timeoutSetting *ClientTimeoutSetting) *Client {
return createClient(&http.Client{
Jar: cookieJar,
Jar: createCookieJar(),
Transport: createTransport(
createDialer(nil, timeoutSetting),
timeoutSetting,
),
})
}

Expand All @@ -29,12 +57,106 @@ func NewWithClient(hc *http.Client) *Client {
return createClient(hc)
}

// 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 createClient(&http.Client{
Jar: createCookieJar(),
Transport: createTransport(dialer, defaultClientTimeout),
})
}

// NewWithLocalAddr method creates a new Resty client with given Local Address
// to dial from.
func NewWithLocalAddr(localAddr net.Addr) *Client {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return createClient(&http.Client{
Jar: cookieJar,
Transport: createTransport(localAddr),
Jar: createCookieJar(),
Transport: createTransport(
createDialer(localAddr, defaultClientTimeout),
defaultClientTimeout,
),
})
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Unexported methods
//_______________________________________________________________________

func createDialer(localAddr net.Addr, timeoutSetting *ClientTimeoutSetting) *net.Dialer {
dialer := &net.Dialer{
Timeout: timeoutSetting.DialerTimeout,
KeepAlive: timeoutSetting.DialerKeepAlive,
}
if localAddr != nil {
dialer.LocalAddr = localAddr
}
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,
}
}

func createCookieJar() *cookiejar.Jar {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return cookieJar
}

func createClient(hc *http.Client) *Client {
c := &Client{ // not setting language default values
QueryParam: url.Values{},
FormData: url.Values{},
Header: http.Header{},
Cookies: make([]*http.Cookie, 0),
RetryWaitTime: defaultWaitTime,
RetryMaxWaitTime: defaultMaxWaitTime,
PathParams: make(map[string]string),
RawPathParams: make(map[string]string),
JSONMarshal: json.Marshal,
JSONUnmarshal: json.Unmarshal,
XMLMarshal: xml.Marshal,
XMLUnmarshal: xml.Unmarshal,
HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"),

jsonEscapeHTML: true,
httpClient: hc,
debugBodySizeLimit: math.MaxInt32,
udBeforeRequestLock: &sync.RWMutex{},
afterResponseLock: &sync.RWMutex{},
}

// Logger
c.SetLogger(createLogger())

// default before request middlewares
c.beforeRequest = []RequestMiddleware{
parseRequestURL,
parseRequestHeader,
parseRequestBody,
createHTTPRequest,
addCredentials,
}

// user defined request middlewares
c.udBeforeRequest = []RequestMiddleware{}

// default after response middlewares
c.afterResponse = []ResponseMiddleware{
responseLogger,
parseResponseBody,
saveResponseIntoFile,
}

return c
}
Loading

0 comments on commit a5d485c

Please sign in to comment.