1502 lines
47 KiB
Go
1502 lines
47 KiB
Go
// Copyright (c) 2015-2024 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 (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// MethodGet HTTP method
|
|
MethodGet = "GET"
|
|
|
|
// MethodPost HTTP method
|
|
MethodPost = "POST"
|
|
|
|
// MethodPut HTTP method
|
|
MethodPut = "PUT"
|
|
|
|
// MethodDelete HTTP method
|
|
MethodDelete = "DELETE"
|
|
|
|
// MethodPatch HTTP method
|
|
MethodPatch = "PATCH"
|
|
|
|
// MethodHead HTTP method
|
|
MethodHead = "HEAD"
|
|
|
|
// MethodOptions HTTP method
|
|
MethodOptions = "OPTIONS"
|
|
)
|
|
|
|
var (
|
|
hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent")
|
|
hdrAcceptKey = http.CanonicalHeaderKey("Accept")
|
|
hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type")
|
|
hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length")
|
|
hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding")
|
|
hdrLocationKey = http.CanonicalHeaderKey("Location")
|
|
hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization")
|
|
hdrWwwAuthenticateKey = http.CanonicalHeaderKey("WWW-Authenticate")
|
|
|
|
plainTextType = "text/plain; charset=utf-8"
|
|
jsonContentType = "application/json"
|
|
formContentType = "application/x-www-form-urlencoded"
|
|
|
|
jsonCheck = regexp.MustCompile(`(?i:(application|text)/(.*json.*)(;|$))`)
|
|
xmlCheck = regexp.MustCompile(`(?i:(application|text)/(.*xml.*)(;|$))`)
|
|
|
|
hdrUserAgentValue = "go-resty/" + Version + " (https://github.com/go-resty/resty)"
|
|
bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
|
)
|
|
|
|
type (
|
|
// RequestMiddleware type is for request middleware, called before a request is sent
|
|
RequestMiddleware func(*Client, *Request) error
|
|
|
|
// ResponseMiddleware type is for response middleware, called after a response has been received
|
|
ResponseMiddleware func(*Client, *Response) error
|
|
|
|
// PreRequestHook type is for the request hook, called right before the request is sent
|
|
PreRequestHook func(*Client, *http.Request) error
|
|
|
|
// RequestLogCallback type is for request logs, called before the request is logged
|
|
RequestLogCallback func(*RequestLog) error
|
|
|
|
// ResponseLogCallback type is for response logs, called before the response is logged
|
|
ResponseLogCallback func(*ResponseLog) error
|
|
|
|
// ErrorHook type is for reacting to request errors, called after all retries were attempted
|
|
ErrorHook func(*Request, error)
|
|
|
|
// SuccessHook type is for reacting to request success
|
|
SuccessHook func(*Client, *Response)
|
|
)
|
|
|
|
// Client struct is used to create a Resty client with client-level settings,
|
|
// these settings apply to all the requests raised from the client.
|
|
//
|
|
// Resty also provides an option to override most of the client settings
|
|
// 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
|
|
RawPathParams map[string]string
|
|
Header http.Header
|
|
UserInfo *User
|
|
Token string
|
|
AuthScheme string
|
|
Cookies []*http.Cookie
|
|
Error reflect.Type
|
|
Debug bool
|
|
DisableWarn bool
|
|
AllowGetMethodPayload bool
|
|
RetryCount int
|
|
RetryWaitTime time.Duration
|
|
RetryMaxWaitTime time.Duration
|
|
RetryConditions []RetryConditionFunc
|
|
RetryHooks []OnRetryFunc
|
|
RetryAfter RetryAfterFunc
|
|
RetryResetReaders bool
|
|
JSONMarshal func(v interface{}) ([]byte, error)
|
|
JSONUnmarshal func(data []byte, v interface{}) error
|
|
XMLMarshal func(v interface{}) ([]byte, error)
|
|
XMLUnmarshal func(data []byte, v interface{}) error
|
|
|
|
// HeaderAuthorizationKey is used to set/access Request Authorization header
|
|
// value when `SetAuthToken` option is used.
|
|
HeaderAuthorizationKey string
|
|
ResponseBodyLimit int
|
|
|
|
jsonEscapeHTML bool
|
|
setContentLength bool
|
|
closeConnection bool
|
|
notParseResponse bool
|
|
trace bool
|
|
debugBodySizeLimit int64
|
|
outputDirectory string
|
|
scheme string
|
|
log Logger
|
|
httpClient *http.Client
|
|
proxyURL *url.URL
|
|
beforeRequest []RequestMiddleware
|
|
udBeforeRequest []RequestMiddleware
|
|
udBeforeRequestLock *sync.RWMutex
|
|
preReqHook PreRequestHook
|
|
successHooks []SuccessHook
|
|
afterResponse []ResponseMiddleware
|
|
afterResponseLock *sync.RWMutex
|
|
requestLog RequestLogCallback
|
|
responseLog ResponseLogCallback
|
|
errorHooks []ErrorHook
|
|
invalidHooks []ErrorHook
|
|
panicHooks []ErrorHook
|
|
rateLimiter RateLimiter
|
|
generateCurlOnDebug bool
|
|
unescapeQueryParams bool
|
|
}
|
|
|
|
// User type is to hold an username and password information
|
|
type User struct {
|
|
Username, Password string
|
|
}
|
|
|
|
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
// Client methods
|
|
//___________________________________
|
|
|
|
// SetHostURL method sets the Host URL in the client instance. It will be used with a request
|
|
// raised from this client with a relative URL
|
|
//
|
|
// // Setting HTTP address
|
|
// client.SetHostURL("http://myjeeva.com")
|
|
//
|
|
// // Setting HTTPS address
|
|
// client.SetHostURL("https://myjeeva.com")
|
|
//
|
|
// Deprecated: use [Client.SetBaseURL] instead. To be removed in the v3.0.0 release.
|
|
func (c *Client) SetHostURL(url string) *Client {
|
|
c.SetBaseURL(url)
|
|
return c
|
|
}
|
|
|
|
// SetBaseURL method sets the Base URL in the client instance. It will be used with a request
|
|
// raised from this client with a relative URL
|
|
//
|
|
// // Setting HTTP address
|
|
// client.SetBaseURL("http://myjeeva.com")
|
|
//
|
|
// // Setting HTTPS address
|
|
// client.SetBaseURL("https://myjeeva.com")
|
|
func (c *Client) SetBaseURL(url string) *Client {
|
|
c.BaseURL = strings.TrimRight(url, "/")
|
|
c.HostURL = c.BaseURL
|
|
return c
|
|
}
|
|
|
|
// SetHeader method sets a single header field and its value in the client instance.
|
|
// These headers will be applied to all requests from this client instance.
|
|
// Also, it can be overridden by request-level header options.
|
|
//
|
|
// See [Request.SetHeader] or [Request.SetHeaders].
|
|
//
|
|
// For Example: To set `Content-Type` and `Accept` as `application/json`
|
|
//
|
|
// client.
|
|
// SetHeader("Content-Type", "application/json").
|
|
// SetHeader("Accept", "application/json")
|
|
func (c *Client) SetHeader(header, value string) *Client {
|
|
c.Header.Set(header, value)
|
|
return c
|
|
}
|
|
|
|
// SetHeaders method sets multiple header fields and their values at one go in the client instance.
|
|
// These headers will be applied to all requests from this client instance. Also, it can be
|
|
// overridden at request level headers options.
|
|
//
|
|
// See [Request.SetHeaders] or [Request.SetHeader].
|
|
//
|
|
// For Example: To set `Content-Type` and `Accept` as `application/json`
|
|
//
|
|
// client.SetHeaders(map[string]string{
|
|
// "Content-Type": "application/json",
|
|
// "Accept": "application/json",
|
|
// })
|
|
func (c *Client) SetHeaders(headers map[string]string) *Client {
|
|
for h, v := range headers {
|
|
c.Header.Set(h, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetHeaderVerbatim method sets a single header field and its value verbatim in the current request.
|
|
//
|
|
// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
|
|
//
|
|
// client.
|
|
// SetHeaderVerbatim("all_lowercase", "available").
|
|
// SetHeaderVerbatim("UPPERCASE", "available")
|
|
func (c *Client) SetHeaderVerbatim(header, value string) *Client {
|
|
c.Header[header] = []string{value}
|
|
return c
|
|
}
|
|
|
|
// SetCookieJar method sets custom [http.CookieJar] in the resty client. It's a way to override the default.
|
|
//
|
|
// For Example, sometimes we don't want to save cookies in API mode so that we can remove the default
|
|
// CookieJar in resty client.
|
|
//
|
|
// client.SetCookieJar(nil)
|
|
func (c *Client) SetCookieJar(jar http.CookieJar) *Client {
|
|
c.httpClient.Jar = jar
|
|
return c
|
|
}
|
|
|
|
// SetCookie method appends a single cookie to the client instance.
|
|
// These cookies will be added to all the requests from this client instance.
|
|
//
|
|
// client.SetCookie(&http.Cookie{
|
|
// Name:"go-resty",
|
|
// Value:"This is cookie value",
|
|
// })
|
|
func (c *Client) SetCookie(hc *http.Cookie) *Client {
|
|
c.Cookies = append(c.Cookies, hc)
|
|
return c
|
|
}
|
|
|
|
// SetCookies method sets an array of cookies in the client instance.
|
|
// These cookies will be added to all the requests from this client instance.
|
|
//
|
|
// cookies := []*http.Cookie{
|
|
// &http.Cookie{
|
|
// Name:"go-resty-1",
|
|
// Value:"This is cookie 1 value",
|
|
// },
|
|
// &http.Cookie{
|
|
// Name:"go-resty-2",
|
|
// Value:"This is cookie 2 value",
|
|
// },
|
|
// }
|
|
//
|
|
// // Setting a cookies into resty
|
|
// client.SetCookies(cookies)
|
|
func (c *Client) SetCookies(cs []*http.Cookie) *Client {
|
|
c.Cookies = append(c.Cookies, cs...)
|
|
return c
|
|
}
|
|
|
|
// SetQueryParam method sets a single parameter and its value in the client instance.
|
|
// It will be formed as a query string for the request.
|
|
//
|
|
// For Example: `search=kitchen%20papers&size=large`
|
|
//
|
|
// In the URL after the `?` mark. These query params will be added to all the requests raised from
|
|
// this client instance. Also, it can be overridden at the request level.
|
|
//
|
|
// See [Request.SetQueryParam] or [Request.SetQueryParams].
|
|
//
|
|
// client.
|
|
// SetQueryParam("search", "kitchen papers").
|
|
// SetQueryParam("size", "large")
|
|
func (c *Client) SetQueryParam(param, value string) *Client {
|
|
c.QueryParam.Set(param, value)
|
|
return c
|
|
}
|
|
|
|
// SetQueryParams method sets multiple parameters and their values at one go in the client instance.
|
|
// It will be formed as a query string for the request.
|
|
//
|
|
// For Example: `search=kitchen%20papers&size=large`
|
|
//
|
|
// In the URL after the `?` mark. These query params will be added to all the requests raised from this
|
|
// client instance. Also, it can be overridden at the request level.
|
|
//
|
|
// See [Request.SetQueryParams] or [Request.SetQueryParam].
|
|
//
|
|
// client.SetQueryParams(map[string]string{
|
|
// "search": "kitchen papers",
|
|
// "size": "large",
|
|
// })
|
|
func (c *Client) SetQueryParams(params map[string]string) *Client {
|
|
for p, v := range params {
|
|
c.SetQueryParam(p, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetUnescapeQueryParams method sets the unescape query parameters choice for request URL.
|
|
// To prevent broken URL, resty replaces space (" ") with "+" in the query parameters.
|
|
//
|
|
// See [Request.SetUnescapeQueryParams]
|
|
//
|
|
// NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.
|
|
func (c *Client) SetUnescapeQueryParams(unescape bool) *Client {
|
|
c.unescapeQueryParams = unescape
|
|
return c
|
|
}
|
|
|
|
// SetFormData method sets Form parameters and their values in the client instance.
|
|
// It applies only to HTTP methods `POST` and `PUT`, and the request content type would be set as
|
|
// `application/x-www-form-urlencoded`. These form data will be added to all the requests raised from
|
|
// this client instance. Also, it can be overridden at the request level.
|
|
//
|
|
// See [Request.SetFormData].
|
|
//
|
|
// client.SetFormData(map[string]string{
|
|
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
|
|
// "user_id": "3455454545",
|
|
// })
|
|
func (c *Client) SetFormData(data map[string]string) *Client {
|
|
for k, v := range data {
|
|
c.FormData.Set(k, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetBasicAuth method sets the basic authentication header in the HTTP request. For Example:
|
|
//
|
|
// Authorization: Basic <base64-encoded-value>
|
|
//
|
|
// For Example: To set the header for username "go-resty" and password "welcome"
|
|
//
|
|
// client.SetBasicAuth("go-resty", "welcome")
|
|
//
|
|
// This basic auth information is added to all requests from this client instance.
|
|
// It can also be overridden at the request level.
|
|
//
|
|
// See [Request.SetBasicAuth].
|
|
func (c *Client) SetBasicAuth(username, password string) *Client {
|
|
c.UserInfo = &User{Username: username, Password: password}
|
|
return c
|
|
}
|
|
|
|
// SetAuthToken method sets the auth token of the `Authorization` header for all HTTP requests.
|
|
// The default auth scheme is `Bearer`; it can be customized with the method [Client.SetAuthScheme]. For Example:
|
|
//
|
|
// Authorization: <auth-scheme> <auth-token-value>
|
|
//
|
|
// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
|
|
//
|
|
// client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
|
|
//
|
|
// This auth token gets added to all the requests raised from this client instance.
|
|
// Also, it can be overridden at the request level.
|
|
//
|
|
// See [Request.SetAuthToken].
|
|
func (c *Client) SetAuthToken(token string) *Client {
|
|
c.Token = token
|
|
return c
|
|
}
|
|
|
|
// SetAuthScheme method sets the auth scheme type in the HTTP request. For Example:
|
|
//
|
|
// Authorization: <auth-scheme-value> <auth-token-value>
|
|
//
|
|
// For Example: To set the scheme to use OAuth
|
|
//
|
|
// client.SetAuthScheme("OAuth")
|
|
//
|
|
// This auth scheme gets added to all the requests raised from this client instance.
|
|
// Also, it can be overridden at the request level.
|
|
//
|
|
// Information about auth schemes can be found in [RFC 7235], IANA [HTTP Auth schemes].
|
|
//
|
|
// See [Request.SetAuthToken].
|
|
//
|
|
// [RFC 7235]: https://tools.ietf.org/html/rfc7235
|
|
// [HTTP Auth schemes]: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
|
|
func (c *Client) SetAuthScheme(scheme string) *Client {
|
|
c.AuthScheme = scheme
|
|
return c
|
|
}
|
|
|
|
// SetDigestAuth method sets the Digest Access auth scheme for the client. If a server responds with 401 and sends
|
|
// a Digest challenge in the WWW-Authenticate Header, requests will be resent with the appropriate Authorization Header.
|
|
//
|
|
// For Example: To set the Digest scheme with user "Mufasa" and password "Circle Of Life"
|
|
//
|
|
// client.SetDigestAuth("Mufasa", "Circle Of Life")
|
|
//
|
|
// Information about Digest Access Authentication can be found in [RFC 7616].
|
|
//
|
|
// See [Request.SetDigestAuth].
|
|
//
|
|
// [RFC 7616]: https://datatracker.ietf.org/doc/html/rfc7616
|
|
func (c *Client) SetDigestAuth(username, password string) *Client {
|
|
oldTransport := c.httpClient.Transport
|
|
c.OnBeforeRequest(func(c *Client, _ *Request) error {
|
|
c.httpClient.Transport = &digestTransport{
|
|
digestCredentials: digestCredentials{username, password},
|
|
transport: oldTransport,
|
|
}
|
|
return nil
|
|
})
|
|
c.OnAfterResponse(func(c *Client, _ *Response) error {
|
|
c.httpClient.Transport = oldTransport
|
|
return nil
|
|
})
|
|
return c
|
|
}
|
|
|
|
// R method creates a new request instance; it's used for Get, Post, Put, Delete, Patch, Head, Options, etc.
|
|
func (c *Client) R() *Request {
|
|
r := &Request{
|
|
QueryParam: url.Values{},
|
|
FormData: url.Values{},
|
|
Header: http.Header{},
|
|
Cookies: make([]*http.Cookie, 0),
|
|
PathParams: map[string]string{},
|
|
RawPathParams: map[string]string{},
|
|
Debug: c.Debug,
|
|
AuthScheme: c.AuthScheme,
|
|
Token: c.Token,
|
|
|
|
client: c,
|
|
multipartFiles: []*File{},
|
|
multipartFields: []*MultipartField{},
|
|
jsonEscapeHTML: c.jsonEscapeHTML,
|
|
log: c.log,
|
|
responseBodyLimit: c.ResponseBodyLimit,
|
|
generateCurlOnDebug: c.generateCurlOnDebug,
|
|
unescapeQueryParams: c.unescapeQueryParams,
|
|
}
|
|
return r
|
|
}
|
|
|
|
// NewRequest method is an alias for method `R()`.
|
|
func (c *Client) NewRequest() *Request {
|
|
return c.R()
|
|
}
|
|
|
|
// OnBeforeRequest method appends a request middleware to the before request chain.
|
|
// The user-defined middlewares are applied before the default Resty request middlewares.
|
|
// After all middlewares have been applied, the request is sent from Resty to the host server.
|
|
//
|
|
// client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
|
|
// // Now you have access to the Client and Request instance
|
|
// // manipulate it as per your need
|
|
//
|
|
// return nil // if its successful otherwise return error
|
|
// })
|
|
func (c *Client) OnBeforeRequest(m RequestMiddleware) *Client {
|
|
c.udBeforeRequestLock.Lock()
|
|
defer c.udBeforeRequestLock.Unlock()
|
|
|
|
c.udBeforeRequest = append(c.udBeforeRequest, m)
|
|
|
|
return c
|
|
}
|
|
|
|
// OnAfterResponse method appends response middleware to the after-response chain.
|
|
// Once we receive a response from the host server, the default Resty response middleware
|
|
// gets applied, and then the user-assigned response middleware is applied.
|
|
//
|
|
// client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
|
|
// // Now you have access to the Client and Response instance
|
|
// // manipulate it as per your need
|
|
//
|
|
// return nil // if its successful otherwise return error
|
|
// })
|
|
func (c *Client) OnAfterResponse(m ResponseMiddleware) *Client {
|
|
c.afterResponseLock.Lock()
|
|
defer c.afterResponseLock.Unlock()
|
|
|
|
c.afterResponse = append(c.afterResponse, m)
|
|
|
|
return c
|
|
}
|
|
|
|
// OnError method adds a callback that will be run whenever a request execution fails.
|
|
// This is called after all retries have been attempted (if any).
|
|
// If there was a response from the server, the error will be wrapped in [ResponseError]
|
|
// which has the last response received from the server.
|
|
//
|
|
// client.OnError(func(req *resty.Request, err error) {
|
|
// if v, ok := err.(*resty.ResponseError); ok {
|
|
// // Do something with v.Response
|
|
// }
|
|
// // Log the error, increment a metric, etc...
|
|
// })
|
|
//
|
|
// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]
|
|
// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.
|
|
func (c *Client) OnError(h ErrorHook) *Client {
|
|
c.errorHooks = append(c.errorHooks, h)
|
|
return c
|
|
}
|
|
|
|
// OnSuccess method adds a callback that will be run whenever a request execution
|
|
// succeeds. This is called after all retries have been attempted (if any).
|
|
//
|
|
// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]
|
|
// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.
|
|
func (c *Client) OnSuccess(h SuccessHook) *Client {
|
|
c.successHooks = append(c.successHooks, h)
|
|
return c
|
|
}
|
|
|
|
// OnInvalid method adds a callback that will be run whenever a request execution
|
|
// fails before it starts because the request is invalid.
|
|
//
|
|
// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]
|
|
// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.
|
|
func (c *Client) OnInvalid(h ErrorHook) *Client {
|
|
c.invalidHooks = append(c.invalidHooks, h)
|
|
return c
|
|
}
|
|
|
|
// OnPanic method adds a callback that will be run whenever a request execution
|
|
// panics.
|
|
//
|
|
// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]
|
|
// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.
|
|
//
|
|
// If an [Client.OnSuccess], [Client.OnError], or [Client.OnInvalid] callback panics,
|
|
// then exactly one rule can be violated.
|
|
func (c *Client) OnPanic(h ErrorHook) *Client {
|
|
c.panicHooks = append(c.panicHooks, h)
|
|
return c
|
|
}
|
|
|
|
// SetPreRequestHook method sets the given pre-request function into a resty client.
|
|
// It is called right before the request is fired.
|
|
//
|
|
// NOTE: Only one pre-request hook can be registered. Use [Client.OnBeforeRequest] for multiple.
|
|
func (c *Client) SetPreRequestHook(h PreRequestHook) *Client {
|
|
if c.preReqHook != nil {
|
|
c.log.Warnf("Overwriting an existing pre-request hook: %s", functionName(h))
|
|
}
|
|
c.preReqHook = h
|
|
return c
|
|
}
|
|
|
|
// SetDebug method enables the debug mode on the Resty client. The client logs details
|
|
// of every request and response.
|
|
//
|
|
// client.SetDebug(true)
|
|
//
|
|
// Also, it can be enabled at the request level for a particular request; see [Request.SetDebug].
|
|
// - For [Request], it logs information such as HTTP verb, Relative URL path,
|
|
// Host, Headers, and Body if it has one.
|
|
// - For [Response], it logs information such as Status, Response Time, Headers,
|
|
// and Body if it has one.
|
|
func (c *Client) SetDebug(d bool) *Client {
|
|
c.Debug = d
|
|
return c
|
|
}
|
|
|
|
// SetDebugBodyLimit sets the maximum size in bytes for which the response and
|
|
// request body will be logged in debug mode.
|
|
//
|
|
// client.SetDebugBodyLimit(1000000)
|
|
func (c *Client) SetDebugBodyLimit(sl int64) *Client {
|
|
c.debugBodySizeLimit = sl
|
|
return c
|
|
}
|
|
|
|
// OnRequestLog method sets the request log callback to Resty. Registered callback gets
|
|
// called before the resty logs the information.
|
|
func (c *Client) OnRequestLog(rl RequestLogCallback) *Client {
|
|
if c.requestLog != nil {
|
|
c.log.Warnf("Overwriting an existing on-request-log callback from=%s to=%s",
|
|
functionName(c.requestLog), functionName(rl))
|
|
}
|
|
c.requestLog = rl
|
|
return c
|
|
}
|
|
|
|
// OnResponseLog method sets the response log callback to Resty. Registered callback gets
|
|
// called before the resty logs the information.
|
|
func (c *Client) OnResponseLog(rl ResponseLogCallback) *Client {
|
|
if c.responseLog != nil {
|
|
c.log.Warnf("Overwriting an existing on-response-log callback from=%s to=%s",
|
|
functionName(c.responseLog), functionName(rl))
|
|
}
|
|
c.responseLog = rl
|
|
return c
|
|
}
|
|
|
|
// SetDisableWarn method disables the warning log message on the Resty client.
|
|
//
|
|
// For example, Resty warns users when BasicAuth is used in non-TLS mode.
|
|
//
|
|
// client.SetDisableWarn(true)
|
|
func (c *Client) SetDisableWarn(d bool) *Client {
|
|
c.DisableWarn = d
|
|
return c
|
|
}
|
|
|
|
// SetAllowGetMethodPayload method allows the GET method with payload on the Resty client.
|
|
//
|
|
// For example, Resty allows the user to send a request with a payload using the HTTP GET method.
|
|
//
|
|
// client.SetAllowGetMethodPayload(true)
|
|
func (c *Client) SetAllowGetMethodPayload(a bool) *Client {
|
|
c.AllowGetMethodPayload = a
|
|
return c
|
|
}
|
|
|
|
// SetLogger method sets given writer for logging Resty request and response details.
|
|
//
|
|
// Compliant to interface [resty.Logger]
|
|
func (c *Client) SetLogger(l Logger) *Client {
|
|
c.log = l
|
|
return c
|
|
}
|
|
|
|
// SetContentLength method enables the HTTP header `Content-Length` value for every request.
|
|
// By default, Resty won't set `Content-Length`.
|
|
//
|
|
// client.SetContentLength(true)
|
|
//
|
|
// Also, you have the option to enable a particular request. See [Request.SetContentLength]
|
|
func (c *Client) SetContentLength(l bool) *Client {
|
|
c.setContentLength = l
|
|
return c
|
|
}
|
|
|
|
// SetTimeout method sets the timeout for a request raised by the client.
|
|
//
|
|
// client.SetTimeout(time.Duration(1 * time.Minute))
|
|
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
|
c.httpClient.Timeout = timeout
|
|
return c
|
|
}
|
|
|
|
// SetError method registers the global or client common `Error` object into Resty.
|
|
// It is used for automatic unmarshalling if the response status code is greater than 399 and
|
|
// content type is JSON or XML. It can be a pointer or a non-pointer.
|
|
//
|
|
// client.SetError(&Error{})
|
|
// // OR
|
|
// client.SetError(Error{})
|
|
func (c *Client) SetError(err interface{}) *Client {
|
|
c.Error = typeOf(err)
|
|
return c
|
|
}
|
|
|
|
// SetRedirectPolicy method sets the redirect policy for the client. Resty provides ready-to-use
|
|
// redirect policies. Wanna create one for yourself, refer to `redirect.go`.
|
|
//
|
|
// client.SetRedirectPolicy(FlexibleRedirectPolicy(20))
|
|
//
|
|
// // Need multiple redirect policies together
|
|
// client.SetRedirectPolicy(FlexibleRedirectPolicy(20), DomainCheckRedirectPolicy("host1.com", "host2.net"))
|
|
func (c *Client) SetRedirectPolicy(policies ...interface{}) *Client {
|
|
for _, p := range policies {
|
|
if _, ok := p.(RedirectPolicy); !ok {
|
|
c.log.Errorf("%v does not implement resty.RedirectPolicy (missing Apply method)",
|
|
functionName(p))
|
|
}
|
|
}
|
|
|
|
c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
for _, p := range policies {
|
|
if err := p.(RedirectPolicy).Apply(req, via); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil // looks good, go ahead
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// SetRetryCount method enables retry on Resty client and allows you
|
|
// to set no. of retry count. Resty uses a Backoff mechanism.
|
|
func (c *Client) SetRetryCount(count int) *Client {
|
|
c.RetryCount = count
|
|
return c
|
|
}
|
|
|
|
// SetRetryWaitTime method sets the default wait time for sleep before retrying
|
|
// request.
|
|
//
|
|
// Default is 100 milliseconds.
|
|
func (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client {
|
|
c.RetryWaitTime = waitTime
|
|
return c
|
|
}
|
|
|
|
// SetRetryMaxWaitTime method sets the max wait time for sleep before retrying
|
|
// request.
|
|
//
|
|
// Default is 2 seconds.
|
|
func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {
|
|
c.RetryMaxWaitTime = maxWaitTime
|
|
return c
|
|
}
|
|
|
|
// SetRetryAfter sets a callback to calculate the wait time between retries.
|
|
// Default (nil) implies exponential backoff with jitter
|
|
func (c *Client) SetRetryAfter(callback RetryAfterFunc) *Client {
|
|
c.RetryAfter = callback
|
|
return c
|
|
}
|
|
|
|
// SetJSONMarshaler method sets the JSON marshaler function to marshal the request body.
|
|
// By default, Resty uses [encoding/json] package to marshal the request body.
|
|
func (c *Client) SetJSONMarshaler(marshaler func(v interface{}) ([]byte, error)) *Client {
|
|
c.JSONMarshal = marshaler
|
|
return c
|
|
}
|
|
|
|
// SetJSONUnmarshaler method sets the JSON unmarshaler function to unmarshal the response body.
|
|
// By default, Resty uses [encoding/json] package to unmarshal the response body.
|
|
func (c *Client) SetJSONUnmarshaler(unmarshaler func(data []byte, v interface{}) error) *Client {
|
|
c.JSONUnmarshal = unmarshaler
|
|
return c
|
|
}
|
|
|
|
// SetXMLMarshaler method sets the XML marshaler function to marshal the request body.
|
|
// By default, Resty uses [encoding/xml] package to marshal the request body.
|
|
func (c *Client) SetXMLMarshaler(marshaler func(v interface{}) ([]byte, error)) *Client {
|
|
c.XMLMarshal = marshaler
|
|
return c
|
|
}
|
|
|
|
// SetXMLUnmarshaler method sets the XML unmarshaler function to unmarshal the response body.
|
|
// By default, Resty uses [encoding/xml] package to unmarshal the response body.
|
|
func (c *Client) SetXMLUnmarshaler(unmarshaler func(data []byte, v interface{}) error) *Client {
|
|
c.XMLUnmarshal = unmarshaler
|
|
return c
|
|
}
|
|
|
|
// AddRetryCondition method adds a retry condition function to an array of functions
|
|
// that are checked to determine if the request is retried. The request will
|
|
// retry if any functions return true and the error is nil.
|
|
//
|
|
// NOTE: These retry conditions are applied on all requests made using this Client.
|
|
// For [Request] specific retry conditions, check [Request.AddRetryCondition]
|
|
func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client {
|
|
c.RetryConditions = append(c.RetryConditions, condition)
|
|
return c
|
|
}
|
|
|
|
// AddRetryAfterErrorCondition adds the basic condition of retrying after encountering
|
|
// an error from the HTTP response
|
|
func (c *Client) AddRetryAfterErrorCondition() *Client {
|
|
c.AddRetryCondition(func(response *Response, err error) bool {
|
|
return response.IsError()
|
|
})
|
|
return c
|
|
}
|
|
|
|
// AddRetryHook adds a side-effecting retry hook to an array of hooks
|
|
// that will be executed on each retry.
|
|
func (c *Client) AddRetryHook(hook OnRetryFunc) *Client {
|
|
c.RetryHooks = append(c.RetryHooks, hook)
|
|
return c
|
|
}
|
|
|
|
// SetRetryResetReaders method enables the Resty client to seek the start of all
|
|
// file readers are given as multipart files if the object implements [io.ReadSeeker].
|
|
func (c *Client) SetRetryResetReaders(b bool) *Client {
|
|
c.RetryResetReaders = b
|
|
return c
|
|
}
|
|
|
|
// SetTLSClientConfig method sets TLSClientConfig for underlying client Transport.
|
|
//
|
|
// For Example:
|
|
//
|
|
// // One can set a custom root certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
|
|
// client.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
|
|
//
|
|
// // or One can disable security check (https)
|
|
// client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
|
|
//
|
|
// NOTE: This method overwrites existing [http.Transport.TLSClientConfig]
|
|
func (c *Client) SetTLSClientConfig(config *tls.Config) *Client {
|
|
transport, err := c.Transport()
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
transport.TLSClientConfig = config
|
|
return c
|
|
}
|
|
|
|
// SetProxy method sets the Proxy URL and Port for the Resty client.
|
|
//
|
|
// client.SetProxy("http://proxyserver:8888")
|
|
//
|
|
// OR you could also set Proxy via environment variable, refer to [http.ProxyFromEnvironment]
|
|
func (c *Client) SetProxy(proxyURL string) *Client {
|
|
transport, err := c.Transport()
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
|
|
pURL, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
|
|
c.proxyURL = pURL
|
|
transport.Proxy = http.ProxyURL(c.proxyURL)
|
|
return c
|
|
}
|
|
|
|
// RemoveProxy method removes the proxy configuration from the Resty client
|
|
//
|
|
// client.RemoveProxy()
|
|
func (c *Client) RemoveProxy() *Client {
|
|
transport, err := c.Transport()
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
c.proxyURL = nil
|
|
transport.Proxy = nil
|
|
return c
|
|
}
|
|
|
|
// SetCertificates method helps to conveniently set client certificates into Resty.
|
|
func (c *Client) SetCertificates(certs ...tls.Certificate) *Client {
|
|
config, err := c.tlsConfig()
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
config.Certificates = append(config.Certificates, certs...)
|
|
return c
|
|
}
|
|
|
|
// SetRootCertificate method helps to add one or more root certificates into the Resty client
|
|
//
|
|
// client.SetRootCertificate("/path/to/root/pemFile.pem")
|
|
func (c *Client) SetRootCertificate(pemFilePath string) *Client {
|
|
rootPemData, err := os.ReadFile(pemFilePath)
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
c.handleCAs("root", rootPemData)
|
|
return c
|
|
}
|
|
|
|
// SetRootCertificateFromString method helps to add one or more root certificates
|
|
// into the Resty client
|
|
//
|
|
// client.SetRootCertificateFromString("pem certs content")
|
|
func (c *Client) SetRootCertificateFromString(pemCerts string) *Client {
|
|
c.handleCAs("root", []byte(pemCerts))
|
|
return c
|
|
}
|
|
|
|
// SetClientRootCertificate method helps to add one or more client's root
|
|
// certificates into the Resty client
|
|
//
|
|
// client.SetClientRootCertificate("/path/to/root/pemFile.pem")
|
|
func (c *Client) SetClientRootCertificate(pemFilePath string) *Client {
|
|
rootPemData, err := os.ReadFile(pemFilePath)
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return c
|
|
}
|
|
c.handleCAs("client", rootPemData)
|
|
return c
|
|
}
|
|
|
|
// SetClientRootCertificateFromString method helps to add one or more clients
|
|
// root certificates into the Resty client
|
|
//
|
|
// client.SetClientRootCertificateFromString("pem certs content")
|
|
func (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client {
|
|
c.handleCAs("client", []byte(pemCerts))
|
|
return c
|
|
}
|
|
|
|
func (c *Client) handleCAs(scope string, permCerts []byte) {
|
|
config, err := c.tlsConfig()
|
|
if err != nil {
|
|
c.log.Errorf("%v", err)
|
|
return
|
|
}
|
|
|
|
switch scope {
|
|
case "root":
|
|
if config.RootCAs == nil {
|
|
config.RootCAs = x509.NewCertPool()
|
|
}
|
|
config.RootCAs.AppendCertsFromPEM(permCerts)
|
|
case "client":
|
|
if config.ClientCAs == nil {
|
|
config.ClientCAs = x509.NewCertPool()
|
|
}
|
|
config.ClientCAs.AppendCertsFromPEM(permCerts)
|
|
}
|
|
}
|
|
|
|
// SetOutputDirectory method sets the output directory for saving HTTP responses in a file.
|
|
// Resty creates one if the output directory does not exist. This setting is optional,
|
|
// if you plan to use the absolute path in [Request.SetOutput] and can used together.
|
|
//
|
|
// client.SetOutputDirectory("/save/http/response/here")
|
|
func (c *Client) SetOutputDirectory(dirPath string) *Client {
|
|
c.outputDirectory = dirPath
|
|
return c
|
|
}
|
|
|
|
// SetRateLimiter sets an optional [RateLimiter]. If set, the rate limiter will control
|
|
// all requests were made by this client.
|
|
func (c *Client) SetRateLimiter(rl RateLimiter) *Client {
|
|
c.rateLimiter = rl
|
|
return c
|
|
}
|
|
|
|
// SetTransport method sets custom [http.Transport] or any [http.RoundTripper]
|
|
// compatible interface implementation in the Resty client.
|
|
//
|
|
// transport := &http.Transport{
|
|
// // something like Proxying to httptest.Server, etc...
|
|
// Proxy: func(req *http.Request) (*url.URL, error) {
|
|
// return url.Parse(server.URL)
|
|
// },
|
|
// }
|
|
// client.SetTransport(transport)
|
|
//
|
|
// NOTE:
|
|
// - If transport is not the type of `*http.Transport`, then you may not be able to
|
|
// take advantage of some of the Resty client settings.
|
|
// - It overwrites the Resty client transport instance and its configurations.
|
|
func (c *Client) SetTransport(transport http.RoundTripper) *Client {
|
|
if transport != nil {
|
|
c.httpClient.Transport = transport
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetScheme method sets a custom scheme for the Resty client. It's a way to override the default.
|
|
//
|
|
// client.SetScheme("http")
|
|
func (c *Client) SetScheme(scheme string) *Client {
|
|
if !IsStringEmpty(scheme) {
|
|
c.scheme = strings.TrimSpace(scheme)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetCloseConnection method sets variable `Close` in HTTP request struct with the given
|
|
// value. More info: https://golang.org/src/net/http/request.go
|
|
func (c *Client) SetCloseConnection(close bool) *Client {
|
|
c.closeConnection = close
|
|
return c
|
|
}
|
|
|
|
// SetDoNotParseResponse method instructs Resty not to parse the response body automatically.
|
|
// Resty exposes the raw response body as [io.ReadCloser]. If you use it, do not
|
|
// forget to close the body, otherwise, you might get into connection leaks, and connection
|
|
// reuse may not happen.
|
|
//
|
|
// NOTE: [Response] middlewares are not executed using this option. You have
|
|
// taken over the control of response parsing from Resty.
|
|
func (c *Client) SetDoNotParseResponse(notParse bool) *Client {
|
|
c.notParseResponse = notParse
|
|
return c
|
|
}
|
|
|
|
// SetPathParam method sets a single URL path key-value pair in the
|
|
// Resty client instance.
|
|
//
|
|
// client.SetPathParam("userId", "sample@sample.com")
|
|
//
|
|
// Result:
|
|
// URL - /v1/users/{userId}/details
|
|
// Composed URL - /v1/users/sample@sample.com/details
|
|
//
|
|
// It replaces the value of the key while composing the request URL.
|
|
// The value will be escaped using [url.PathEscape] function.
|
|
//
|
|
// It can be overridden at the request level,
|
|
// see [Request.SetPathParam] or [Request.SetPathParams]
|
|
func (c *Client) SetPathParam(param, value string) *Client {
|
|
c.PathParams[param] = value
|
|
return c
|
|
}
|
|
|
|
// SetPathParams method sets multiple URL path key-value pairs at one go in the
|
|
// Resty client instance.
|
|
//
|
|
// client.SetPathParams(map[string]string{
|
|
// "userId": "sample@sample.com",
|
|
// "subAccountId": "100002",
|
|
// "path": "groups/developers",
|
|
// })
|
|
//
|
|
// Result:
|
|
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
|
|
// Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details
|
|
//
|
|
// It replaces the value of the key while composing the request URL.
|
|
// The values will be escaped using [url.PathEscape] function.
|
|
//
|
|
// It can be overridden at the request level,
|
|
// see [Request.SetPathParam] or [Request.SetPathParams]
|
|
func (c *Client) SetPathParams(params map[string]string) *Client {
|
|
for p, v := range params {
|
|
c.SetPathParam(p, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetRawPathParam method sets a single URL path key-value pair in the
|
|
// Resty client instance.
|
|
//
|
|
// client.SetPathParam("userId", "sample@sample.com")
|
|
//
|
|
// Result:
|
|
// URL - /v1/users/{userId}/details
|
|
// Composed URL - /v1/users/sample@sample.com/details
|
|
//
|
|
// client.SetPathParam("path", "groups/developers")
|
|
//
|
|
// Result:
|
|
// URL - /v1/users/{userId}/details
|
|
// Composed URL - /v1/users/groups%2Fdevelopers/details
|
|
//
|
|
// It replaces the value of the key while composing the request URL.
|
|
// The value will be used as it is and will not be escaped.
|
|
//
|
|
// It can be overridden at the request level,
|
|
// see [Request.SetRawPathParam] or [Request.SetRawPathParams]
|
|
func (c *Client) SetRawPathParam(param, value string) *Client {
|
|
c.RawPathParams[param] = value
|
|
return c
|
|
}
|
|
|
|
// SetRawPathParams method sets multiple URL path key-value pairs at one go in the
|
|
// Resty client instance.
|
|
//
|
|
// client.SetPathParams(map[string]string{
|
|
// "userId": "sample@sample.com",
|
|
// "subAccountId": "100002",
|
|
// "path": "groups/developers",
|
|
// })
|
|
//
|
|
// Result:
|
|
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
|
|
// Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details
|
|
//
|
|
// It replaces the value of the key while composing the request URL.
|
|
// The values will be used as they are and will not be escaped.
|
|
//
|
|
// It can be overridden at the request level,
|
|
// see [Request.SetRawPathParam] or [Request.SetRawPathParams]
|
|
func (c *Client) SetRawPathParams(params map[string]string) *Client {
|
|
for p, v := range params {
|
|
c.SetRawPathParam(p, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SetJSONEscapeHTML method enables or disables the HTML escape on JSON marshal.
|
|
// By default, escape HTML is false.
|
|
//
|
|
// NOTE: This option only applies to the standard JSON Marshaller used by Resty.
|
|
//
|
|
// It can be overridden at the request level, see [Client.SetJSONEscapeHTML]
|
|
func (c *Client) SetJSONEscapeHTML(b bool) *Client {
|
|
c.jsonEscapeHTML = b
|
|
return c
|
|
}
|
|
|
|
// SetResponseBodyLimit method sets a maximum body size limit in bytes on response,
|
|
// avoid reading too much data to memory.
|
|
//
|
|
// Client will return [resty.ErrResponseBodyTooLarge] if the body size of the body
|
|
// in the uncompressed response is larger than the limit.
|
|
// Body size limit will not be enforced in the following cases:
|
|
// - ResponseBodyLimit <= 0, which is the default behavior.
|
|
// - [Request.SetOutput] is called to save response data to the file.
|
|
// - "DoNotParseResponse" is set for client or request.
|
|
//
|
|
// It can be overridden at the request level; see [Request.SetResponseBodyLimit]
|
|
func (c *Client) SetResponseBodyLimit(v int) *Client {
|
|
c.ResponseBodyLimit = v
|
|
return c
|
|
}
|
|
|
|
// EnableTrace method enables the Resty client trace for the requests fired from
|
|
// the client using [httptrace.ClientTrace] and provides insights.
|
|
//
|
|
// client := resty.New().EnableTrace()
|
|
//
|
|
// resp, err := client.R().Get("https://httpbin.org/get")
|
|
// fmt.Println("Error:", err)
|
|
// fmt.Println("Trace Info:", resp.Request.TraceInfo())
|
|
//
|
|
// The method [Request.EnableTrace] is also available to get trace info for a single request.
|
|
func (c *Client) EnableTrace() *Client {
|
|
c.trace = true
|
|
return c
|
|
}
|
|
|
|
// DisableTrace method disables the Resty client trace. Refer to [Client.EnableTrace].
|
|
func (c *Client) DisableTrace() *Client {
|
|
c.trace = false
|
|
return c
|
|
}
|
|
|
|
// EnableGenerateCurlOnDebug method enables the generation of CURL commands in the debug log.
|
|
// It works in conjunction with debug mode.
|
|
//
|
|
// NOTE: Use with care.
|
|
// - Potential to leak sensitive data from [Request] and [Response] in the debug log.
|
|
// - Beware of memory usage since the request body is reread.
|
|
func (c *Client) EnableGenerateCurlOnDebug() *Client {
|
|
c.generateCurlOnDebug = true
|
|
return c
|
|
}
|
|
|
|
// DisableGenerateCurlOnDebug method disables the option set by [Client.EnableGenerateCurlOnDebug].
|
|
func (c *Client) DisableGenerateCurlOnDebug() *Client {
|
|
c.generateCurlOnDebug = false
|
|
return c
|
|
}
|
|
|
|
// IsProxySet method returns the true is proxy is set from the Resty client; otherwise
|
|
// false. By default, the proxy is set from the environment variable; refer to [http.ProxyFromEnvironment].
|
|
func (c *Client) IsProxySet() bool {
|
|
return c.proxyURL != nil
|
|
}
|
|
|
|
// GetClient method returns the underlying [http.Client] used by the Resty.
|
|
func (c *Client) GetClient() *http.Client {
|
|
return c.httpClient
|
|
}
|
|
|
|
// Clone returns a clone of the original client.
|
|
//
|
|
// NOTE: Use with care:
|
|
// - Interface values are not deeply cloned. Thus, both the original and the
|
|
// clone will use the same value.
|
|
// - This function is not safe for concurrent use. You should only use this method
|
|
// when you are sure that any other goroutine is not using the client.
|
|
func (c *Client) Clone() *Client {
|
|
// dereference the pointer and copy the value
|
|
cc := *c
|
|
|
|
// lock values should not be copied - thus new values are used.
|
|
cc.afterResponseLock = &sync.RWMutex{}
|
|
cc.udBeforeRequestLock = &sync.RWMutex{}
|
|
return &cc
|
|
}
|
|
|
|
func (c *Client) executeBefore(req *Request) error {
|
|
// Lock the user-defined pre-request hooks.
|
|
c.udBeforeRequestLock.RLock()
|
|
defer c.udBeforeRequestLock.RUnlock()
|
|
|
|
// Lock the post-request hooks.
|
|
c.afterResponseLock.RLock()
|
|
defer c.afterResponseLock.RUnlock()
|
|
|
|
// Apply Request middleware
|
|
var err error
|
|
|
|
// user defined on before request methods
|
|
// to modify the *resty.Request object
|
|
for _, f := range c.udBeforeRequest {
|
|
if err = f(c, req); err != nil {
|
|
return wrapNoRetryErr(err)
|
|
}
|
|
}
|
|
|
|
// If there is a rate limiter set for this client, the Execute call
|
|
// will return an error if the rate limit is exceeded.
|
|
if req.client.rateLimiter != nil {
|
|
if !req.client.rateLimiter.Allow() {
|
|
return wrapNoRetryErr(ErrRateLimitExceeded)
|
|
}
|
|
}
|
|
|
|
// resty middlewares
|
|
for _, f := range c.beforeRequest {
|
|
if err = f(c, req); err != nil {
|
|
return wrapNoRetryErr(err)
|
|
}
|
|
}
|
|
|
|
if hostHeader := req.Header.Get("Host"); hostHeader != "" {
|
|
req.RawRequest.Host = hostHeader
|
|
}
|
|
|
|
// call pre-request if defined
|
|
if c.preReqHook != nil {
|
|
if err = c.preReqHook(c, req.RawRequest); err != nil {
|
|
return wrapNoRetryErr(err)
|
|
}
|
|
}
|
|
|
|
if err = requestLogger(c, req); err != nil {
|
|
return wrapNoRetryErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Executes method executes the given `Request` object and returns
|
|
// response or error.
|
|
func (c *Client) execute(req *Request) (*Response, error) {
|
|
if err := c.executeBefore(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Time = time.Now()
|
|
resp, err := c.httpClient.Do(req.RawRequest)
|
|
|
|
response := &Response{
|
|
Request: req,
|
|
RawResponse: resp,
|
|
}
|
|
|
|
if err != nil || req.notParseResponse || c.notParseResponse {
|
|
response.setReceivedAt()
|
|
if logErr := responseLogger(c, response); logErr != nil {
|
|
return response, wrapErrors(logErr, err)
|
|
}
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
if !req.isSaveResponse {
|
|
defer closeq(resp.Body)
|
|
body := resp.Body
|
|
|
|
// GitHub #142 & #187
|
|
if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.ContentLength != 0 {
|
|
if _, ok := body.(*gzip.Reader); !ok {
|
|
body, err = gzip.NewReader(body)
|
|
if err != nil {
|
|
err = wrapErrors(responseLogger(c, response), err)
|
|
response.setReceivedAt()
|
|
return response, err
|
|
}
|
|
defer closeq(body)
|
|
}
|
|
}
|
|
|
|
if response.body, err = readAllWithLimit(body, req.responseBodyLimit); err != nil {
|
|
err = wrapErrors(responseLogger(c, response), err)
|
|
response.setReceivedAt()
|
|
return response, err
|
|
}
|
|
|
|
response.size = int64(len(response.body))
|
|
}
|
|
|
|
response.setReceivedAt() // after we read the body
|
|
|
|
// Apply Response middleware
|
|
err = responseLogger(c, response)
|
|
if err != nil {
|
|
return response, wrapNoRetryErr(err)
|
|
}
|
|
|
|
for _, f := range c.afterResponse {
|
|
if err = f(c, response); err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return response, wrapNoRetryErr(err)
|
|
}
|
|
|
|
var ErrResponseBodyTooLarge = errors.New("resty: response body too large")
|
|
|
|
// https://github.com/golang/go/issues/51115
|
|
// [io.LimitedReader] can only return [io.EOF]
|
|
func readAllWithLimit(r io.Reader, maxSize int) ([]byte, error) {
|
|
if maxSize <= 0 {
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
var buf [512]byte // make buf stack allocated
|
|
result := make([]byte, 0, 512)
|
|
total := 0
|
|
for {
|
|
n, err := r.Read(buf[:])
|
|
total += n
|
|
if total > maxSize {
|
|
return nil, ErrResponseBodyTooLarge
|
|
}
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
result = append(result, buf[:n]...)
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
result = append(result, buf[:n]...)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getting TLS client config if not exists then create one
|
|
func (c *Client) tlsConfig() (*tls.Config, error) {
|
|
transport, err := c.Transport()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if transport.TLSClientConfig == nil {
|
|
transport.TLSClientConfig = &tls.Config{}
|
|
}
|
|
return transport.TLSClientConfig, nil
|
|
}
|
|
|
|
// Transport method returns [http.Transport] currently in use or error
|
|
// in case the currently used `transport` is not a [http.Transport].
|
|
//
|
|
// Since v2.8.0 has 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)
|
|
return c
|
|
}
|
|
|
|
// ResponseError is a wrapper that includes the server response with an error.
|
|
// Neither the err nor the response should be nil.
|
|
type ResponseError struct {
|
|
Response *Response
|
|
Err error
|
|
}
|
|
|
|
func (e *ResponseError) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
func (e *ResponseError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// Helper to run errorHooks hooks.
|
|
// It wraps the error in a [ResponseError] if the resp is not nil
|
|
// so hooks can access it.
|
|
func (c *Client) onErrorHooks(req *Request, resp *Response, err error) {
|
|
if err != nil {
|
|
if resp != nil { // wrap with ResponseError
|
|
err = &ResponseError{Response: resp, Err: err}
|
|
}
|
|
for _, h := range c.errorHooks {
|
|
h(req, err)
|
|
}
|
|
} else {
|
|
for _, h := range c.successHooks {
|
|
h(c, resp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper to run panicHooks hooks.
|
|
func (c *Client) onPanicHooks(req *Request, err error) {
|
|
for _, h := range c.panicHooks {
|
|
h(req, err)
|
|
}
|
|
}
|
|
|
|
// Helper to run invalidHooks hooks.
|
|
func (c *Client) onInvalidHooks(req *Request, err error) {
|
|
for _, h := range c.invalidHooks {
|
|
h(req, err)
|
|
}
|
|
}
|
|
|
|
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
// File struct and its methods
|
|
//_______________________________________________________________________
|
|
|
|
// File struct represents file information for multipart request
|
|
type File struct {
|
|
Name string
|
|
ParamName string
|
|
io.Reader
|
|
}
|
|
|
|
// String method returns the string value of current file details
|
|
func (f *File) String() string {
|
|
return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name)
|
|
}
|
|
|
|
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
// MultipartField struct
|
|
//_______________________________________________________________________
|
|
|
|
// MultipartField struct represents the custom data part for a multipart request
|
|
type MultipartField struct {
|
|
Param string
|
|
FileName string
|
|
ContentType string
|
|
io.Reader
|
|
}
|
|
|
|
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"),
|
|
AuthScheme: "Bearer",
|
|
|
|
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,
|
|
createCurlCmd,
|
|
}
|
|
|
|
// user defined request middlewares
|
|
c.udBeforeRequest = []RequestMiddleware{}
|
|
|
|
// default after response middlewares
|
|
c.afterResponse = []ResponseMiddleware{
|
|
parseResponseBody,
|
|
saveResponseIntoFile,
|
|
}
|
|
|
|
return c
|
|
}
|