diff --git a/BUILD.bazel b/BUILD.bazel index f461c29d..6e730167 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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", diff --git a/client.go b/client.go index 1bcafba8..20292a7b 100644 --- a/client.go +++ b/client.go @@ -9,12 +9,9 @@ import ( "compress/gzip" "crypto/tls" "crypto/x509" - "encoding/json" - "encoding/xml" "errors" "fmt" "io" - "math" "net/http" "net/url" "os" @@ -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. // @@ -99,7 +117,6 @@ type ( // at request level. type Client struct { BaseURL string - HostURL string // Deprecated: use BaseURL instead. To be removed in v3.0.0 release. QueryParam url.Values FormData url.Values PathParams map[string]string @@ -164,21 +181,6 @@ type User struct { // Client methods //___________________________________ -// SetHostURL method is to set Host URL in the client instance. It will be used with request -// raised from this client with relative URL -// -// // Setting HTTP address -// client.SetHostURL("http://myjeeva.com") -// -// // Setting HTTPS address -// client.SetHostURL("https://myjeeva.com") -// -// Deprecated: use SetBaseURL instead. To be removed in v3.0.0 release. -func (c *Client) SetHostURL(url string) *Client { - c.SetBaseURL(url) - return c -} - // SetBaseURL method is to set Base URL in the client instance. It will be used with request // raised from this client with relative URL // @@ -191,7 +193,6 @@ func (c *Client) SetHostURL(url string) *Client { // Since v2.7.0 func (c *Client) SetBaseURL(url string) *Client { c.BaseURL = strings.TrimRight(url, "/") - c.HostURL = c.BaseURL return c } @@ -1121,10 +1122,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: @@ -1263,17 +1277,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) @@ -1354,59 +1357,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 -} diff --git a/client_test.go b/client_test.go index 6ea3dbd5..faa27103 100644 --- a/client_test.go +++ b/client_test.go @@ -29,7 +29,7 @@ func TestClientBasicAuth(t *testing.T) { c := dc() c.SetBasicAuth("myuser", "basicauth"). - SetHostURL(ts.URL). + SetBaseURL(ts.URL). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) resp, err := c.R(). @@ -50,7 +50,7 @@ func TestClientAuthToken(t *testing.T) { c := dc() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). - SetHostURL(ts.URL + "/") + SetBaseURL(ts.URL + "/") resp, err := c.R().Get("/profile") @@ -66,7 +66,7 @@ func TestClientAuthScheme(t *testing.T) { // Ensure default Bearer c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). - SetHostURL(ts.URL + "/") + SetBaseURL(ts.URL + "/") resp, err := c.R().Get("/profile") @@ -384,8 +384,8 @@ func TestClientOptions(t *testing.T) { client.SetContentLength(true) assertEqual(t, client.setContentLength, true) - client.SetHostURL("http://httpbin.org") - assertEqual(t, "http://httpbin.org", client.HostURL) + client.SetBaseURL("http://httpbin.org") + assertEqual(t, "http://httpbin.org", client.BaseURL) client.SetHeader(hdrContentTypeKey, "application/json; charset=utf-8") client.SetHeaders(map[string]string{ @@ -791,13 +791,48 @@ 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.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() + + dialer := &net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 15 * time.Second, + } + client := NewWithDialer(dialer) + client.SetBaseURL(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() localAddress, _ := net.ResolveTCPAddr("tcp", "127.0.0.1") client := NewWithLocalAddr(localAddress) - client.SetHostURL(ts.URL) + client.SetBaseURL(ts.URL) resp, err := client.R().Get("/") assertNil(t, err) @@ -1012,7 +1047,7 @@ func TestHostURLForGH318AndGH407(t *testing.T) { // assertNotNil(t, resp) t.Log("with leading `/` on request & with trailing `/` on host url") - c.SetHostURL(ts.URL + "/") + c.SetBaseURL(ts.URL + "/") resp, err := c.R(). SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}). Post("/login") @@ -1060,7 +1095,7 @@ func TestUnixSocket(t *testing.T) { // Set the previous transport that we created, set the scheme of the communication to the // socket and set the unixSocket as the HostURL. - client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocketAddr) + client.SetTransport(&transport).SetScheme("http").SetBaseURL(unixSocketAddr) // No need to write the host's URL on the request, just the path. res, err := client.R().Get("http://localhost/") diff --git a/go.mod b/go.mod index bffef819..2959f044 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/middleware.go b/middleware.go index 603448df..d116ae0f 100644 --- a/middleware.go +++ b/middleware.go @@ -114,12 +114,7 @@ func parseRequestURL(c *Client, r *Request) error { r.URL = "/" + r.URL } - // TODO: change to use c.BaseURL only in v3.0.0 - baseURL := c.BaseURL - if len(baseURL) == 0 { - baseURL = c.HostURL - } - reqURL, err = url.Parse(baseURL + r.URL) + reqURL, err = url.Parse(c.BaseURL + r.URL) if err != nil { return err } diff --git a/middleware_test.go b/middleware_test.go index 49733e95..d585221c 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -182,7 +182,7 @@ func Test_parseRequestURL(t *testing.T) { { name: "using deprecated HostURL with relative path in request URL", init: func(c *Client, r *Request) { - c.HostURL = "https://example.com" + c.BaseURL = "https://example.com" r.URL = "foo/bar" }, expectedURL: "https://example.com/foo/bar", diff --git a/request_test.go b/request_test.go index 7312128f..01e07844 100644 --- a/request_test.go +++ b/request_test.go @@ -164,7 +164,7 @@ func TestGetRelativePath(t *testing.T) { defer ts.Close() c := dc() - c.SetHostURL(ts.URL) + c.SetBaseURL(ts.URL) resp, err := c.R().Get("mypage2") @@ -594,7 +594,7 @@ func TestRequestBasicAuth(t *testing.T) { defer ts.Close() c := dc() - c.SetHostURL(ts.URL). + c.SetBaseURL(ts.URL). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) resp, err := c.R(). @@ -618,7 +618,7 @@ func TestRequestInsecureBasicAuth(t *testing.T) { logger.l.SetOutput(&logBuf) c := dc() - c.SetHostURL(ts.URL) + c.SetBaseURL(ts.URL) resp, err := c.R(). SetBasicAuth("myuser", "basicauth"). @@ -1039,7 +1039,7 @@ func TestGetWithCookie(t *testing.T) { defer ts.Close() c := dcl() - c.SetHostURL(ts.URL) + c.SetBaseURL(ts.URL) c.SetCookie(&http.Cookie{ Name: "go-resty-1", Value: "This is cookie 1 value", @@ -1070,7 +1070,7 @@ func TestGetWithCookies(t *testing.T) { defer ts.Close() c := dc() - c.SetHostURL(ts.URL).SetDebug(true) + c.SetBaseURL(ts.URL).SetDebug(true) tu, _ := url.Parse(ts.URL) c.GetClient().Jar.SetCookies(tu, []*http.Cookie{ @@ -1369,7 +1369,7 @@ func TestIncorrectURL(t *testing.T) { assertEqual(t, true, (strings.Contains(err.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also") || strings.Contains(err.Error(), "parse \"//not.a.user@%66%6f%6f.com/just/a/path/also\""))) - c.SetHostURL("//not.a.user@%66%6f%6f.com") + c.SetBaseURL("//not.a.user@%66%6f%6f.com") _, err1 := c.R().Get("/just/a/path/also") assertEqual(t, true, (strings.Contains(err1.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also") || strings.Contains(err1.Error(), "parse \"//not.a.user@%66%6f%6f.com/just/a/path/also\""))) @@ -1765,7 +1765,7 @@ func TestRequestOverridesClientAuthorizationHeader(t *testing.T) { c := dc() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetHeader("Authorization", "some token"). - SetHostURL(ts.URL + "/") + SetBaseURL(ts.URL + "/") resp, err := c.R(). SetHeader("Authorization", "Bearer 004DDB79-6801-4587-B976-F093E6AC44FF"). @@ -1929,7 +1929,7 @@ func TestTraceInfo(t *testing.T) { serverAddr := ts.URL[strings.LastIndex(ts.URL, "/")+1:] client := dc() - client.SetHostURL(ts.URL).EnableTrace() + client.SetBaseURL(ts.URL).EnableTrace() for _, u := range []string{"/", "/json", "/long-text", "/long-json"} { resp, err := client.R().Get(u) assertNil(t, err) @@ -1974,7 +1974,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) { defer ts.Close() client := dc() - client.SetHostURL(ts.URL) + client.SetBaseURL(ts.URL) for _, u := range []string{"/", "/json", "/long-text", "/long-json"} { resp, err := client.R().Get(u) assertNil(t, err) @@ -1992,7 +1992,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) { func TestTraceInfoOnTimeout(t *testing.T) { client := dc() - client.SetHostURL("http://resty-nowhere.local").EnableTrace() + client.SetBaseURL("http://resty-nowhere.local").EnableTrace() resp, err := client.R().Get("/") assertNotNil(t, err) diff --git a/resty.go b/resty.go index 8b3d48b8..7ceaba86 100644 --- a/resty.go +++ b/resty.go @@ -6,9 +6,16 @@ package resty import ( + "encoding/json" + "encoding/xml" + "math" "net" "net/http" "net/http/cookiejar" + "net/url" + "runtime" + "sync" + "time" "golang.org/x/net/publicsuffix" ) @@ -16,11 +23,32 @@ 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 { - 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, + ), }) } @@ -29,12 +57,114 @@ 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 NewWithDialerAndTimeout(dialer, defaultClientTimeout) +} + +// NewWithDialer 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), + }) +} + // 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 +} diff --git a/transport.go b/transport.go deleted file mode 100644 index 191cd519..00000000 --- a/transport.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build go1.13 -// +build go1.13 - -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. -// resty source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package resty - -import ( - "net" - "net/http" - "runtime" - "time" -) - -func createTransport(localAddr net.Addr) *http.Transport { - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - } - if localAddr != nil { - dialer.LocalAddr = localAddr - } - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: transportDialContext(dialer), - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, - } -} diff --git a/transport112.go b/transport112.go deleted file mode 100644 index d4aa4175..00000000 --- a/transport112.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build !go1.13 -// +build !go1.13 - -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. -// resty source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package resty - -import ( - "net" - "net/http" - "runtime" - "time" -) - -func createTransport(localAddr net.Addr) *http.Transport { - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - } - if localAddr != nil { - dialer.LocalAddr = localAddr - } - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: dialer.DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, - } -}