From 483f28f79b728d27b549d1a32fa716b289702601 Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Thu, 14 Nov 2024 17:18:24 -0600 Subject: [PATCH 1/8] Add basic util to request api --- utils/apiclient/client.go | 34 ++++++++++++++++++++++++++++++++++ utils/apiclient/client_test.go | 22 ++++++++++++++++++++++ utils/apiclient/main.go | 6 ++++++ 3 files changed, 62 insertions(+) create mode 100644 utils/apiclient/client.go create mode 100644 utils/apiclient/client_test.go create mode 100644 utils/apiclient/main.go diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go new file mode 100644 index 0000000000..b7b28c9c0e --- /dev/null +++ b/utils/apiclient/client.go @@ -0,0 +1,34 @@ +package apiclient + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/pkg/errors" +) + +func (c *APIClient) getRequest(endpoint string, queryParams url.Values) error { + fullURL := c.url(endpoint, queryParams) + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + return errors.Wrap(err, "http GET request creation failed") + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errors.Wrap(err, "http GET request failed") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request failed with status %d", resp.StatusCode) + } + + return nil +} + +func (c *APIClient) url(endpoint string, qstr url.Values) string { + return fmt.Sprintf("%s/%s?%s", c.BaseURL, endpoint, qstr.Encode()) +} diff --git a/utils/apiclient/client_test.go b/utils/apiclient/client_test.go new file mode 100644 index 0000000000..8a808e37d1 --- /dev/null +++ b/utils/apiclient/client_test.go @@ -0,0 +1,22 @@ +package apiclient + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_url(t *testing.T) { + c := &APIClient{ + BaseURL: "https://stellar.org", + } + + qstr := url.Values{} + qstr.Add("type", "forward") + qstr.Add("federation_type", "bank_account") + qstr.Add("swift", "BOPBPHMM") + qstr.Add("acct", "2382376") + furl := c.url("federation", qstr) + assert.Equal(t, "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", furl) +} diff --git a/utils/apiclient/main.go b/utils/apiclient/main.go new file mode 100644 index 0000000000..f2a5ef65dd --- /dev/null +++ b/utils/apiclient/main.go @@ -0,0 +1,6 @@ +package apiclient + +type APIClient struct { + BaseURL string + AuthToken string +} From 7fc2c620a570404f5bad52569669e5a896ce418e Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Fri, 15 Nov 2024 13:10:03 -0600 Subject: [PATCH 2/8] Add test for getRequest --- utils/apiclient/client.go | 28 ++++++++++++++++++++++------ utils/apiclient/client_test.go | 28 ++++++++++++++++++++++++++++ utils/apiclient/main.go | 12 ++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index b7b28c9c0e..a61bec5cfc 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -1,32 +1,48 @@ package apiclient import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "github.com/pkg/errors" ) -func (c *APIClient) getRequest(endpoint string, queryParams url.Values) error { +func (c *APIClient) getRequest(endpoint string, queryParams url.Values) (interface{}, error) { + client := c.HTTP + if client == nil { + client = &http.Client{} + } + fullURL := c.url(endpoint, queryParams) req, err := http.NewRequest("GET", fullURL, nil) if err != nil { - return errors.Wrap(err, "http GET request creation failed") + return nil, errors.Wrap(err, "http GET request creation failed") } - client := &http.Client{} resp, err := client.Do(req) if err != nil { - return errors.Wrap(err, "http GET request failed") + return nil, errors.Wrap(err, "http GET request failed") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API request failed with status %d", resp.StatusCode) + return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var result interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) } - return nil + return result, nil } func (c *APIClient) url(endpoint string, qstr url.Values) string { diff --git a/utils/apiclient/client_test.go b/utils/apiclient/client_test.go index 8a808e37d1..9dbbc61ffa 100644 --- a/utils/apiclient/client_test.go +++ b/utils/apiclient/client_test.go @@ -4,6 +4,7 @@ import ( "net/url" "testing" + "github.com/stellar/go/support/http/httptest" "github.com/stretchr/testify/assert" ) @@ -20,3 +21,30 @@ func Test_url(t *testing.T) { furl := c.url("federation", qstr) assert.Equal(t, "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", furl) } + +func Test_getRequest(t *testing.T) { + friendbotFundResponse := `{"key": "value"}` + + hmock := httptest.NewClient() + c := &APIClient{ + BaseURL: "https://stellar.org", + HTTP: hmock, + } + hmock.On( + "GET", + "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", + ).ReturnString(200, friendbotFundResponse) + qstr := url.Values{} + + qstr.Add("type", "forward") + qstr.Add("federation_type", "bank_account") + qstr.Add("swift", "BOPBPHMM") + qstr.Add("acct", "2382376") + + result, err := c.getRequest("federation", qstr) + if err != nil { + t.Fatal(err) + } + expected := map[string]interface{}{"key": "value"} + assert.Equal(t, expected, result) +} diff --git a/utils/apiclient/main.go b/utils/apiclient/main.go index f2a5ef65dd..707b019dac 100644 --- a/utils/apiclient/main.go +++ b/utils/apiclient/main.go @@ -1,6 +1,18 @@ package apiclient +import ( + "net/http" + "net/url" +) + +type HTTP interface { + Do(req *http.Request) (resp *http.Response, err error) + Get(url string) (resp *http.Response, err error) + PostForm(url string, data url.Values) (resp *http.Response, err error) +} + type APIClient struct { BaseURL string AuthToken string + HTTP HTTP } From 525079ff478615ad47d31e8fcf694d551ea48657 Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Fri, 15 Nov 2024 15:05:05 -0600 Subject: [PATCH 3/8] Add method for basic auth and api key auth. Also support headers --- utils/apiclient/client.go | 60 ++++++++++++++++++++++++++++++---- utils/apiclient/client_test.go | 12 +++++-- utils/apiclient/main.go | 5 ++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index a61bec5cfc..5b39da610b 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -1,6 +1,7 @@ package apiclient import ( + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -10,17 +11,20 @@ import ( "github.com/pkg/errors" ) -func (c *APIClient) getRequest(endpoint string, queryParams url.Values) (interface{}, error) { - client := c.HTTP - if client == nil { - client = &http.Client{} - } - +func (c *APIClient) createRequestBody(endpoint string, queryParams url.Values) (*http.Request, error) { fullURL := c.url(endpoint, queryParams) req, err := http.NewRequest("GET", fullURL, nil) if err != nil { return nil, errors.Wrap(err, "http GET request creation failed") } + return req, nil +} + +func (c *APIClient) callAPI(req *http.Request) (interface{}, error) { + client := c.HTTP + if client == nil { + client = &http.Client{} + } resp, err := client.Do(req) if err != nil { @@ -45,6 +49,50 @@ func (c *APIClient) getRequest(endpoint string, queryParams url.Values) (interfa return result, nil } +func setHeaders(req *http.Request, args map[string]interface{}) { + for key, value := range args { + strValue, ok := value.(string) + if !ok { + fmt.Printf("Skipping non-string value for header %s\n", key) + continue + } + + req.Header.Set(key, strValue) + } +} + +func setAuthHeaders(req *http.Request, authType string, args map[string]interface{}) error { + switch authType { + case "basic": + username, ok := args["username"].(string) + if !ok { + return fmt.Errorf("missing or invalid username") + } + password, ok := args["password"].(string) + if !ok { + return fmt.Errorf("missing or invalid password") + } + + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) + setHeaders(req, map[string]interface{}{ + "Authorization": authHeader, + }) + + case "api_key": + apiKey, ok := args["api_key"].(string) + if !ok { + return fmt.Errorf("missing or invalid API key") + } + setHeaders(req, map[string]interface{}{ + "Authorization": apiKey, + }) + + default: + return fmt.Errorf("unsupported auth type: %s", authType) + } + return nil +} + func (c *APIClient) url(endpoint string, qstr url.Values) string { return fmt.Sprintf("%s/%s?%s", c.BaseURL, endpoint, qstr.Encode()) } diff --git a/utils/apiclient/client_test.go b/utils/apiclient/client_test.go index 9dbbc61ffa..8db1193cc7 100644 --- a/utils/apiclient/client_test.go +++ b/utils/apiclient/client_test.go @@ -22,7 +22,7 @@ func Test_url(t *testing.T) { assert.Equal(t, "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", furl) } -func Test_getRequest(t *testing.T) { +func Test_callAPI(t *testing.T) { friendbotFundResponse := `{"key": "value"}` hmock := httptest.NewClient() @@ -41,10 +41,18 @@ func Test_getRequest(t *testing.T) { qstr.Add("swift", "BOPBPHMM") qstr.Add("acct", "2382376") - result, err := c.getRequest("federation", qstr) + req, err := c.createRequestBody("federation", qstr) if err != nil { t.Fatal(err) } + setAuthHeaders(req, "api_key", map[string]interface{}{"api_key": "test_api_key"}) + assert.Equal(t, "test_api_key", req.Header.Get("Authorization")) + + result, err := c.callAPI(req) + if err != nil { + t.Fatal(err) + } + expected := map[string]interface{}{"key": "value"} assert.Equal(t, expected, result) } diff --git a/utils/apiclient/main.go b/utils/apiclient/main.go index 707b019dac..952f17fa43 100644 --- a/utils/apiclient/main.go +++ b/utils/apiclient/main.go @@ -12,7 +12,6 @@ type HTTP interface { } type APIClient struct { - BaseURL string - AuthToken string - HTTP HTTP + BaseURL string + HTTP HTTP } From a28db134bc3b993565a89dc898073ee02e8fb561 Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Mon, 18 Nov 2024 15:53:15 -0600 Subject: [PATCH 4/8] Split module in client and requests. Add unit tests --- support/http/httptest/client_expectation.go | 32 +++++ support/http/httptest/main.go | 6 + utils/apiclient/client.go | 123 +++++++++--------- utils/apiclient/client_test.go | 115 ++++++++++++----- utils/apiclient/main.go | 13 +- utils/apiclient/request.go | 61 +++++++++ utils/apiclient/request_test.go | 130 ++++++++++++++++++++ 7 files changed, 380 insertions(+), 100 deletions(-) create mode 100644 utils/apiclient/request.go create mode 100644 utils/apiclient/request_test.go diff --git a/support/http/httptest/client_expectation.go b/support/http/httptest/client_expectation.go index f056ae7ebf..474f691285 100644 --- a/support/http/httptest/client_expectation.go +++ b/support/http/httptest/client_expectation.go @@ -1,6 +1,7 @@ package httptest import ( + "fmt" "net/http" "net/url" "strconv" @@ -85,6 +86,37 @@ func (ce *ClientExpectation) ReturnStringWithHeader( return ce.Return(httpmock.ResponderFromResponse(&cResp)) } +// ReturnMultipleResults registers multiple sequential responses for a given client expectation. +// Useful for testing retries +func (ce *ClientExpectation) ReturnMultipleResults(responseSets []ResponseData) *ClientExpectation { + var allResponses []httpmock.Responder + for _, response := range responseSets { + resp := http.Response{ + Status: strconv.Itoa(response.Status), + StatusCode: response.Status, + Body: httpmock.NewRespBodyFromString(response.Body), + Header: response.Header, + } + allResponses = append(allResponses, httpmock.ResponderFromResponse(&resp)) + } + responseIndex := 0 + ce.Client.MockTransport.RegisterResponder( + ce.Method, + ce.URL, + func(req *http.Request) (*http.Response, error) { + if responseIndex >= len(allResponses) { + panic(fmt.Sprintf("no responses available")) + } + + resp := allResponses[responseIndex] + responseIndex++ + return resp(req) + }, + ) + + return ce +} + // ReturnJSONWithHeader causes this expectation to resolve to a json-based body with the provided // status code and response header. Panics when the provided body cannot be encoded to JSON. func (ce *ClientExpectation) ReturnJSONWithHeader( diff --git a/support/http/httptest/main.go b/support/http/httptest/main.go index 47a00b1991..18b986ba1b 100644 --- a/support/http/httptest/main.go +++ b/support/http/httptest/main.go @@ -67,3 +67,9 @@ func NewServer(t *testing.T, handler http.Handler) *Server { Expect: httpexpect.New(t, server.URL), } } + +type ResponseData struct { + Status int + Body string + Header http.Header +} diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index 5b39da610b..6f89443b9f 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -1,98 +1,85 @@ package apiclient import ( - "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" + "time" "github.com/pkg/errors" ) -func (c *APIClient) createRequestBody(endpoint string, queryParams url.Values) (*http.Request, error) { - fullURL := c.url(endpoint, queryParams) - req, err := http.NewRequest("GET", fullURL, nil) - if err != nil { - return nil, errors.Wrap(err, "http GET request creation failed") - } - return req, nil +const ( + maxRetries = 5 + initialBackoff = 1 * time.Second +) + +func isRetryableStatusCode(statusCode int) bool { + return statusCode == http.StatusTooManyRequests || statusCode == http.StatusServiceUnavailable } -func (c *APIClient) callAPI(req *http.Request) (interface{}, error) { - client := c.HTTP - if client == nil { - client = &http.Client{} - } +func (c *APIClient) GetURL(endpoint string, qstr url.Values) string { + return fmt.Sprintf("%s/%s?%s", c.BaseURL, endpoint, qstr.Encode()) +} - resp, err := client.Do(req) - if err != nil { - return nil, errors.Wrap(err, "http GET request failed") +func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { + if reqParams.QueryParams == nil { + reqParams.QueryParams = url.Values{} } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode) + if reqParams.Headers == nil { + reqParams.Headers = map[string]interface{}{} } - body, err := ioutil.ReadAll(resp.Body) + url := c.GetURL(reqParams.Endpoint, reqParams.QueryParams) + reqBody, err := CreateRequestBody(reqParams.RequestType, url) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, errors.Wrap(err, "http request creation failed") } - var result interface{} - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + SetAuthHeaders(reqBody, c.authType, c.authHeaders) + SetHeaders(reqBody, reqParams.Headers) + client := c.HTTP + if client == nil { + client = &http.Client{} } - return result, nil -} - -func setHeaders(req *http.Request, args map[string]interface{}) { - for key, value := range args { - strValue, ok := value.(string) - if !ok { - fmt.Printf("Skipping non-string value for header %s\n", key) - continue - } - - req.Header.Set(key, strValue) - } -} + var result interface{} + retries := 0 -func setAuthHeaders(req *http.Request, authType string, args map[string]interface{}) error { - switch authType { - case "basic": - username, ok := args["username"].(string) - if !ok { - return fmt.Errorf("missing or invalid username") - } - password, ok := args["password"].(string) - if !ok { - return fmt.Errorf("missing or invalid password") + for retries <= maxRetries { + resp, err := client.Do(reqBody) + if err != nil { + return nil, errors.Wrap(err, "http request failed") } - - authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) - setHeaders(req, map[string]interface{}{ - "Authorization": authHeader, - }) - - case "api_key": - apiKey, ok := args["api_key"].(string) - if !ok { - return fmt.Errorf("missing or invalid API key") + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + return result, nil + } else if isRetryableStatusCode(resp.StatusCode) { + retries++ + backoffDuration := initialBackoff * time.Duration(1< Date: Tue, 19 Nov 2024 10:28:25 -0600 Subject: [PATCH 5/8] Fix casing to export --- utils/apiclient/client.go | 4 ++-- utils/apiclient/client_test.go | 9 ++------- utils/apiclient/main.go | 4 ++-- utils/apiclient/request_test.go | 14 +++++++------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index 6f89443b9f..7622dfda50 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -39,7 +39,7 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { return nil, errors.Wrap(err, "http request creation failed") } - SetAuthHeaders(reqBody, c.authType, c.authHeaders) + SetAuthHeaders(reqBody, c.AuthType, c.AuthHeaders) SetHeaders(reqBody, reqParams.Headers) client := c.HTTP if client == nil { @@ -74,7 +74,7 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { fmt.Printf("Received retryable status %d. Retrying in %v...\n", resp.StatusCode, backoffDuration) time.Sleep(backoffDuration) } else { - return nil, fmt.Errorf("Maximum retries reached after receiving status %d", resp.StatusCode) + return nil, fmt.Errorf("maximum retries reached after receiving status %d", resp.StatusCode) } } else { return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode) diff --git a/utils/apiclient/client_test.go b/utils/apiclient/client_test.go index 5e6aa946d6..707cbbcdda 100644 --- a/utils/apiclient/client_test.go +++ b/utils/apiclient/client_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_GetURL(t *testing.T) { +func TestGetURL(t *testing.T) { c := &APIClient{ BaseURL: "https://stellar.org", } @@ -23,13 +23,12 @@ func Test_GetURL(t *testing.T) { assert.Equal(t, "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", furl) } -func Test_CallAPI(t *testing.T) { +func TestCallAPI(t *testing.T) { testCases := []struct { name string mockResponses []httptest.ResponseData expected interface{} expectedError string - retries bool }{ { name: "status 200 - Success", @@ -38,7 +37,6 @@ func Test_CallAPI(t *testing.T) { }, expected: map[string]interface{}{"data": "Okay Response"}, expectedError: "", - retries: false, }, { name: "success with retries - status 429 and 503 then 200", @@ -50,7 +48,6 @@ func Test_CallAPI(t *testing.T) { }, expected: map[string]interface{}{"data": "Third Response"}, expectedError: "", - retries: true, }, { name: "failure - status 500", @@ -59,7 +56,6 @@ func Test_CallAPI(t *testing.T) { }, expected: nil, expectedError: "API request failed with status 500", - retries: false, }, { name: "failure - status 401", @@ -68,7 +64,6 @@ func Test_CallAPI(t *testing.T) { }, expected: nil, expectedError: "API request failed with status 401", - retries: false, }, } diff --git a/utils/apiclient/main.go b/utils/apiclient/main.go index 735df1e7f8..abdb855ca2 100644 --- a/utils/apiclient/main.go +++ b/utils/apiclient/main.go @@ -14,8 +14,8 @@ type HTTP interface { type APIClient struct { BaseURL string HTTP HTTP - authType string - authHeaders map[string]interface{} + AuthType string + AuthHeaders map[string]interface{} } type RequestParams struct { diff --git a/utils/apiclient/request_test.go b/utils/apiclient/request_test.go index 85dfc51751..4f1b43aa2e 100644 --- a/utils/apiclient/request_test.go +++ b/utils/apiclient/request_test.go @@ -6,14 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_CreateRequestBody_ValidURL(t *testing.T) { +func TestCreateRequestBodyValidURL(t *testing.T) { // Valid case req, err := CreateRequestBody("GET", "http://stellar.org") assert.NotNil(t, req) assert.NoError(t, err) } -func Test_CreateRequestBody_InvalidURL(t *testing.T) { +func TestCreateRequestBodyInvalidURL(t *testing.T) { invalidURL := "://invalid-url" req, err := CreateRequestBody("GET", invalidURL) assert.Nil(t, req) @@ -21,7 +21,7 @@ func Test_CreateRequestBody_InvalidURL(t *testing.T) { assert.Contains(t, err.Error(), "http GET request creation failed") } -func Test_SetHeaders_ValidHeaders(t *testing.T) { +func TestSetHeadersValidHeaders(t *testing.T) { req, err := CreateRequestBody("GET", "http://stellar.org") assert.NoError(t, err) @@ -34,7 +34,7 @@ func Test_SetHeaders_ValidHeaders(t *testing.T) { assert.Equal(t, "GoClient/1.0", req.Header.Get("User-Agent")) } -func Test_SetHeaders_InvalidHeaders(t *testing.T) { +func TestSetHeadersInvalidHeaders(t *testing.T) { req, err := CreateRequestBody("GET", "http://stellar.org") assert.NoError(t, err) @@ -51,15 +51,15 @@ func Test_SetHeaders_InvalidHeaders(t *testing.T) { assert.Equal(t, "Bearer token123", req.Header.Get("Authorization")) // Set correctly } -type SetAuthHeadersTestCase struct { +type setAuthHeadersTestCase struct { authType string args map[string]interface{} expectedHeader string expectedError string } -func Test_SetAuthHeaders(t *testing.T) { - testCases := []SetAuthHeadersTestCase{ +func TestSetAuthHeaders(t *testing.T) { + testCases := []setAuthHeadersTestCase{ { authType: "basic", args: map[string]interface{}{ From f5792cd3a40b6d35cab7367424685caa69fa6931 Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Mon, 2 Dec 2024 17:10:08 -0600 Subject: [PATCH 6/8] Feedback: change log, configurable retries, and wrap error --- support/http/httptest/client_expectation.go | 2 +- utils/apiclient/client.go | 29 +++++++++++++++------ utils/apiclient/client_test.go | 25 ++++++++---------- utils/apiclient/main.go | 11 +++++--- utils/apiclient/request.go | 3 ++- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/support/http/httptest/client_expectation.go b/support/http/httptest/client_expectation.go index 474f691285..e19bdc334f 100644 --- a/support/http/httptest/client_expectation.go +++ b/support/http/httptest/client_expectation.go @@ -105,7 +105,7 @@ func (ce *ClientExpectation) ReturnMultipleResults(responseSets []ResponseData) ce.URL, func(req *http.Request) (*http.Response, error) { if responseIndex >= len(allResponses) { - panic(fmt.Sprintf("no responses available")) + panic(fmt.Errorf("no responses available")) } resp := allResponses[responseIndex] diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index 7622dfda50..65fe194335 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -9,11 +9,12 @@ import ( "time" "github.com/pkg/errors" + "github.com/stellar/go/support/log" ) const ( - maxRetries = 5 - initialBackoff = 1 * time.Second + defaultMaxRetries = 5 + defaultInitialBackoffTime = 1 * time.Second ) func isRetryableStatusCode(statusCode int) bool { @@ -33,6 +34,18 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { reqParams.Headers = map[string]interface{}{} } + if c.MaxRetries == 0 { + c.MaxRetries = defaultMaxRetries + } + + if c.InitialBackoffTime == 0 { + c.InitialBackoffTime = defaultInitialBackoffTime + } + + if reqParams.Endpoint == "" { + return nil, fmt.Errorf("Please set endpoint to query") + } + url := c.GetURL(reqParams.Endpoint, reqParams.QueryParams) reqBody, err := CreateRequestBody(reqParams.RequestType, url) if err != nil { @@ -49,7 +62,7 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { var result interface{} retries := 0 - for retries <= maxRetries { + for retries <= c.MaxRetries { resp, err := client.Do(reqBody) if err != nil { return nil, errors.Wrap(err, "http request failed") @@ -59,19 +72,19 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { if resp.StatusCode == http.StatusOK { body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, errors.Wrap(err, "failed to read response body") } if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + return nil, errors.Wrap(err, "failed to unmarshal JSON") } return result, nil } else if isRetryableStatusCode(resp.StatusCode) { retries++ - backoffDuration := initialBackoff * time.Duration(1< Date: Mon, 2 Dec 2024 17:15:15 -0600 Subject: [PATCH 7/8] Rename variable --- utils/apiclient/client.go | 4 ++-- utils/apiclient/client_test.go | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index 65fe194335..ccb488d76e 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -21,8 +21,8 @@ func isRetryableStatusCode(statusCode int) bool { return statusCode == http.StatusTooManyRequests || statusCode == http.StatusServiceUnavailable } -func (c *APIClient) GetURL(endpoint string, qstr url.Values) string { - return fmt.Sprintf("%s/%s?%s", c.BaseURL, endpoint, qstr.Encode()) +func (c *APIClient) GetURL(endpoint string, queryParams url.Values) string { + return fmt.Sprintf("%s/%s?%s", c.BaseURL, endpoint, queryParams.Encode()) } func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { diff --git a/utils/apiclient/client_test.go b/utils/apiclient/client_test.go index 39eab7f988..2f9fdc62f4 100644 --- a/utils/apiclient/client_test.go +++ b/utils/apiclient/client_test.go @@ -14,12 +14,12 @@ func TestGetURL(t *testing.T) { BaseURL: "https://stellar.org", } - qstr := url.Values{} - qstr.Add("type", "forward") - qstr.Add("federation_type", "bank_account") - qstr.Add("swift", "BOPBPHMM") - qstr.Add("acct", "2382376") - furl := c.GetURL("federation", qstr) + queryParams := url.Values{} + queryParams.Add("type", "forward") + queryParams.Add("federation_type", "bank_account") + queryParams.Add("swift", "BOPBPHMM") + queryParams.Add("acct", "2382376") + furl := c.GetURL("federation", queryParams) assert.Equal(t, "https://stellar.org/federation?acct=2382376&federation_type=bank_account&swift=BOPBPHMM&type=forward", furl) } @@ -80,13 +80,13 @@ func TestCallAPI(t *testing.T) { HTTP: hmock, } - qstr := url.Values{} - qstr.Add("acct", "2382376") + queryParams := url.Values{} + queryParams.Add("acct", "2382376") reqParams := RequestParams{ RequestType: "GET", Endpoint: "federation", - QueryParams: qstr, + QueryParams: queryParams, } result, err := c.CallAPI(reqParams) From 66f236c8f1c2fb2a72c43ae20a315c44b8d15291 Mon Sep 17 00:00:00 2001 From: Amisha Singla Date: Tue, 3 Dec 2024 11:12:16 -0600 Subject: [PATCH 8/8] Use fmt error instead of error module as error module is onsolete --- utils/apiclient/client.go | 9 ++++----- utils/apiclient/request.go | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/utils/apiclient/client.go b/utils/apiclient/client.go index ccb488d76e..82501df3fc 100644 --- a/utils/apiclient/client.go +++ b/utils/apiclient/client.go @@ -8,7 +8,6 @@ import ( "net/url" "time" - "github.com/pkg/errors" "github.com/stellar/go/support/log" ) @@ -49,7 +48,7 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { url := c.GetURL(reqParams.Endpoint, reqParams.QueryParams) reqBody, err := CreateRequestBody(reqParams.RequestType, url) if err != nil { - return nil, errors.Wrap(err, "http request creation failed") + return nil, fmt.Errorf("http request creation failed") } SetAuthHeaders(reqBody, c.AuthType, c.AuthHeaders) @@ -65,18 +64,18 @@ func (c *APIClient) CallAPI(reqParams RequestParams) (interface{}, error) { for retries <= c.MaxRetries { resp, err := client.Do(reqBody) if err != nil { - return nil, errors.Wrap(err, "http request failed") + return nil, fmt.Errorf("http request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, errors.Wrap(err, "failed to read response body") + return nil, fmt.Errorf("failed to read response body: %w", err) } if err := json.Unmarshal(body, &result); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal JSON") + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) } return result, nil diff --git a/utils/apiclient/request.go b/utils/apiclient/request.go index 557f5b70eb..135ed1a437 100644 --- a/utils/apiclient/request.go +++ b/utils/apiclient/request.go @@ -5,14 +5,13 @@ import ( "fmt" "net/http" - "github.com/pkg/errors" "github.com/stellar/go/support/log" ) func CreateRequestBody(requestType string, url string) (*http.Request, error) { req, err := http.NewRequest(requestType, url, nil) if err != nil { - return nil, errors.Wrap(err, "http GET request creation failed") + return nil, fmt.Errorf("http GET request creation failed: %w", err) } return req, nil }