diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 83380bb35..dda016a45 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
- name: Fail if changes
run: git diff-index --exit-code HEAD
-
+
test:
runs-on: ubuntu-latest
env:
@@ -81,7 +81,7 @@ jobs:
notify-slack:
runs-on: ubuntu-latest
needs: [test]
- if: always() && github.repository == 'linode/linodego' # Run even if integration tests fail and only on main repository
+ if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' # Run even if integration tests fail and only on main repository
steps:
- name: Notify Slack
diff --git a/client.go b/client.go
index 28433b766..9fd3cde08 100644
--- a/client.go
+++ b/client.go
@@ -3,6 +3,8 @@ package linodego
import (
"bytes"
"context"
+ "crypto/tls"
+ "crypto/x509"
"encoding/json"
"fmt"
"io"
@@ -14,13 +16,12 @@ import (
"path/filepath"
"reflect"
"regexp"
+ "runtime"
"strconv"
"strings"
"sync"
"text/template"
"time"
-
- "github.com/go-resty/resty/v2"
)
const (
@@ -49,7 +50,6 @@ const (
APIDefaultCacheExpiration = time.Minute * 15
)
-//nolint:unused
var (
reqLogTemplate = template.Must(template.New("request").Parse(`Sending request:
Method: {{.Method}}
@@ -63,20 +63,35 @@ Headers: {{.Headers}}
Body: {{.Body}}`))
)
+type RequestLog struct {
+ Method string
+ URL string
+ Headers http.Header
+ Body string
+}
+
+type ResponseLog struct {
+ Method string
+ URL string
+ Headers http.Header
+ Body string
+}
+
var envDebug = false
-// Client is a wrapper around the Resty client
+// Client is a wrapper around the http client
type Client struct {
- resty *resty.Client
- userAgent string
- debug bool
- retryConditionals []RetryConditional
+ httpClient *http.Client
+ userAgent string
+ debug bool
pollInterval time.Duration
baseURL string
apiVersion string
apiProto string
+ hostURL string
+ header http.Header
selectedProfile string
loadedProfile string
@@ -87,6 +102,16 @@ type Client struct {
cacheExpiration time.Duration
cachedEntries map[string]clientCacheEntry
cachedEntryLock *sync.RWMutex
+ logger Logger
+ requestLog func(*RequestLog) error
+ onBeforeRequest []func(*http.Request) error
+ onAfterResponse []func(*http.Response) error
+
+ retryConditionals []RetryConditional
+ retryMaxWaitTime time.Duration
+ retryMinWaitTime time.Duration
+ retryAfter RetryAfter
+ retryCount int
}
type EnvDefaults struct {
@@ -103,13 +128,11 @@ type clientCacheEntry struct {
}
type (
- Request = resty.Request
- Response = resty.Response
- Logger = resty.Logger
+ Request = http.Request
+ Response = http.Response
)
func init() {
- // Whether we will enable Resty debugging output
if apiDebug, ok := os.LookupEnv("LINODE_DEBUG"); ok {
if parsed, err := strconv.ParseBool(apiDebug); err == nil {
envDebug = parsed
@@ -123,20 +146,20 @@ func init() {
// SetUserAgent sets a custom user-agent for HTTP requests
func (c *Client) SetUserAgent(ua string) *Client {
c.userAgent = ua
- c.resty.SetHeader("User-Agent", c.userAgent)
+ c.SetHeader("User-Agent", c.userAgent)
return c
}
-type RequestParams struct {
- Body any
+type requestParams struct {
+ Body *bytes.Reader
Response any
}
// Generic helper to execute HTTP requests using the net/http package
//
-// nolint:unused, funlen, gocognit
-func (c *httpClient) doRequest(ctx context.Context, method, url string, params RequestParams) error {
+// nolint:funlen, gocognit, nestif
+func (c *Client) doRequest(ctx context.Context, method, endpoint string, params requestParams, paginationMutator *func(*http.Request) error) error {
var (
req *http.Request
bodyBuffer *bytes.Buffer
@@ -144,18 +167,32 @@ func (c *httpClient) doRequest(ctx context.Context, method, url string, params R
err error
)
- for range httpDefaultRetryCount {
- req, bodyBuffer, err = c.createRequest(ctx, method, url, params)
+ for range c.retryCount {
+ // Reset the body to the start for each retry if it's not nil
+ if params.Body != nil {
+ _, err := params.Body.Seek(0, io.SeekStart)
+ if err != nil {
+ return c.ErrorAndLogf("failed to seek to the start of the body: %v", err.Error())
+ }
+ }
+
+ req, bodyBuffer, err = c.createRequest(ctx, method, endpoint, params)
if err != nil {
return err
}
+ if paginationMutator != nil {
+ if err := (*paginationMutator)(req); err != nil {
+ return c.ErrorAndLogf("failed to mutate before request: %v", err.Error())
+ }
+ }
+
if err = c.applyBeforeRequest(req); err != nil {
return err
}
if c.debug && c.logger != nil {
- c.logRequest(req, method, url, bodyBuffer)
+ c.logRequest(req, method, endpoint, bodyBuffer)
}
processResponse := func() error {
@@ -206,85 +243,91 @@ func (c *httpClient) doRequest(ctx context.Context, method, url string, params R
}
// Sleep for the specified duration before retrying.
- // If retryAfter is 0 (i.e., Retry-After header is not found),
- // no delay is applied.
- time.Sleep(retryAfter)
+ if retryAfter > 0 {
+ waitTime := retryAfter
+
+ // Ensure the wait time is within the defined bounds
+ if waitTime < c.retryMinWaitTime {
+ waitTime = c.retryMinWaitTime
+ } else if waitTime > c.retryMaxWaitTime {
+ waitTime = c.retryMaxWaitTime
+ }
+
+ // Sleep for the calculated duration before retrying
+ time.Sleep(waitTime)
+ }
}
return err
}
-// nolint:unused
-func (c *httpClient) shouldRetry(resp *http.Response, err error) bool {
+func (c *Client) shouldRetry(resp *http.Response, err error) bool {
for _, retryConditional := range c.retryConditionals {
if retryConditional(resp, err) {
+ log.Printf("[INFO] Received error %v - Retrying", err)
return true
}
}
return false
}
-// nolint:unused
-func (c *httpClient) createRequest(ctx context.Context, method, url string, params RequestParams) (*http.Request, *bytes.Buffer, error) {
+func (c *Client) createRequest(ctx context.Context, method, endpoint string, params requestParams) (*http.Request, *bytes.Buffer, error) {
var bodyReader io.Reader
var bodyBuffer *bytes.Buffer
if params.Body != nil {
- bodyBuffer = new(bytes.Buffer)
- if err := json.NewEncoder(bodyBuffer).Encode(params.Body); err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to encode body: %v", err)
- }
- return nil, nil, fmt.Errorf("failed to encode body: %w", err)
+ // Reset the body position to the start before using it
+ _, err := params.Body.Seek(0, io.SeekStart)
+ if err != nil {
+ return nil, nil, c.ErrorAndLogf("failed to seek to the start of the body: %v", err.Error())
}
- bodyReader = bodyBuffer
+
+ bodyReader = params.Body
}
- req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
+ req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s/%s", strings.TrimRight(c.hostURL, "/"),
+ strings.TrimLeft(endpoint, "/")), bodyReader)
if err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to create request: %v", err)
- }
- return nil, nil, fmt.Errorf("failed to create request: %w", err)
+ return nil, nil, c.ErrorAndLogf("failed to create request: %v", err.Error())
}
+ // Set the default headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}
+ // Set additional headers added to the client
+ for name, values := range c.header {
+ for _, value := range values {
+ req.Header.Set(name, value)
+ }
+ }
+
return req, bodyBuffer, nil
}
-// nolint:unused
-func (c *httpClient) applyBeforeRequest(req *http.Request) error {
+func (c *Client) applyBeforeRequest(req *http.Request) error {
for _, mutate := range c.onBeforeRequest {
if err := mutate(req); err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to mutate before request: %v", err)
- }
- return fmt.Errorf("failed to mutate before request: %w", err)
+ return c.ErrorAndLogf("failed to mutate before request: %v", err.Error())
}
}
+
return nil
}
-// nolint:unused
-func (c *httpClient) applyAfterResponse(resp *http.Response) error {
+func (c *Client) applyAfterResponse(resp *http.Response) error {
for _, mutate := range c.onAfterResponse {
if err := mutate(resp); err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to mutate after response: %v", err)
- }
- return fmt.Errorf("failed to mutate after response: %w", err)
+ return c.ErrorAndLogf("failed to mutate after response: %v", err.Error())
}
}
return nil
}
-// nolint:unused
-func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffer *bytes.Buffer) {
+func (c *Client) logRequest(req *http.Request, method, url string, bodyBuffer *bytes.Buffer) {
var reqBody string
if bodyBuffer != nil {
reqBody = bodyBuffer.String()
@@ -292,44 +335,48 @@ func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffe
reqBody = "nil"
}
+ reqLog := &RequestLog{
+ Method: method,
+ URL: url,
+ Headers: req.Header,
+ Body: reqBody,
+ }
+
+ e := c.requestLog(reqLog)
+ if e != nil {
+ _ = c.ErrorAndLogf("failed to mutate after response: %v", e.Error())
+ }
+
var logBuf bytes.Buffer
err := reqLogTemplate.Execute(&logBuf, map[string]interface{}{
- "Method": method,
- "URL": url,
- "Headers": req.Header,
- "Body": reqBody,
+ "Method": reqLog.Method,
+ "URL": reqLog.URL,
+ "Headers": reqLog.Headers,
+ "Body": reqLog.Body,
})
if err == nil {
c.logger.Debugf(logBuf.String())
}
}
-// nolint:unused
-func (c *httpClient) sendRequest(req *http.Request) (*http.Response, error) {
+func (c *Client) sendRequest(req *http.Request) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to send request: %v", err)
- }
- return nil, fmt.Errorf("failed to send request: %w", err)
+ return nil, c.ErrorAndLogf("failed to send request: %w", err)
}
return resp, nil
}
-// nolint:unused
-func (c *httpClient) checkHTTPError(resp *http.Response) error {
- _, err := coupleAPIErrorsHTTP(resp, nil)
+func (c *Client) checkHTTPError(resp *http.Response) error {
+ _, err := coupleAPIErrors(resp, nil)
if err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("received HTTP error: %v", err)
- }
+ _ = c.ErrorAndLogf("received HTTP error: %v", err.Error())
return err
}
return nil
}
-// nolint:unused
-func (c *httpClient) logResponse(resp *http.Response) (*http.Response, error) {
+func (c *Client) logResponse(resp *http.Response) (*http.Response, error) {
var respBody bytes.Buffer
if _, err := io.Copy(&respBody, resp.Body); err != nil {
c.logger.Errorf("failed to read response body: %v", err)
@@ -349,82 +396,31 @@ func (c *httpClient) logResponse(resp *http.Response) (*http.Response, error) {
return resp, nil
}
-// nolint:unused
-func (c *httpClient) decodeResponseBody(resp *http.Response, response interface{}) error {
+func (c *Client) decodeResponseBody(resp *http.Response, response interface{}) error {
if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
- if c.debug && c.logger != nil {
- c.logger.Errorf("failed to decode response: %v", err)
- }
- return fmt.Errorf("failed to decode response: %w", err)
+ return c.ErrorAndLogf("failed to decode response: %v", err.Error())
}
return nil
}
-// R wraps resty's R method
-func (c *Client) R(ctx context.Context) *resty.Request {
- return c.resty.R().
- ExpectContentType("application/json").
- SetHeader("Content-Type", "application/json").
- SetContext(ctx).
- SetError(APIError{})
-}
-
-// SetDebug sets the debug on resty's client
func (c *Client) SetDebug(debug bool) *Client {
c.debug = debug
- c.resty.SetDebug(debug)
return c
}
-// SetLogger allows the user to override the output
-// logger for debug logs.
func (c *Client) SetLogger(logger Logger) *Client {
- c.resty.SetLogger(logger)
-
- return c
-}
-
-//nolint:unused
-func (c *httpClient) httpSetDebug(debug bool) *httpClient {
- c.debug = debug
-
- return c
-}
-
-//nolint:unused
-func (c *httpClient) httpSetLogger(logger httpLogger) *httpClient {
c.logger = logger
return c
}
-// OnBeforeRequest adds a handler to the request body to run before the request is sent
-func (c *Client) OnBeforeRequest(m func(request *Request) error) {
- c.resty.OnBeforeRequest(func(_ *resty.Client, req *resty.Request) error {
- return m(req)
- })
-}
-
-// OnAfterResponse adds a handler to the request body to run before the request is sent
-func (c *Client) OnAfterResponse(m func(response *Response) error) {
- c.resty.OnAfterResponse(func(_ *resty.Client, req *resty.Response) error {
- return m(req)
- })
-}
-
-// nolint:unused
-func (c *httpClient) httpOnBeforeRequest(m func(*http.Request) error) *httpClient {
+func (c *Client) OnBeforeRequest(m func(*http.Request) error) {
c.onBeforeRequest = append(c.onBeforeRequest, m)
-
- return c
}
-// nolint:unused
-func (c *httpClient) httpOnAfterResponse(m func(*http.Response) error) *httpClient {
+func (c *Client) OnAfterResponse(m func(*http.Response) error) {
c.onAfterResponse = append(c.onAfterResponse, m)
-
- return c
}
// UseURL parses the individual components of the given API URL and configures the client
@@ -458,7 +454,6 @@ func (c *Client) UseURL(apiURL string) (*Client, error) {
return c, nil
}
-// SetBaseURL sets the base URL of the Linode v4 API (https://api.linode.com/v4)
func (c *Client) SetBaseURL(baseURL string) *Client {
baseURLPath, _ := url.Parse(baseURL)
@@ -496,51 +491,68 @@ func (c *Client) updateHostURL() {
apiProto = c.apiProto
}
- c.resty.SetBaseURL(
- fmt.Sprintf(
- "%s://%s/%s",
- apiProto,
- baseURL,
- url.PathEscape(apiVersion),
- ),
- )
+ c.hostURL = strings.TrimRight(fmt.Sprintf("%s://%s/%s", apiProto, baseURL, url.PathEscape(apiVersion)), "/")
+}
+
+func (c *Client) Transport() (*http.Transport, error) {
+ if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
+ return transport, nil
+ }
+ return nil, fmt.Errorf("current transport is not an *http.Transport instance")
+}
+
+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{
+ MinVersion: tls.VersionTLS12,
+ }
+ }
+ return transport.TLSClientConfig, nil
}
// SetRootCertificate adds a root certificate to the underlying TLS client config
func (c *Client) SetRootCertificate(path string) *Client {
- c.resty.SetRootCertificate(path)
+ config, err := c.tlsConfig()
+ if err != nil {
+ c.logger.Errorf("%v", err)
+ return c
+ }
+ if config.RootCAs == nil {
+ config.RootCAs = x509.NewCertPool()
+ }
+
+ config.RootCAs.AppendCertsFromPEM([]byte(path))
return c
}
// SetToken sets the API token for all requests from this client
// Only necessary if you haven't already provided the http client to NewClient() configured with the token.
func (c *Client) SetToken(token string) *Client {
- c.resty.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token))
+ c.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token))
return c
}
// SetRetries adds retry conditions for "Linode Busy." errors and 429s.
func (c *Client) SetRetries() *Client {
c.
- addRetryConditional(linodeBusyRetryCondition).
- addRetryConditional(tooManyRequestsRetryCondition).
- addRetryConditional(serviceUnavailableRetryCondition).
- addRetryConditional(requestTimeoutRetryCondition).
- addRetryConditional(requestGOAWAYRetryCondition).
- addRetryConditional(requestNGINXRetryCondition).
+ AddRetryCondition(LinodeBusyRetryCondition).
+ AddRetryCondition(TooManyRequestsRetryCondition).
+ AddRetryCondition(ServiceUnavailableRetryCondition).
+ AddRetryCondition(RequestTimeoutRetryCondition).
+ AddRetryCondition(RequestGOAWAYRetryCondition).
+ AddRetryCondition(RequestNGINXRetryCondition).
SetRetryMaxWaitTime(APIRetryMaxWaitTime)
- configureRetries(c)
+ ConfigureRetries(c)
return c
}
// AddRetryCondition adds a RetryConditional function to the Client
func (c *Client) AddRetryCondition(retryCondition RetryConditional) *Client {
- c.resty.AddRetryCondition(resty.RetryConditionFunc(retryCondition))
- return c
-}
-
-func (c *Client) addRetryConditional(retryConditional RetryConditional) *Client {
- c.retryConditionals = append(c.retryConditionals, retryConditional)
+ c.retryConditionals = append(c.retryConditionals, retryCondition)
return c
}
@@ -655,26 +667,26 @@ func (c *Client) UseCache(value bool) {
// SetRetryMaxWaitTime sets the maximum delay before retrying a request.
func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {
- c.resty.SetRetryMaxWaitTime(maxWaitTime)
+ c.retryMaxWaitTime = maxWaitTime
return c
}
// SetRetryWaitTime sets the default (minimum) delay before retrying a request.
func (c *Client) SetRetryWaitTime(minWaitTime time.Duration) *Client {
- c.resty.SetRetryWaitTime(minWaitTime)
+ c.retryMinWaitTime = minWaitTime
return c
}
// SetRetryAfter sets the callback function to be invoked with a failed request
// to determine wben it should be retried.
func (c *Client) SetRetryAfter(callback RetryAfter) *Client {
- c.resty.SetRetryAfter(resty.RetryAfterFunc(callback))
+ c.retryAfter = callback
return c
}
// SetRetryCount sets the maximum retry attempts before aborting.
func (c *Client) SetRetryCount(count int) *Client {
- c.resty.SetRetryCount(count)
+ c.retryCount = count
return c
}
@@ -695,13 +707,29 @@ func (c *Client) GetPollDelay() time.Duration {
// client.
// NOTE: Some headers may be overridden by the individual request functions.
func (c *Client) SetHeader(name, value string) {
- c.resty.SetHeader(name, value)
+ if c.header == nil {
+ c.header = make(http.Header) // Initialize header if nil
+ }
+ c.header.Set(name, value)
+}
+
+func (c *Client) onRequestLog(rl func(*RequestLog) error) *Client {
+ if c.requestLog != nil {
+ c.logger.Warnf("Overwriting an existing on-request-log callback from=%s to=%s",
+ functionName(c.requestLog), functionName(rl))
+ }
+ c.requestLog = rl
+ return c
+}
+
+func functionName(i interface{}) string {
+ return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func (c *Client) enableLogSanitization() *Client {
- c.resty.OnRequestLog(func(r *resty.RequestLog) error {
+ c.onRequestLog(func(r *RequestLog) error {
// masking authorization header
- r.Header.Set("Authorization", "Bearer *******************************")
+ r.Headers.Set("Authorization", "Bearer *******************************")
return nil
})
@@ -709,22 +737,36 @@ func (c *Client) enableLogSanitization() *Client {
}
// NewClient factory to create new Client struct
+// nolint:funlen
func NewClient(hc *http.Client) (client Client) {
if hc != nil {
- client.resty = resty.NewWithClient(hc)
+ client.httpClient = hc
} else {
- client.resty = resty.New()
+ client.httpClient = &http.Client{}
+ }
+
+ // Ensure that the Header map is not nil
+ if client.httpClient.Transport == nil {
+ client.httpClient.Transport = &http.Transport{}
}
client.shouldCache = true
client.cacheExpiration = APIDefaultCacheExpiration
client.cachedEntries = make(map[string]clientCacheEntry)
client.cachedEntryLock = &sync.RWMutex{}
+ client.configProfiles = make(map[string]ConfigProfile)
+
+ const (
+ retryMinWaitDuration = 100 * time.Millisecond
+ retryMaxWaitDuration = 2 * time.Second
+ )
+
+ client.retryMinWaitTime = retryMinWaitDuration
+ client.retryMaxWaitTime = retryMaxWaitDuration
client.SetUserAgent(DefaultUserAgent)
baseURL, baseURLExists := os.LookupEnv(APIHostVar)
-
if baseURLExists {
client.SetBaseURL(baseURL)
}
@@ -736,7 +778,6 @@ func NewClient(hc *http.Client) (client Client) {
}
certPath, certPathExists := os.LookupEnv(APIHostCert)
-
if certPathExists {
cert, err := os.ReadFile(filepath.Clean(certPath))
if err != nil {
@@ -754,6 +795,7 @@ func NewClient(hc *http.Client) (client Client) {
SetRetryWaitTime(APISecondsPerPoll * time.Second).
SetPollDelay(APISecondsPerPoll * time.Second).
SetRetries().
+ SetLogger(createLogger()).
SetDebug(envDebug).
enableLogSanitization()
@@ -879,3 +921,10 @@ func generateListCacheURL(endpoint string, opts *ListOptions) (string, error) {
return fmt.Sprintf("%s:%s", endpoint, hashedOpts), nil
}
+
+func (c *Client) ErrorAndLogf(format string, args ...interface{}) error {
+ if c.debug && c.logger != nil {
+ c.logger.Errorf(format, args...)
+ }
+ return fmt.Errorf(format, args...)
+}
diff --git a/client_http.go b/client_http.go
deleted file mode 100644
index 7f16362c5..000000000
--- a/client_http.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package linodego
-
-import (
- "net/http"
- "sync"
- "time"
-)
-
-// Client is a wrapper around the Resty client
-//
-//nolint:unused
-type httpClient struct {
- //nolint:unused
- httpClient *http.Client
- //nolint:unused
- userAgent string
- //nolint:unused
- debug bool
- //nolint:unused
- retryConditionals []httpRetryConditional
- //nolint:unused
- retryAfter httpRetryAfter
-
- //nolint:unused
- pollInterval time.Duration
-
- //nolint:unused
- baseURL string
- //nolint:unused
- apiVersion string
- //nolint:unused
- apiProto string
- //nolint:unused
- selectedProfile string
- //nolint:unused
- loadedProfile string
-
- //nolint:unused
- configProfiles map[string]ConfigProfile
-
- // Fields for caching endpoint responses
- //nolint:unused
- shouldCache bool
- //nolint:unused
- cacheExpiration time.Duration
- //nolint:unused
- cachedEntries map[string]clientCacheEntry
- //nolint:unused
- cachedEntryLock *sync.RWMutex
- //nolint:unused
- logger httpLogger
- //nolint:unused
- onBeforeRequest []func(*http.Request) error
- //nolint:unused
- onAfterResponse []func(*http.Response) error
-}
diff --git a/client_test.go b/client_test.go
index 93b133f97..6af453695 100644
--- a/client_test.go
+++ b/client_test.go
@@ -5,6 +5,8 @@ import (
"context"
"errors"
"fmt"
+ "github.com/jarcoal/httpmock"
+ "github.com/linode/linodego/internal/testutil"
"net/http"
"net/http/httptest"
"reflect"
@@ -12,8 +14,6 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "github.com/jarcoal/httpmock"
- "github.com/linode/linodego/internal/testutil"
)
func TestClient_SetAPIVersion(t *testing.T) {
@@ -33,39 +33,39 @@ func TestClient_SetAPIVersion(t *testing.T) {
client := NewClient(nil)
- if client.resty.BaseURL != defaultURL {
- t.Fatal(cmp.Diff(client.resty.BaseURL, defaultURL))
+ if client.hostURL != defaultURL {
+ t.Fatal(cmp.Diff(client.hostURL, defaultURL))
}
client.SetBaseURL(baseURL)
client.SetAPIVersion(apiVersion)
- if client.resty.BaseURL != expectedHost {
- t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost))
+ if client.hostURL != expectedHost {
+ t.Fatal(cmp.Diff(client.hostURL, expectedHost))
}
// Ensure setting twice does not cause conflicts
client.SetBaseURL(updatedBaseURL)
client.SetAPIVersion(updatedAPIVersion)
- if client.resty.BaseURL != updatedExpectedHost {
- t.Fatal(cmp.Diff(client.resty.BaseURL, updatedExpectedHost))
+ if client.hostURL != updatedExpectedHost {
+ t.Fatal(cmp.Diff(client.hostURL, updatedExpectedHost))
}
// Revert
client.SetBaseURL(baseURL)
client.SetAPIVersion(apiVersion)
- if client.resty.BaseURL != expectedHost {
- t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost))
+ if client.hostURL != expectedHost {
+ t.Fatal(cmp.Diff(client.hostURL, expectedHost))
}
// Custom protocol
client.SetBaseURL(protocolBaseURL)
client.SetAPIVersion(protocolAPIVersion)
- if client.resty.BaseURL != protocolExpectedHost {
- t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost))
+ if client.hostURL != protocolExpectedHost {
+ t.Fatal(cmp.Diff(client.hostURL, expectedHost))
}
}
@@ -107,7 +107,7 @@ func TestClient_NewFromEnvToken(t *testing.T) {
t.Fatal(err)
}
- if client.resty.Header.Get("Authorization") != "Bearer blah" {
+ if client.header.Get("Authorization") != "Bearer blah" {
t.Fatal("token not found in auth header: blah")
}
}
@@ -171,12 +171,12 @@ func TestDebugLogSanitization(t *testing.T) {
logger.L.SetOutput(&lgr)
mockClient.SetDebug(true)
- if !mockClient.resty.Debug {
+ if !mockClient.debug {
t.Fatal("debug should be enabled")
}
mockClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", plainTextToken))
- if mockClient.resty.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", plainTextToken) {
+ if mockClient.header.Get("Authorization") != fmt.Sprintf("Bearer %s", plainTextToken) {
t.Fatal("token not found in auth header")
}
@@ -204,22 +204,25 @@ func TestDebugLogSanitization(t *testing.T) {
func TestDoRequest_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write([]byte(`{"message":"success"}`))
+ if r.URL.Path == "/v4/foo/bar" {
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"message":"success"}`))
+ } else {
+ http.NotFound(w, r)
+ }
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
- params := RequestParams{
+ params := requestParams{
Response: &map[string]string{},
}
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, params)
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", params, nil) // Pass only the endpoint
if err != nil {
t.Fatal(cmp.Diff(nil, err))
}
@@ -231,31 +234,11 @@ func TestDoRequest_Success(t *testing.T) {
}
}
-func TestDoRequest_FailedEncodeBody(t *testing.T) {
- client := &httpClient{
- httpClient: http.DefaultClient,
- }
-
- params := RequestParams{
- Body: map[string]interface{}{
- "invalid": func() {},
- },
- }
-
- err := client.doRequest(context.Background(), http.MethodPost, "http://example.com", params)
- expectedErr := "failed to encode body"
- if err == nil || !strings.Contains(err.Error(), expectedErr) {
- t.Fatalf("expected error %q, got: %v", expectedErr, err)
- }
-}
-
func TestDoRequest_FailedCreateRequest(t *testing.T) {
- client := &httpClient{
- httpClient: http.DefaultClient,
- }
+ client := NewClient(nil)
- // Create a request with an invalid URL to simulate a request creation failure
- err := client.doRequest(context.Background(), http.MethodGet, "http://invalid url", RequestParams{})
+ // Create a request with an invalid method to simulate a request creation failure
+ err := client.doRequest(context.Background(), "bad method", "/foo/bar", requestParams{}, nil)
expectedErr := "failed to create request"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
@@ -264,26 +247,28 @@ func TestDoRequest_FailedCreateRequest(t *testing.T) {
func TestDoRequest_Non2xxStatusCode(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
- http.Error(w, "error", http.StatusInternalServerError)
+ http.Error(w, "error", http.StatusInternalServerError) // Simulate a 500 Internal Server Error
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", requestParams{}, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
- httpError, ok := err.(Error)
+
+ httpError, ok := err.(*Error)
if !ok {
t.Fatalf("expected error to be of type Error, got %T", err)
}
+
if httpError.Code != http.StatusInternalServerError {
t.Fatalf("expected status code %d, got %d", http.StatusInternalServerError, httpError.Code)
}
+
if !strings.Contains(httpError.Message, "error") {
t.Fatalf("expected error message to contain %q, got %v", "error", httpError.Message)
}
@@ -293,21 +278,21 @@ func TestDoRequest_FailedDecodeResponse(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write([]byte(`invalid json`))
+ _, _ = w.Write([]byte(`invalid json`)) // Simulate invalid JSON
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
- params := RequestParams{
+ params := requestParams{
Response: &map[string]string{},
}
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, params)
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", params, nil)
expectedErr := "failed to decode response"
+
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
@@ -325,24 +310,21 @@ func TestDoRequest_BeforeRequestSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
- // Define a mutator that successfully modifies the request
mutator := func(req *http.Request) error {
req.Header.Set("X-Custom-Header", "CustomValue")
return nil
}
- client.httpOnBeforeRequest(mutator)
+ client.OnBeforeRequest(mutator)
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", requestParams{}, nil)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
- // Check if the header was successfully added to the captured request
if reqHeader := capturedRequest.Header.Get("X-Custom-Header"); reqHeader != "CustomValue" {
t.Fatalf("expected X-Custom-Header to be set to CustomValue, got: %v", reqHeader)
}
@@ -357,18 +339,18 @@ func TestDoRequest_BeforeRequestError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
mutator := func(req *http.Request) error {
return errors.New("mutator error")
}
- client.httpOnBeforeRequest(mutator)
+ client.OnBeforeRequest(mutator)
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", requestParams{}, nil)
expectedErr := "failed to mutate before request"
+
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
@@ -387,23 +369,21 @@ func TestDoRequest_AfterResponseSuccess(t *testing.T) {
tr := &testRoundTripper{
Transport: server.Client().Transport,
}
- client := &httpClient{
- httpClient: &http.Client{Transport: tr},
- }
+ client := NewClient(&http.Client{Transport: tr})
+ client.SetBaseURL(server.URL)
mutator := func(resp *http.Response) error {
resp.Header.Set("X-Modified-Header", "ModifiedValue")
return nil
}
- client.httpOnAfterResponse(mutator)
+ client.OnAfterResponse(mutator)
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", requestParams{}, nil)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
- // Check if the header was successfully added to the response
if respHeader := tr.Response.Header.Get("X-Modified-Header"); respHeader != "ModifiedValue" {
t.Fatalf("expected X-Modified-Header to be set to ModifiedValue, got: %v", respHeader)
}
@@ -418,17 +398,16 @@ func TestDoRequest_AfterResponseError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
- client := &httpClient{
- httpClient: server.Client(),
- }
+ client := NewClient(server.Client())
+ client.SetBaseURL(server.URL)
mutator := func(resp *http.Response) error {
return errors.New("mutator error")
}
- client.httpOnAfterResponse(mutator)
+ client.OnAfterResponse(mutator)
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
+ err := client.doRequest(context.Background(), http.MethodGet, "/foo/bar", requestParams{}, nil)
expectedErr := "failed to mutate after response"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
@@ -440,11 +419,9 @@ func TestDoRequestLogging_Success(t *testing.T) {
logger := createLogger()
logger.l.SetOutput(&logBuffer) // Redirect log output to buffer
- client := &httpClient{
- httpClient: http.DefaultClient,
- debug: true,
- logger: logger,
- }
+ client := NewClient(nil)
+ client.SetDebug(true)
+ client.SetLogger(logger)
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -452,13 +429,14 @@ func TestDoRequestLogging_Success(t *testing.T) {
_, _ = w.Write([]byte(`{"message":"success"}`))
}
server := httptest.NewServer(http.HandlerFunc(handler))
+ client.SetBaseURL(server.URL)
defer server.Close()
- params := RequestParams{
+ params := requestParams{
Response: &map[string]string{},
}
- err := client.doRequest(context.Background(), http.MethodGet, server.URL, params)
+ err := client.doRequest(context.Background(), http.MethodGet, server.URL, params, nil)
if err != nil {
t.Fatal(cmp.Diff(nil, err))
}
@@ -467,8 +445,8 @@ func TestDoRequestLogging_Success(t *testing.T) {
logInfoWithoutTimestamps := removeTimestamps(logInfo)
// Expected logs with templates filled in
- expectedRequestLog := "DEBUG RESTY Sending request:\nMethod: GET\nURL: " + server.URL + "\nHeaders: map[Accept:[application/json] Content-Type:[application/json]]\nBody: "
- expectedResponseLog := "DEBUG RESTY Received response:\nStatus: 200 OK\nHeaders: map[Content-Length:[21] Content-Type:[text/plain; charset=utf-8]]\nBody: {\"message\":\"success\"}"
+ expectedRequestLog := "DEBUG Sending request:\nMethod: GET\nURL: " + server.URL + "\nHeaders: map[Accept:[application/json] Authorization:[Bearer *******************************] Content-Type:[application/json] User-Agent:[linodego/dev https://github.com/linode/linodego]]\nBody: "
+ expectedResponseLog := "DEBUG Received response:\nStatus: 200 OK\nHeaders: map[Content-Length:[21] Content-Type:[text/plain; charset=utf-8]]\nBody: {\"message\":\"success\"}"
if !strings.Contains(logInfo, expectedRequestLog) {
t.Fatalf("expected log %q not found in logs", expectedRequestLog)
@@ -478,31 +456,52 @@ func TestDoRequestLogging_Success(t *testing.T) {
}
}
+// func TestDoRequestLogging_Error(t *testing.T) {
+// var logBuffer bytes.Buffer
+// logger := createLogger()
+// logger.l.SetOutput(&logBuffer) // Redirect log output to buffer
+//
+// client := NewClient(nil)
+// client.SetDebug(true)
+// client.SetLogger(logger)
+//
+// params := requestParams{
+// Body: map[string]interface{}{
+// "invalid": func() {},
+// },
+// }
+//
+// err := client.doRequest(context.Background(), http.MethodPost, "/foo/bar", params, nil)
+// expectedErr := "failed to read body: params.Body is not a *bytes.Reader"
+// if err == nil || !strings.Contains(err.Error(), expectedErr) {
+// t.Fatalf("expected error %q, got: %v", expectedErr, err)
+// }
+//
+// logInfo := logBuffer.String()
+// expectedLog := "ERROR failed to read body: params.Body is not a *bytes.Reader"
+//
+// if !strings.Contains(logInfo, expectedLog) {
+// t.Fatalf("expected log %q not found in logs", expectedLog)
+// }
+// }
func TestDoRequestLogging_Error(t *testing.T) {
var logBuffer bytes.Buffer
logger := createLogger()
logger.l.SetOutput(&logBuffer) // Redirect log output to buffer
- client := &httpClient{
- httpClient: http.DefaultClient,
- debug: true,
- logger: logger,
- }
-
- params := RequestParams{
- Body: map[string]interface{}{
- "invalid": func() {},
- },
- }
+ client := NewClient(nil)
+ client.SetDebug(true)
+ client.SetLogger(logger)
- err := client.doRequest(context.Background(), http.MethodPost, "http://example.com", params)
- expectedErr := "failed to encode body"
+ // Create a request with an invalid method to simulate a request creation failure
+ err := client.doRequest(context.Background(), "bad method", "/foo/bar", requestParams{}, nil)
+ expectedErr := "failed to create request"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
logInfo := logBuffer.String()
- expectedLog := "ERROR RESTY failed to encode body"
+ expectedLog := "ERROR failed to create request"
if !strings.Contains(logInfo, expectedLog) {
t.Fatalf("expected log %q not found in logs", expectedLog)
diff --git a/config_test.go b/config_test.go
index b4b3db418..628cc1415 100644
--- a/config_test.go
+++ b/config_test.go
@@ -42,11 +42,11 @@ func TestConfig_LoadWithDefaults(t *testing.T) {
expectedURL := "https://api.cool.linode.com/v4beta"
- if client.resty.BaseURL != expectedURL {
- t.Fatalf("mismatched host url: %s != %s", client.resty.BaseURL, expectedURL)
+ if client.hostURL != expectedURL {
+ t.Fatalf("mismatched host url: %s != %s", client.hostURL, expectedURL)
}
- if client.resty.Header.Get("Authorization") != "Bearer "+p.APIToken {
+ if client.header.Get("Authorization") != "Bearer "+p.APIToken {
t.Fatalf("token not found in auth header: %s", p.APIToken)
}
}
@@ -88,11 +88,11 @@ func TestConfig_OverrideDefaults(t *testing.T) {
expectedURL := "https://api.cool.linode.com/v4"
- if client.resty.BaseURL != expectedURL {
- t.Fatalf("mismatched host url: %s != %s", client.resty.BaseURL, expectedURL)
+ if client.hostURL != expectedURL {
+ t.Fatalf("mismatched host url: %s != %s", client.hostURL, expectedURL)
}
- if client.resty.Header.Get("Authorization") != "Bearer "+p.APIToken {
+ if client.header.Get("Authorization") != "Bearer "+p.APIToken {
t.Fatalf("token not found in auth header: %s", p.APIToken)
}
}
@@ -124,7 +124,7 @@ func TestConfig_NoDefaults(t *testing.T) {
t.Fatalf("mismatched api token: %s != %s", p.APIToken, "mytoken")
}
- if client.resty.Header.Get("Authorization") != "Bearer "+p.APIToken {
+ if client.header.Get("Authorization") != "Bearer "+p.APIToken {
t.Fatalf("token not found in auth header: %s", p.APIToken)
}
}
diff --git a/errors.go b/errors.go
index be15c0146..40b1852d9 100644
--- a/errors.go
+++ b/errors.go
@@ -1,15 +1,13 @@
package linodego
import (
- "encoding/json"
+ "bytes"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strings"
-
- "github.com/go-resty/resty/v2"
)
const (
@@ -48,74 +46,43 @@ type APIError struct {
Errors []APIErrorReason `json:"errors"`
}
-// String returns the error reason in a formatted string
-func (r APIErrorReason) String() string {
- return fmt.Sprintf("[%s] %s", r.Field, r.Reason)
-}
-
-func coupleAPIErrors(r *resty.Response, err error) (*resty.Response, error) {
+//nolint:nestif
+func coupleAPIErrors(resp *http.Response, err error) (*http.Response, error) {
if err != nil {
- // an error was raised in go code, no need to check the resty Response
return nil, NewError(err)
}
- if r.Error() == nil {
- // no error in the resty Response
- return r, nil
- }
-
- // handle the resty Response errors
-
- // Check that response is of the correct content-type before unmarshalling
- expectedContentType := r.Request.Header.Get("Accept")
- responseContentType := r.Header().Get("Content-Type")
-
- // If the upstream Linode API server being fronted fails to respond to the request,
- // the http server will respond with a default "Bad Gateway" page with Content-Type
- // "text/html".
- if r.StatusCode() == http.StatusBadGateway && responseContentType == "text/html" { //nolint:goconst
- return nil, Error{Code: http.StatusBadGateway, Message: http.StatusText(http.StatusBadGateway)}
- }
-
- if responseContentType != expectedContentType {
- msg := fmt.Sprintf(
- "Unexpected Content-Type: Expected: %v, Received: %v\nResponse body: %s",
- expectedContentType,
- responseContentType,
- string(r.Body()),
- )
-
- return nil, Error{Code: r.StatusCode(), Message: msg}
- }
-
- apiError, ok := r.Error().(*APIError)
- if !ok || (ok && len(apiError.Errors) == 0) {
- return r, nil
+ if resp == nil {
+ return nil, NewError(fmt.Errorf("response is nil"))
}
- return nil, NewError(r)
-}
-
-//nolint:unused
-func coupleAPIErrorsHTTP(resp *http.Response, err error) (*http.Response, error) {
- if err != nil {
- // an error was raised in go code, no need to check the http.Response
- return nil, NewError(err)
- }
-
- if resp == nil || resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// Check that response is of the correct content-type before unmarshalling
- expectedContentType := resp.Request.Header.Get("Accept")
+ expectedContentType := ""
+ if resp.Request != nil && resp.Request.Header != nil {
+ expectedContentType = resp.Request.Header.Get("Accept")
+ }
+
responseContentType := resp.Header.Get("Content-Type")
// If the upstream server fails to respond to the request,
- // the http server will respond with a default error page with Content-Type "text/html".
- if resp.StatusCode == http.StatusBadGateway && responseContentType == "text/html" { //nolint:goconst
- return nil, Error{Code: http.StatusBadGateway, Message: http.StatusText(http.StatusBadGateway)}
+ // the HTTP server will respond with a default error page with Content-Type "text/html".
+ if resp.StatusCode == http.StatusBadGateway && responseContentType == "text/html" {
+ return nil, &Error{Code: http.StatusBadGateway, Message: http.StatusText(http.StatusBadGateway), Response: resp}
}
if responseContentType != expectedContentType {
- bodyBytes, _ := io.ReadAll(resp.Body)
+ if resp.Body == nil {
+ return nil, NewError(fmt.Errorf("response body is nil"))
+ }
+
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, NewError(fmt.Errorf("failed to read response body: %w", err))
+ }
+
+ resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+
msg := fmt.Sprintf(
"Unexpected Content-Type: Expected: %v, Received: %v\nResponse body: %s",
expectedContentType,
@@ -123,11 +90,12 @@ func coupleAPIErrorsHTTP(resp *http.Response, err error) (*http.Response, error)
string(bodyBytes),
)
- return nil, Error{Code: resp.StatusCode, Message: msg}
+ return nil, &Error{Code: resp.StatusCode, Message: msg}
}
- var apiError APIError
- if err := json.NewDecoder(resp.Body).Decode(&apiError); err != nil {
+ // Must check if there is no list of reasons in the error before making a call to NewError
+ apiError, ok := getAPIError(resp)
+ if !ok {
return nil, NewError(fmt.Errorf("failed to decode response body: %w", err))
}
@@ -135,10 +103,9 @@ func coupleAPIErrorsHTTP(resp *http.Response, err error) (*http.Response, error)
return resp, nil
}
- return nil, Error{Code: resp.StatusCode, Message: apiError.Errors[0].String()}
+ return nil, NewError(resp)
}
- // no error in the http.Response
return resp, nil
}
@@ -171,7 +138,7 @@ func (err Error) Is(target error) bool {
// - ErrorFromString (1) from a string
// - ErrorFromError (2) for an error
// - ErrorFromStringer (3) for a Stringer
-// - HTTP Status Codes (100-600) for a resty.Response object
+// - HTTP Status Codes (100-600) for a http.Response object
func NewError(err any) *Error {
if err == nil {
return nil
@@ -180,17 +147,17 @@ func NewError(err any) *Error {
switch e := err.(type) {
case *Error:
return e
- case *resty.Response:
- apiError, ok := e.Error().(*APIError)
+ case *http.Response:
+ apiError, ok := getAPIError(e)
if !ok {
- return &Error{Code: ErrorUnsupported, Message: "Unexpected Resty Error Response, no error"}
+ return &Error{Code: ErrorUnsupported, Message: "Unexpected HTTP Error Response, no error"}
}
return &Error{
- Code: e.RawResponse.StatusCode,
+ Code: e.StatusCode,
Message: apiError.Error(),
- Response: e.RawResponse,
+ Response: e,
}
case error:
return &Error{Code: ErrorFromError, Message: e.Error()}
diff --git a/errors_test.go b/errors_test.go
index 68428fcd6..45ed16b7e 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -7,11 +7,13 @@ import (
"errors"
"fmt"
"io"
+ "io/ioutil"
"net/http"
"net/http/httptest"
+ "strconv"
+ "strings"
"testing"
- "github.com/go-resty/resty/v2"
"github.com/google/go-cmp/cmp"
)
@@ -27,10 +29,10 @@ func (e testError) Error() string {
return string(e)
}
-func restyError(reason, field string) *resty.Response {
+func httpError(reason, field string) *http.Response {
var reasons []APIErrorReason
- // allow for an empty reasons
+ // Allow for an empty reasons
if reason != "" && field != "" {
reasons = append(reasons, APIErrorReason{
Reason: reason,
@@ -38,15 +40,18 @@ func restyError(reason, field string) *resty.Response {
})
}
- return &resty.Response{
- RawResponse: &http.Response{
- StatusCode: 500,
- },
- Request: &resty.Request{
- Error: &APIError{
- Errors: reasons,
- },
- },
+ apiError := &APIError{
+ Errors: reasons,
+ }
+
+ body, err := json.Marshal(apiError)
+ if err != nil {
+ panic("Failed to marshal APIError")
+ }
+
+ return &http.Response{
+ StatusCode: 500,
+ Body: ioutil.NopCloser(bytes.NewReader(body)),
}
}
@@ -73,12 +78,12 @@ func TestNewError(t *testing.T) {
t.Error("Error should be itself")
}
- if err := NewError(&resty.Response{Request: &resty.Request{}}); err.Message != "Unexpected Resty Error Response, no error" {
- t.Error("Unexpected Resty Error Response, no error")
+ if err := NewError(&http.Response{Request: &http.Request{}}); err.Message != "Unexpected HTTP Error Response, no error" {
+ t.Error("Unexpected HTTP Error Response, no error")
}
- if err := NewError(restyError("testreason", "testfield")); err.Message != "[testfield] testreason" {
- t.Error("rest response error should should be set")
+ if err := NewError(httpError("testreason", "testfield")); err.Message != "[testfield] testreason" {
+ t.Error("http response error should should be set")
}
if err := NewError("stringerror"); err.Message != "stringerror" || err.Code != ErrorFromString {
@@ -119,98 +124,6 @@ func TestCoupleAPIErrors(t *testing.T) {
}
})
- t.Run("resty 500 response error with reasons", func(t *testing.T) {
- if _, err := coupleAPIErrors(restyError("testreason", "testfield"), nil); err.Error() != "[500] [testfield] testreason" {
- t.Error("resty error should return with proper format [code] [field] reason")
- }
- })
-
- t.Run("resty 500 response error without reasons", func(t *testing.T) {
- if _, err := coupleAPIErrors(restyError("", ""), nil); err != nil {
- t.Error("resty error with no reasons should return no error")
- }
- })
-
- t.Run("resty response with nil error", func(t *testing.T) {
- emptyErr := &resty.Response{
- RawResponse: &http.Response{
- StatusCode: 500,
- },
- Request: &resty.Request{
- Error: nil,
- },
- }
- if _, err := coupleAPIErrors(emptyErr, nil); err != nil {
- t.Error("resty error with no reasons should return no error")
- }
- })
-
- t.Run("generic html error", func(t *testing.T) {
- rawResponse := `
-
500 Internal Server Error
-
-500 Internal Server Error
-
nginx
-
-`
- route := "/v4/linode/instances/123"
- ts, client := createTestServer(http.MethodGet, route, "text/html", rawResponse, http.StatusInternalServerError)
- // client.SetDebug(true)
- defer ts.Close()
-
- expectedError := Error{
- Code: http.StatusInternalServerError,
- Message: "Unexpected Content-Type: Expected: application/json, Received: text/html\nResponse body: " + rawResponse,
- }
-
- _, err := coupleAPIErrors(client.R(context.Background()).SetResult(&Instance{}).Get(ts.URL + route))
- if diff := cmp.Diff(expectedError, err); diff != "" {
- t.Errorf("expected error to match but got diff:\n%s", diff)
- }
- })
-
- t.Run("bad gateway error", func(t *testing.T) {
- rawResponse := []byte(`
-502 Bad Gateway
-
-502 Bad Gateway
-
nginx
-
-`)
- buf := io.NopCloser(bytes.NewBuffer(rawResponse))
-
- resp := &resty.Response{
- Request: &resty.Request{
- Error: errors.New("Bad Gateway"),
- },
- RawResponse: &http.Response{
- Header: http.Header{
- "Content-Type": []string{"text/html"},
- },
- StatusCode: http.StatusBadGateway,
- Body: buf,
- },
- }
-
- expectedError := Error{
- Code: http.StatusBadGateway,
- Message: http.StatusText(http.StatusBadGateway),
- }
-
- if _, err := coupleAPIErrors(resp, nil); !cmp.Equal(err, expectedError) {
- t.Errorf("expected error %#v to match error %#v", err, expectedError)
- }
- })
-}
-
-func TestCoupleAPIErrorsHTTP(t *testing.T) {
- t.Run("not nil error generates error", func(t *testing.T) {
- err := errors.New("test")
- if _, err := coupleAPIErrorsHTTP(nil, err); !cmp.Equal(err, NewError(err)) {
- t.Errorf("expect a not nil error to be returned as an Error")
- }
- })
-
t.Run("http 500 response error with reasons", func(t *testing.T) {
// Create the simulated HTTP response with a 500 status and a JSON body containing the error details
apiError := APIError{
@@ -228,7 +141,7 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
Request: &http.Request{Header: http.Header{"Accept": []string{"application/json"}}},
}
- _, err := coupleAPIErrorsHTTP(resp, nil)
+ _, err := coupleAPIErrors(resp, nil)
expectedMessage := "[500] [testfield] testreason"
if err == nil || err.Error() != expectedMessage {
t.Errorf("expected error message %q, got: %v", expectedMessage, err)
@@ -250,7 +163,7 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
Request: &http.Request{Header: http.Header{"Accept": []string{"application/json"}}},
}
- _, err := coupleAPIErrorsHTTP(resp, nil)
+ _, err := coupleAPIErrors(resp, nil)
if err != nil {
t.Error("http error with no reasons should return no error")
}
@@ -265,7 +178,7 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
Request: &http.Request{Header: http.Header{"Accept": []string{"application/json"}}},
}
- _, err := coupleAPIErrorsHTTP(resp, nil)
+ _, err := coupleAPIErrors(resp, nil)
if err != nil {
t.Error("http error with no reasons should return no error")
}
@@ -288,15 +201,10 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
}))
defer ts.Close()
- client := &httpClient{
+ client := &Client{
httpClient: ts.Client(),
}
- expectedError := Error{
- Code: http.StatusInternalServerError,
- Message: "Unexpected Content-Type: Expected: application/json, Received: text/html\nResponse body: " + rawResponse,
- }
-
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, ts.URL+route, nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
@@ -308,11 +216,23 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
if err != nil {
t.Fatalf("failed to send request: %v", err)
}
+
+ expectedError := &Error{
+ Response: resp,
+ Code: http.StatusInternalServerError,
+ Message: "Unexpected Content-Type: Expected: application/json, Received: text/html\nResponse body: " + rawResponse,
+ }
+
defer resp.Body.Close()
- _, err = coupleAPIErrorsHTTP(resp, nil)
- if diff := cmp.Diff(expectedError, err); diff != "" {
- t.Errorf("expected error to match but got diff:\n%s", diff)
+ _, err = coupleAPIErrors(resp, nil)
+
+ if !strings.Contains(err.Error(), strconv.Itoa(expectedError.Code)) {
+ t.Errorf("expected error code %d, got %d", expectedError.Code, resp.StatusCode)
+ }
+
+ if !strings.Contains(err.Error(), expectedError.Message) {
+ t.Errorf("expected error message %s, got %s", expectedError.Message, err.Error())
}
})
@@ -337,14 +257,19 @@ func TestCoupleAPIErrorsHTTP(t *testing.T) {
},
}
- expectedError := Error{
- Code: http.StatusBadGateway,
- Message: http.StatusText(http.StatusBadGateway),
+ expectedError := &Error{
+ Response: resp,
+ Code: http.StatusBadGateway,
+ Message: http.StatusText(http.StatusBadGateway),
+ }
+
+ _, err := coupleAPIErrors(resp, nil)
+ if !strings.Contains(err.Error(), strconv.Itoa(expectedError.Code)) {
+ t.Errorf("expected error code %d, got %d", expectedError.Code, resp.StatusCode)
}
- _, err := coupleAPIErrorsHTTP(resp, nil)
- if !cmp.Equal(err, expectedError) {
- t.Errorf("expected error %#v to match error %#v", err, expectedError)
+ if !strings.Contains(err.Error(), expectedError.Message) {
+ t.Errorf("expected error message %s, got %s", expectedError.Message, err.Error())
}
})
}
@@ -382,26 +307,26 @@ func TestErrorIs(t *testing.T) {
expectedResult: true,
},
{
- testName: "default and Error from empty resty error",
- err1: NewError(restyError("", "")),
+ testName: "default and Error from empty http error",
+ err1: NewError(httpError("", "")),
err2: defaultError,
expectedResult: true,
},
{
- testName: "default and Error from resty error with field",
- err1: NewError(restyError("", "test field")),
+ testName: "default and Error from http error with field",
+ err1: NewError(httpError("", "test field")),
err2: defaultError,
expectedResult: true,
},
{
- testName: "default and Error from resty error with field and reason",
- err1: NewError(restyError("test reason", "test field")),
+ testName: "default and Error from http error with field and reason",
+ err1: NewError(httpError("test reason", "test field")),
err2: defaultError,
expectedResult: true,
},
{
- testName: "default and Error from resty error with reason",
- err1: NewError(restyError("test reason", "")),
+ testName: "default and Error from http error with reason",
+ err1: NewError(httpError("test reason", "")),
err2: defaultError,
expectedResult: true,
},
diff --git a/go.mod b/go.mod
index 7d10cf442..9ce635bf1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,11 @@
module github.com/linode/linodego
require (
- github.com/go-resty/resty/v2 v2.13.1
github.com/google/go-cmp v0.6.0
github.com/jarcoal/httpmock v1.3.1
golang.org/x/net v0.29.0
golang.org/x/oauth2 v0.23.0
- golang.org/x/text v0.18.0
+ golang.org/x/text v0.19.0
gopkg.in/ini.v1 v1.66.6
)
diff --git a/go.sum b/go.sum
index ef206efaa..5c00f7ce1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,5 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
-github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
@@ -12,58 +10,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
diff --git a/go.work.sum b/go.work.sum
index 8c2843240..1208b0d8b 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -31,6 +31,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
@@ -46,10 +47,7 @@ golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
diff --git a/images.go b/images.go
index 9bbb20757..da967f0d2 100644
--- a/images.go
+++ b/images.go
@@ -4,9 +4,9 @@ import (
"context"
"encoding/json"
"io"
+ "net/http"
"time"
- "github.com/go-resty/resty/v2"
"github.com/linode/linodego/internal/parseabletime"
)
@@ -219,17 +219,45 @@ func (c *Client) CreateImageUpload(ctx context.Context, opts ImageCreateUploadOp
// UploadImageToURL uploads the given image to the given upload URL.
func (c *Client) UploadImageToURL(ctx context.Context, uploadURL string, image io.Reader) error {
- // Linode-specific headers do not need to be sent to this endpoint
- req := resty.New().SetDebug(c.resty.Debug).R().
- SetContext(ctx).
- SetContentLength(true).
- SetHeader("Content-Type", "application/octet-stream").
- SetBody(image)
+ clonedClient := *c.httpClient
+ clonedClient.Transport = http.DefaultTransport
- _, err := coupleAPIErrors(req.
- Put(uploadURL))
+ var contentLength int64 = -1
- return err
+ if seeker, ok := image.(io.Seeker); ok {
+ size, err := seeker.Seek(0, io.SeekEnd)
+ if err != nil {
+ return err
+ }
+
+ _, err = seeker.Seek(0, io.SeekStart)
+ if err != nil {
+ return err
+ }
+
+ contentLength = size
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, image)
+ if err != nil {
+ return err
+ }
+
+ if contentLength >= 0 {
+ req.ContentLength = contentLength
+ }
+
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("User-Agent", c.userAgent)
+
+ resp, err := clonedClient.Do(req)
+
+ _, err = coupleAPIErrors(resp, err)
+ if err != nil {
+ return err
+ }
+
+ return nil
}
// UploadImage creates and uploads an image.
diff --git a/internal/testutil/mock.go b/internal/testutil/mock.go
index aaaeebb47..a033f6d17 100644
--- a/internal/testutil/mock.go
+++ b/internal/testutil/mock.go
@@ -108,15 +108,15 @@ type TestLogger struct {
}
func (l *TestLogger) Errorf(format string, v ...interface{}) {
- l.outputf("ERROR RESTY "+format, v...)
+ l.outputf("ERROR "+format, v...)
}
func (l *TestLogger) Warnf(format string, v ...interface{}) {
- l.outputf("WARN RESTY "+format, v...)
+ l.outputf("WARN "+format, v...)
}
func (l *TestLogger) Debugf(format string, v ...interface{}) {
- l.outputf("DEBUG RESTY "+format, v...)
+ l.outputf("DEBUG "+format, v...)
}
func (l *TestLogger) outputf(format string, v ...interface{}) {
diff --git a/k8s/go.mod b/k8s/go.mod
index c8cf226b6..d6ee7c517 100644
--- a/k8s/go.mod
+++ b/k8s/go.mod
@@ -14,7 +14,6 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
- github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -32,7 +31,7 @@ require (
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
- golang.org/x/text v0.18.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/k8s/go.sum b/k8s/go.sum
index b295efe46..9b203c392 100644
--- a/k8s/go.sum
+++ b/k8s/go.sum
@@ -12,8 +12,6 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
-github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -79,27 +77,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
@@ -107,46 +93,23 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/logger.go b/logger.go
index 5de758591..115766caa 100644
--- a/logger.go
+++ b/logger.go
@@ -5,43 +5,35 @@ import (
"os"
)
-//nolint:unused
-type httpLogger interface {
+type Logger interface {
Errorf(format string, v ...interface{})
Warnf(format string, v ...interface{})
Debugf(format string, v ...interface{})
}
-//nolint:unused
type logger struct {
l *log.Logger
}
-//nolint:unused
func createLogger() *logger {
l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
return l
}
-//nolint:unused
-var _ httpLogger = (*logger)(nil)
+var _ Logger = (*logger)(nil)
-//nolint:unused
func (l *logger) Errorf(format string, v ...interface{}) {
- l.output("ERROR RESTY "+format, v...)
+ l.output("ERROR "+format, v...)
}
-//nolint:unused
func (l *logger) Warnf(format string, v ...interface{}) {
- l.output("WARN RESTY "+format, v...)
+ l.output("WARN "+format, v...)
}
-//nolint:unused
func (l *logger) Debugf(format string, v ...interface{}) {
- l.output("DEBUG RESTY "+format, v...)
+ l.output("DEBUG "+format, v...)
}
-//nolint:unused
func (l *logger) output(format string, v ...interface{}) { //nolint:goprintffuncname
if len(v) == 0 {
l.l.Print(format)
diff --git a/pagination.go b/pagination.go
index 3b3f50ac9..e8127a58d 100644
--- a/pagination.go
+++ b/pagination.go
@@ -9,10 +9,9 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
+ "net/http"
"reflect"
"strconv"
-
- "github.com/go-resty/resty/v2"
)
// PageOptions are the pagination parameters for List endpoints
@@ -56,38 +55,48 @@ func (l ListOptions) Hash() (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}
-func applyListOptionsToRequest(opts *ListOptions, req *resty.Request) error {
+func createListOptionsToRequestMutator(opts *ListOptions) func(*http.Request) error {
if opts == nil {
return nil
}
- if opts.QueryParams != nil {
- params, err := flattenQueryStruct(opts.QueryParams)
- if err != nil {
- return fmt.Errorf("failed to apply list options: %w", err)
+ // Return a mutator to apply query parameters and headers
+ return func(req *http.Request) error {
+ query := req.URL.Query()
+
+ // Apply QueryParams from ListOptions if present
+ if opts.QueryParams != nil {
+ params, err := flattenQueryStruct(opts.QueryParams)
+ if err != nil {
+ return fmt.Errorf("failed to apply list options: %w", err)
+ }
+ for key, value := range params {
+ query.Set(key, value)
+ }
}
- req.SetQueryParams(params)
- }
-
- if opts.PageOptions != nil && opts.Page > 0 {
- req.SetQueryParam("page", strconv.Itoa(opts.Page))
- }
+ // Apply pagination options
+ if opts.PageOptions != nil && opts.Page > 0 {
+ query.Set("page", strconv.Itoa(opts.Page))
+ }
+ if opts.PageSize > 0 {
+ query.Set("page_size", strconv.Itoa(opts.PageSize))
+ }
- if opts.PageSize > 0 {
- req.SetQueryParam("page_size", strconv.Itoa(opts.PageSize))
- }
+ // Apply filters as headers
+ if len(opts.Filter) > 0 {
+ req.Header.Set("X-Filter", opts.Filter)
+ }
- if len(opts.Filter) > 0 {
- req.SetHeader("X-Filter", opts.Filter)
+ // Assign the updated query back to the request URL
+ req.URL.RawQuery = query.Encode()
+ return nil
}
-
- return nil
}
type PagedResponse interface {
endpoint(...any) string
- castResult(*resty.Request, string) (int, int, error)
+ castResult(*http.Request, string) (int, int, error)
}
// flattenQueryStruct flattens a structure into a Resty-compatible query param map.
diff --git a/request_helpers.go b/request_helpers.go
index 152a26433..b19924730 100644
--- a/request_helpers.go
+++ b/request_helpers.go
@@ -1,9 +1,11 @@
package linodego
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
+ "net/http"
"net/url"
"reflect"
)
@@ -26,8 +28,6 @@ func getPaginatedResults[T any](
endpoint string,
opts *ListOptions,
) ([]T, error) {
- var resultType paginatedResponse[T]
-
result := make([]T, 0)
if opts == nil {
@@ -41,34 +41,33 @@ func getPaginatedResults[T any](
// Makes a request to a particular page and
// appends the response to the result
handlePage := func(page int) error {
- // Override the page to be applied in applyListOptionsToRequest(...)
+ var resultType paginatedResponse[T]
opts.Page = page
- // This request object cannot be reused for each page request
- // because it can lead to possible data corruption
- req := client.R(ctx).SetResult(resultType)
-
- // Apply all user-provided list options to the request
- if err := applyListOptionsToRequest(opts, req); err != nil {
- return err
+ params := requestParams{
+ Response: &resultType,
}
- res, err := coupleAPIErrors(req.Get(endpoint))
+ // Create a mutator to all user-provided list options to the request
+ mutator := createListOptionsToRequestMutator(opts)
+
+ // Make the request using doRequest
+ err := client.doRequest(ctx, http.MethodGet, endpoint, params, &mutator)
if err != nil {
return err
}
- response := res.Result().(*paginatedResponse[T])
-
+ // Extract the result from the response
opts.Page = page
- opts.Pages = response.Pages
- opts.Results = response.Results
+ opts.Pages = resultType.Pages
+ opts.Results = resultType.Results
- result = append(result, response.Data...)
+ // Append the data to the result slice
+ result = append(result, resultType.Data...)
return nil
}
- // This helps simplify the logic below
+ // Determine starting page
startingPage := 1
pageDefined := opts.Page > 0
@@ -81,13 +80,12 @@ func getPaginatedResults[T any](
return nil, err
}
- // If the user has explicitly specified a page, we don't
- // need to get any other pages.
+ // If a specific page is defined, return the result
if pageDefined {
return result, nil
}
- // Get the rest of the pages
+ // Get the remaining pages
for page := 2; page <= opts.Pages; page++ {
if err := handlePage(page); err != nil {
return nil, err
@@ -105,14 +103,16 @@ func doGETRequest[T any](
endpoint string,
) (*T, error) {
var resultType T
+ params := requestParams{
+ Response: &resultType,
+ }
- req := client.R(ctx).SetResult(&resultType)
- r, err := coupleAPIErrors(req.Get(endpoint))
+ err := client.doRequest(ctx, http.MethodGet, endpoint, params, nil)
if err != nil {
return nil, err
}
- return r.Result().(*T), nil
+ return &resultType, nil
}
// doPOSTRequest runs a PUT request using the given client, API endpoint,
@@ -124,29 +124,27 @@ func doPOSTRequest[T, O any](
options ...O,
) (*T, error) {
var resultType T
-
numOpts := len(options)
-
if numOpts > 1 {
- return nil, fmt.Errorf("invalid number of options: %d", len(options))
+ return nil, fmt.Errorf("invalid number of options: %d", numOpts)
}
- req := client.R(ctx).SetResult(&resultType)
-
+ params := requestParams{
+ Response: &resultType,
+ }
if numOpts > 0 && !isNil(options[0]) {
body, err := json.Marshal(options[0])
if err != nil {
return nil, err
}
- req.SetBody(string(body))
+ params.Body = bytes.NewReader(body)
}
- r, err := coupleAPIErrors(req.Post(endpoint))
+ err := client.doRequest(ctx, http.MethodPost, endpoint, params, nil)
if err != nil {
return nil, err
}
-
- return r.Result().(*T), nil
+ return &resultType, nil
}
// doPUTRequest runs a PUT request using the given client, API endpoint,
@@ -158,29 +156,27 @@ func doPUTRequest[T, O any](
options ...O,
) (*T, error) {
var resultType T
-
numOpts := len(options)
-
if numOpts > 1 {
- return nil, fmt.Errorf("invalid number of options: %d", len(options))
+ return nil, fmt.Errorf("invalid number of options: %d", numOpts)
}
- req := client.R(ctx).SetResult(&resultType)
-
+ params := requestParams{
+ Response: &resultType,
+ }
if numOpts > 0 && !isNil(options[0]) {
body, err := json.Marshal(options[0])
if err != nil {
return nil, err
}
- req.SetBody(string(body))
+ params.Body = bytes.NewReader(body)
}
- r, err := coupleAPIErrors(req.Put(endpoint))
+ err := client.doRequest(ctx, http.MethodPut, endpoint, params, nil)
if err != nil {
return nil, err
}
-
- return r.Result().(*T), nil
+ return &resultType, nil
}
// doDELETERequest runs a DELETE request using the given client
@@ -190,8 +186,8 @@ func doDELETERequest(
client *Client,
endpoint string,
) error {
- req := client.R(ctx)
- _, err := coupleAPIErrors(req.Delete(endpoint))
+ params := requestParams{}
+ err := client.doRequest(ctx, http.MethodDelete, endpoint, params, nil)
return err
}
diff --git a/request_helpers_test.go b/request_helpers_test.go
index bed2e7403..03a02bed1 100644
--- a/request_helpers_test.go
+++ b/request_helpers_test.go
@@ -3,14 +3,13 @@ package linodego
import (
"context"
"fmt"
+ "github.com/stretchr/testify/require"
"math"
"net/http"
"reflect"
"strconv"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/linode/linodego/internal/testutil"
"github.com/google/go-cmp/cmp"
@@ -82,11 +81,8 @@ func TestRequestHelpers_post(t *testing.T) {
func TestRequestHelpers_postNoOptions(t *testing.T) {
client := testutil.CreateMockClient(t, NewClient)
- httpmock.RegisterRegexpResponder(
- "POST",
- testutil.MockRequestURL("/foo/bar"),
- testutil.MockRequestBodyValidateNoBody(t, testResponse),
- )
+ httpmock.RegisterRegexpResponder("POST", testutil.MockRequestURL("/foo/bar"),
+ testutil.MockRequestBodyValidateNoBody(t, testResponse))
result, err := doPOSTRequest[testResultType, any](
context.Background(),
@@ -126,11 +122,8 @@ func TestRequestHelpers_put(t *testing.T) {
func TestRequestHelpers_putNoOptions(t *testing.T) {
client := testutil.CreateMockClient(t, NewClient)
- httpmock.RegisterRegexpResponder(
- "PUT",
- testutil.MockRequestURL("/foo/bar"),
- testutil.MockRequestBodyValidateNoBody(t, testResponse),
- )
+ httpmock.RegisterRegexpResponder("PUT", testutil.MockRequestURL("/foo/bar"),
+ testutil.MockRequestBodyValidateNoBody(t, testResponse))
result, err := doPUTRequest[testResultType, any](
context.Background(),
@@ -149,11 +142,8 @@ func TestRequestHelpers_putNoOptions(t *testing.T) {
func TestRequestHelpers_delete(t *testing.T) {
client := testutil.CreateMockClient(t, NewClient)
- httpmock.RegisterRegexpResponder(
- "DELETE",
- testutil.MockRequestURL("/foo/bar/foo%20bar"),
- httpmock.NewStringResponder(200, "{}"),
- )
+ httpmock.RegisterRegexpResponder("DELETE", testutil.MockRequestURL("/foo/bar/foo%20bar"),
+ httpmock.NewStringResponder(200, "{}"))
if err := doDELETERequest(
context.Background(),
@@ -171,14 +161,8 @@ func TestRequestHelpers_paginateAll(t *testing.T) {
numRequests := 0
- httpmock.RegisterRegexpResponder(
- "GET",
- testutil.MockRequestURL("/foo/bar"),
- mockPaginatedResponse(
- buildPaginatedEntries(totalResults),
- &numRequests,
- ),
- )
+ httpmock.RegisterRegexpResponder("GET", testutil.MockRequestURL("/foo/bar"),
+ mockPaginatedResponse(buildPaginatedEntries(totalResults), &numRequests))
response, err := getPaginatedResults[testResultType](
context.Background(),
@@ -207,14 +191,8 @@ func TestRequestHelpers_paginateSingle(t *testing.T) {
numRequests := 0
- httpmock.RegisterRegexpResponder(
- "GET",
- testutil.MockRequestURL("/foo/bar"),
- mockPaginatedResponse(
- buildPaginatedEntries(12),
- &numRequests,
- ),
- )
+ httpmock.RegisterRegexpResponder("GET", testutil.MockRequestURL("/foo/bar"),
+ mockPaginatedResponse(buildPaginatedEntries(12), &numRequests))
response, err := getPaginatedResults[testResultType](
context.Background(),
diff --git a/retries.go b/retries.go
index 148864420..371ee03d5 100644
--- a/retries.go
+++ b/retries.go
@@ -1,72 +1,89 @@
package linodego
import (
+ "bytes"
+ "encoding/json"
"errors"
+ "io"
"log"
"net/http"
"strconv"
"time"
- "github.com/go-resty/resty/v2"
"golang.org/x/net/http2"
)
const (
- retryAfterHeaderName = "Retry-After"
- maintenanceModeHeaderName = "X-Maintenance-Mode"
-
- defaultRetryCount = 1000
+ RetryAfterHeaderName = "Retry-After"
+ MaintenanceModeHeaderName = "X-Maintenance-Mode"
+ DefaultRetryCount = 1000
)
-// type RetryConditional func(r *resty.Response) (shouldRetry bool)
-type RetryConditional resty.RetryConditionFunc
+// RetryConditional is a type alias for a function that determines if a request should be retried based on the response and error.
+type RetryConditional func(*http.Response, error) bool
-// type RetryAfter func(c *resty.Client, r *resty.Response) (time.Duration, error)
-type RetryAfter resty.RetryAfterFunc
+// RetryAfter is a type alias for a function that determines the duration to wait before retrying based on the response.
+type RetryAfter func(*http.Response) (time.Duration, error)
-// Configures resty to
-// lock until enough time has passed to retry the request as determined by the Retry-After response header.
-// If the Retry-After header is not set, we fall back to value of SetPollDelay.
-func configureRetries(c *Client) {
- c.resty.
- SetRetryCount(defaultRetryCount).
- AddRetryCondition(checkRetryConditionals(c)).
- SetRetryAfter(respectRetryAfter)
+// Configures http.Client to lock until enough time has passed to retry the request as determined by the Retry-After response header.
+// If the Retry-After header is not set, we fall back to the value of SetPollDelay.
+func ConfigureRetries(c *Client) {
+ c.SetRetryAfter(RespectRetryAfter)
+ c.SetRetryCount(DefaultRetryCount)
}
-func checkRetryConditionals(c *Client) func(*resty.Response, error) bool {
- return func(r *resty.Response, err error) bool {
- for _, retryConditional := range c.retryConditionals {
- retry := retryConditional(r, err)
- if retry {
- log.Printf("[INFO] Received error %s - Retrying", r.Error())
- return true
- }
- }
- return false
+func RespectRetryAfter(resp *http.Response) (time.Duration, error) {
+ if resp == nil {
+ return 0, nil
}
+
+ retryAfterStr := resp.Header.Get(RetryAfterHeaderName)
+ if retryAfterStr == "" {
+ return 0, nil
+ }
+
+ retryAfter, err := strconv.Atoi(retryAfterStr)
+ if err != nil {
+ return 0, err
+ }
+
+ duration := time.Duration(retryAfter) * time.Second
+ log.Printf("[INFO] Respecting Retry-After Header of %d (%s)", retryAfter, duration)
+ return duration, nil
}
-// SetLinodeBusyRetry configures resty to retry specifically on "Linode busy." errors
-// The retry wait time is configured in SetPollDelay
-func linodeBusyRetryCondition(r *resty.Response, _ error) bool {
- apiError, ok := r.Error().(*APIError)
+// Retry conditions
+
+func LinodeBusyRetryCondition(resp *http.Response, _ error) bool {
+ if resp == nil {
+ return false
+ }
+
+ apiError, ok := getAPIError(resp)
linodeBusy := ok && apiError.Error() == "Linode busy."
- retry := r.StatusCode() == http.StatusBadRequest && linodeBusy
+ retry := resp.StatusCode == http.StatusBadRequest && linodeBusy
return retry
}
-func tooManyRequestsRetryCondition(r *resty.Response, _ error) bool {
- return r.StatusCode() == http.StatusTooManyRequests
+func TooManyRequestsRetryCondition(resp *http.Response, _ error) bool {
+ if resp == nil {
+ return false
+ }
+
+ return resp.StatusCode == http.StatusTooManyRequests
}
-func serviceUnavailableRetryCondition(r *resty.Response, _ error) bool {
- serviceUnavailable := r.StatusCode() == http.StatusServiceUnavailable
+func ServiceUnavailableRetryCondition(resp *http.Response, _ error) bool {
+ if resp == nil {
+ return false
+ }
+
+ serviceUnavailable := resp.StatusCode == http.StatusServiceUnavailable
// During maintenance events, the API will return a 503 and add
// an `X-MAINTENANCE-MODE` header. Don't retry during maintenance
// events, only for legitimate 503s.
- if serviceUnavailable && r.Header().Get(maintenanceModeHeaderName) != "" {
+ if serviceUnavailable && resp.Header.Get(MaintenanceModeHeaderName) != "" {
log.Printf("[INFO] Linode API is under maintenance, request will not be retried - please see status.linode.com for more information")
return false
}
@@ -74,32 +91,46 @@ func serviceUnavailableRetryCondition(r *resty.Response, _ error) bool {
return serviceUnavailable
}
-func requestTimeoutRetryCondition(r *resty.Response, _ error) bool {
- return r.StatusCode() == http.StatusRequestTimeout
+func RequestTimeoutRetryCondition(resp *http.Response, _ error) bool {
+ if resp == nil {
+ return false
+ }
+
+ return resp.StatusCode == http.StatusRequestTimeout
}
-func requestGOAWAYRetryCondition(_ *resty.Response, e error) bool {
- return errors.As(e, &http2.GoAwayError{})
+func RequestGOAWAYRetryCondition(_ *http.Response, err error) bool {
+ return errors.As(err, &http2.GoAwayError{})
}
-func requestNGINXRetryCondition(r *resty.Response, _ error) bool {
- return r.StatusCode() == http.StatusBadRequest &&
- r.Header().Get("Server") == "nginx" &&
- r.Header().Get("Content-Type") == "text/html"
+func RequestNGINXRetryCondition(resp *http.Response, _ error) bool {
+ if resp == nil {
+ return false
+ }
+
+ return resp.StatusCode == http.StatusBadRequest &&
+ resp.Header.Get("Server") == "nginx" &&
+ resp.Header.Get("Content-Type") == "text/html"
}
-func respectRetryAfter(client *resty.Client, resp *resty.Response) (time.Duration, error) {
- retryAfterStr := resp.Header().Get(retryAfterHeaderName)
- if retryAfterStr == "" {
- return 0, nil
+// Helper function to extract APIError from response
+func getAPIError(resp *http.Response) (*APIError, bool) {
+ if resp.Body == nil {
+ return nil, false
}
- retryAfter, err := strconv.Atoi(retryAfterStr)
+ body, err := io.ReadAll(resp.Body)
if err != nil {
- return 0, err
+ return nil, false
}
- duration := time.Duration(retryAfter) * time.Second
- log.Printf("[INFO] Respecting Retry-After Header of %d (%s) (max %s)", retryAfter, duration, client.RetryMaxWaitTime)
- return duration, nil
+ resp.Body = io.NopCloser(bytes.NewReader(body))
+
+ var apiError APIError
+ err = json.Unmarshal(body, &apiError)
+ if err != nil {
+ return nil, false
+ }
+
+ return &apiError, true
}
diff --git a/retries_http.go b/retries_http.go
deleted file mode 100644
index 0439af48e..000000000
--- a/retries_http.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package linodego
-
-import (
- "encoding/json"
- "errors"
- "log"
- "net/http"
- "strconv"
- "time"
-
- "golang.org/x/net/http2"
-)
-
-const (
- // nolint:unused
- httpRetryAfterHeaderName = "Retry-After"
- // nolint:unused
- httpMaintenanceModeHeaderName = "X-Maintenance-Mode"
-
- // nolint:unused
- httpDefaultRetryCount = 1000
-)
-
-// RetryConditional is a type alias for a function that determines if a request should be retried based on the response and error.
-// nolint:unused
-type httpRetryConditional func(*http.Response, error) bool
-
-// RetryAfter is a type alias for a function that determines the duration to wait before retrying based on the response.
-// nolint:unused
-type httpRetryAfter func(*http.Response) (time.Duration, error)
-
-// Configures http.Client to lock until enough time has passed to retry the request as determined by the Retry-After response header.
-// If the Retry-After header is not set, we fall back to the value of SetPollDelay.
-// nolint:unused
-func httpConfigureRetries(c *httpClient) {
- c.retryConditionals = append(c.retryConditionals, httpcheckRetryConditionals(c))
- c.retryAfter = httpRespectRetryAfter
-}
-
-// nolint:unused
-func httpcheckRetryConditionals(c *httpClient) httpRetryConditional {
- return func(resp *http.Response, err error) bool {
- for _, retryConditional := range c.retryConditionals {
- retry := retryConditional(resp, err)
- if retry {
- log.Printf("[INFO] Received error %v - Retrying", err)
- return true
- }
- }
- return false
- }
-}
-
-// nolint:unused
-func httpRespectRetryAfter(resp *http.Response) (time.Duration, error) {
- retryAfterStr := resp.Header.Get(retryAfterHeaderName)
- if retryAfterStr == "" {
- return 0, nil
- }
-
- retryAfter, err := strconv.Atoi(retryAfterStr)
- if err != nil {
- return 0, err
- }
-
- duration := time.Duration(retryAfter) * time.Second
- log.Printf("[INFO] Respecting Retry-After Header of %d (%s)", retryAfter, duration)
- return duration, nil
-}
-
-// Retry conditions
-
-// nolint:unused
-func httpLinodeBusyRetryCondition(resp *http.Response, _ error) bool {
- apiError, ok := getAPIError(resp)
- linodeBusy := ok && apiError.Error() == "Linode busy."
- retry := resp.StatusCode == http.StatusBadRequest && linodeBusy
- return retry
-}
-
-// nolint:unused
-func httpTooManyRequestsRetryCondition(resp *http.Response, _ error) bool {
- return resp.StatusCode == http.StatusTooManyRequests
-}
-
-// nolint:unused
-func httpServiceUnavailableRetryCondition(resp *http.Response, _ error) bool {
- serviceUnavailable := resp.StatusCode == http.StatusServiceUnavailable
-
- // During maintenance events, the API will return a 503 and add
- // an `X-MAINTENANCE-MODE` header. Don't retry during maintenance
- // events, only for legitimate 503s.
- if serviceUnavailable && resp.Header.Get(maintenanceModeHeaderName) != "" {
- log.Printf("[INFO] Linode API is under maintenance, request will not be retried - please see status.linode.com for more information")
- return false
- }
-
- return serviceUnavailable
-}
-
-// nolint:unused
-func httpRequestTimeoutRetryCondition(resp *http.Response, _ error) bool {
- return resp.StatusCode == http.StatusRequestTimeout
-}
-
-// nolint:unused
-func httpRequestGOAWAYRetryCondition(_ *http.Response, err error) bool {
- return errors.As(err, &http2.GoAwayError{})
-}
-
-// nolint:unused
-func httpRequestNGINXRetryCondition(resp *http.Response, _ error) bool {
- return resp.StatusCode == http.StatusBadRequest &&
- resp.Header.Get("Server") == "nginx" &&
- resp.Header.Get("Content-Type") == "text/html"
-}
-
-// Helper function to extract APIError from response
-// nolint:unused
-func getAPIError(resp *http.Response) (*APIError, bool) {
- var apiError APIError
- err := json.NewDecoder(resp.Body).Decode(&apiError)
- if err != nil {
- return nil, false
- }
- return &apiError, true
-}
diff --git a/retries_http_test.go b/retries_http_test.go
deleted file mode 100644
index 35eea5fc9..000000000
--- a/retries_http_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package linodego
-
-import (
- "bytes"
- "encoding/json"
- "io"
- "net/http"
- "testing"
- "time"
-)
-
-func TestHTTPLinodeBusyRetryCondition(t *testing.T) {
- var retry bool
-
- // Initialize response body
- rawResponse := &http.Response{
- StatusCode: http.StatusBadRequest,
- Body: io.NopCloser(bytes.NewBuffer(nil)),
- }
-
- retry = httpLinodeBusyRetryCondition(rawResponse, nil)
-
- if retry {
- t.Errorf("Should not have retried")
- }
-
- apiError := APIError{
- Errors: []APIErrorReason{
- {Reason: "Linode busy."},
- },
- }
- rawResponse.Body = createResponseBody(apiError)
-
- retry = httpLinodeBusyRetryCondition(rawResponse, nil)
-
- if !retry {
- t.Errorf("Should have retried")
- }
-}
-
-func TestHTTPServiceUnavailableRetryCondition(t *testing.T) {
- rawResponse := &http.Response{
- StatusCode: http.StatusServiceUnavailable,
- Header: http.Header{httpRetryAfterHeaderName: []string{"20"}},
- Body: io.NopCloser(bytes.NewBuffer(nil)), // Initialize response body
- }
-
- if retry := httpServiceUnavailableRetryCondition(rawResponse, nil); !retry {
- t.Error("expected request to be retried")
- }
-
- if retryAfter, err := httpRespectRetryAfter(rawResponse); err != nil {
- t.Errorf("expected error to be nil but got %s", err)
- } else if retryAfter != time.Second*20 {
- t.Errorf("expected retryAfter to be 20 but got %d", retryAfter)
- }
-}
-
-func TestHTTPServiceMaintenanceModeRetryCondition(t *testing.T) {
- rawResponse := &http.Response{
- StatusCode: http.StatusServiceUnavailable,
- Header: http.Header{
- httpRetryAfterHeaderName: []string{"20"},
- httpMaintenanceModeHeaderName: []string{"Currently in maintenance mode."},
- },
- Body: io.NopCloser(bytes.NewBuffer(nil)), // Initialize response body
- }
-
- if retry := httpServiceUnavailableRetryCondition(rawResponse, nil); retry {
- t.Error("expected retry to be skipped due to maintenance mode header")
- }
-}
-
-// Helper function to create a response body from an object
-func createResponseBody(obj interface{}) io.ReadCloser {
- body, err := json.Marshal(obj)
- if err != nil {
- panic(err)
- }
- return io.NopCloser(bytes.NewBuffer(body))
-}
diff --git a/retries_test.go b/retries_test.go
index 4f0029388..45b5fc4d3 100644
--- a/retries_test.go
+++ b/retries_test.go
@@ -1,24 +1,24 @@
package linodego
import (
+ "bytes"
+ "encoding/json"
+ "io"
"net/http"
"testing"
"time"
-
- "github.com/go-resty/resty/v2"
)
func TestLinodeBusyRetryCondition(t *testing.T) {
var retry bool
- request := resty.Request{}
- rawResponse := http.Response{StatusCode: http.StatusBadRequest}
- response := resty.Response{
- Request: &request,
- RawResponse: &rawResponse,
+ // Initialize response body
+ rawResponse := &http.Response{
+ StatusCode: http.StatusBadRequest,
+ Body: io.NopCloser(bytes.NewBuffer(nil)),
}
- retry = linodeBusyRetryCondition(&response, nil)
+ retry = LinodeBusyRetryCondition(rawResponse, nil)
if retry {
t.Errorf("Should not have retried")
@@ -29,48 +29,53 @@ func TestLinodeBusyRetryCondition(t *testing.T) {
{Reason: "Linode busy."},
},
}
- request.SetError(&apiError)
+ rawResponse.Body = createResponseBody(apiError)
- retry = linodeBusyRetryCondition(&response, nil)
+ retry = LinodeBusyRetryCondition(rawResponse, nil)
if !retry {
t.Errorf("Should have retried")
}
}
-func TestLinodeServiceUnavailableRetryCondition(t *testing.T) {
- request := resty.Request{}
- rawResponse := http.Response{StatusCode: http.StatusServiceUnavailable, Header: http.Header{
- retryAfterHeaderName: []string{"20"},
- }}
- response := resty.Response{
- Request: &request,
- RawResponse: &rawResponse,
+func TestServiceUnavailableRetryCondition(t *testing.T) {
+ rawResponse := &http.Response{
+ StatusCode: http.StatusServiceUnavailable,
+ Header: http.Header{RetryAfterHeaderName: []string{"20"}},
+ Body: io.NopCloser(bytes.NewBuffer(nil)), // Initialize response body
}
- if retry := serviceUnavailableRetryCondition(&response, nil); !retry {
+ if retry := ServiceUnavailableRetryCondition(rawResponse, nil); !retry {
t.Error("expected request to be retried")
}
- if retryAfter, err := respectRetryAfter(NewClient(nil).resty, &response); err != nil {
+ if retryAfter, err := RespectRetryAfter(rawResponse); err != nil {
t.Errorf("expected error to be nil but got %s", err)
} else if retryAfter != time.Second*20 {
t.Errorf("expected retryAfter to be 20 but got %d", retryAfter)
}
}
-func TestLinodeServiceMaintenanceModeRetryCondition(t *testing.T) {
- request := resty.Request{}
- rawResponse := http.Response{StatusCode: http.StatusServiceUnavailable, Header: http.Header{
- retryAfterHeaderName: []string{"20"},
- maintenanceModeHeaderName: []string{"Currently in maintenance mode."},
- }}
- response := resty.Response{
- Request: &request,
- RawResponse: &rawResponse,
+func TestServiceMaintenanceModeRetryCondition(t *testing.T) {
+ rawResponse := &http.Response{
+ StatusCode: http.StatusServiceUnavailable,
+ Header: http.Header{
+ RetryAfterHeaderName: []string{"20"},
+ MaintenanceModeHeaderName: []string{"Currently in maintenance mode."},
+ },
+ Body: io.NopCloser(bytes.NewBuffer(nil)), // Initialize response body
}
- if retry := serviceUnavailableRetryCondition(&response, nil); retry {
+ if retry := ServiceUnavailableRetryCondition(rawResponse, nil); retry {
t.Error("expected retry to be skipped due to maintenance mode header")
}
}
+
+// Helper function to create a response body from an object
+func createResponseBody(obj interface{}) io.ReadCloser {
+ body, err := json.Marshal(obj)
+ if err != nil {
+ panic(err)
+ }
+ return io.NopCloser(bytes.NewBuffer(body))
+}
diff --git a/test/go.mod b/test/go.mod
index a09f94ba9..89aeab295 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -19,7 +19,6 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
- github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -36,7 +35,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
- golang.org/x/text v0.18.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/test/go.sum b/test/go.sum
index 905e845a7..40ae7cb1b 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -14,8 +14,6 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
-github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -84,27 +82,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
@@ -112,46 +98,23 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/test/integration/cache_test.go b/test/integration/cache_test.go
index fd4207de2..6f8385997 100644
--- a/test/integration/cache_test.go
+++ b/test/integration/cache_test.go
@@ -27,9 +27,19 @@ func TestCache_RegionList(t *testing.T) {
// Collect request number
totalRequests := int64(0)
+ //client.OnBeforeRequest(func(request *linodego.Request) error {
+ // fmt.Printf("Request URL: %s\n", request.URL.String()) // Log the URL
+ // fmt.Printf("Page: %s\n", request.URL.Query().Get("page")) // Log the page query parameter
+ // if !strings.Contains(request.URL.String(), "regions") || request.URL.Query().Get("page") != "1" {
+ // return nil
+ // }
+ //
+ // atomic.AddInt64(&totalRequests, 1)
+ // return nil
+ //})
client.OnBeforeRequest(func(request *linodego.Request) error {
- page := request.QueryParam.Get("page")
- if !strings.Contains(request.URL, "regions") || page != "1" {
+ page := request.URL.Query().Get("page")
+ if !strings.Contains(request.URL.String(), "regions") || page != "1" {
return nil
}
@@ -91,8 +101,8 @@ func TestCache_Expiration(t *testing.T) {
totalRequests := int64(0)
client.OnBeforeRequest(func(request *linodego.Request) error {
- page := request.QueryParam.Get("page")
- if !strings.Contains(request.URL, "kernels") || page != "1" {
+ page := request.URL.Query().Get("page")
+ if !strings.Contains(request.URL.String(), "kernels") || page != "1" {
return nil
}
diff --git a/test/unit/client_test.go b/test/unit/client_test.go
index e4c3be91d..8fc4ad611 100644
--- a/test/unit/client_test.go
+++ b/test/unit/client_test.go
@@ -2,13 +2,11 @@ package unit
import (
"context"
- "net/http"
- "testing"
-
- "golang.org/x/net/http2"
-
"github.com/jarcoal/httpmock"
"github.com/linode/linodego"
+ "golang.org/x/net/http2"
+ "net/http"
+ "testing"
)
func TestClient_NGINXRetry(t *testing.T) {