Skip to content

Commit 4e9f38f

Browse files
committed
API-7853: add support for retry
add support for retry strategy add unit tests for retry strategy remove debug logs, add support for basic tokens, add docs for retry
1 parent fee9656 commit 4e9f38f

File tree

5 files changed

+153
-23
lines changed

5 files changed

+153
-23
lines changed

agent/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type agentAPI interface {
1515
UploadFile(string, []byte) (string, error)
1616
SetCustomHost(string)
1717
SetCustomHeader(string, string)
18+
SetRetryStrategy(i.RetryStrategyFunc)
1819
}
1920

2021
// API provides the API operation methods for making requests to Agent Chat API via Web API.

agent/api_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,33 @@ func createMockedErrorResponder(t *testing.T, method string) func(req *http.Requ
358358
}
359359
}
360360

361+
func createMockedMultipleAuthErrorsResponder(t *testing.T, fails int) func(req *http.Request) *http.Response {
362+
var n int
363+
364+
responseError := `{
365+
"error": {
366+
"type": "authentication",
367+
"message": "Invalid access token"
368+
}
369+
}`
370+
371+
return func(req *http.Request) *http.Response {
372+
n++
373+
if n > fails {
374+
return &http.Response{
375+
StatusCode: http.StatusOK,
376+
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
377+
Header: make(http.Header),
378+
}
379+
}
380+
return &http.Response{
381+
StatusCode: http.StatusUnauthorized,
382+
Body: ioutil.NopCloser(bytes.NewBufferString(responseError)),
383+
Header: make(http.Header),
384+
}
385+
}
386+
}
387+
361388
func verifyErrorResponse(method string, resp error, t *testing.T) {
362389
if resp == nil {
363390
t.Errorf("%v should fail", method)
@@ -1487,3 +1514,61 @@ func TestInvalidAuthorizationScheme(t *testing.T) {
14871514
t.Errorf("Err should not be nil")
14881515
}
14891516
}
1517+
1518+
func TestRetryStrategyAllFails(t *testing.T) {
1519+
client := NewTestClient(createMockedMultipleAuthErrorsResponder(t, 10))
1520+
1521+
api, err := agent.NewAPI(stubTokenGetter(authorization.BearerToken), client, "client_id")
1522+
if err != nil {
1523+
t.Errorf("API creation failed")
1524+
}
1525+
1526+
var retries uint
1527+
api.SetRetryStrategy(func(attempts uint, err error) bool {
1528+
if attempts < 3 {
1529+
retries++
1530+
return true
1531+
}
1532+
1533+
return false
1534+
})
1535+
1536+
err = api.Call("", nil, nil)
1537+
if err == nil {
1538+
t.Errorf("Err should not be nil")
1539+
}
1540+
1541+
if retries != 3 {
1542+
t.Errorf("Retries should be done 3 times")
1543+
}
1544+
1545+
}
1546+
1547+
func TestRetryStrategyLastSuccess(t *testing.T) {
1548+
client := NewTestClient(createMockedMultipleAuthErrorsResponder(t, 2))
1549+
1550+
api, err := agent.NewAPI(stubTokenGetter(authorization.BearerToken), client, "client_id")
1551+
if err != nil {
1552+
t.Errorf("API creation failed")
1553+
}
1554+
1555+
var retries uint
1556+
api.SetRetryStrategy(func(attempts uint, err error) bool {
1557+
if attempts < 3 {
1558+
retries++
1559+
return true
1560+
}
1561+
1562+
return false
1563+
})
1564+
1565+
err = api.Call("", nil, &struct{}{})
1566+
if err != nil {
1567+
t.Errorf("Err should be nil after 2 retries")
1568+
}
1569+
1570+
if retries != 2 {
1571+
t.Errorf("Retries should be done 2 times")
1572+
}
1573+
1574+
}

configuration/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
type configurationAPI interface {
1313
Call(string, interface{}, interface{}) error
1414
SetCustomHost(string)
15+
SetRetryStrategy(i.RetryStrategyFunc)
1516
}
1617

1718
// API provides the API operation methods for making requests to Livechat Configuration API via Web API.

customer/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type customerAPI interface {
1414
Call(string, interface{}, interface{}) error
1515
UploadFile(string, []byte) (string, error)
1616
SetCustomHost(string)
17+
SetRetryStrategy(i.RetryStrategyFunc)
1718
}
1819

1920
// API provides the API operation methods for making requests to Customer Chat API via Web API.

internal/web_api.go

+65-23
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,22 @@ import (
1616

1717
const apiVersion = "3.2"
1818

19+
// RetryStrategyFunc is called by each API method if set to retry when handling an error.
20+
// If not set, there will be no retry at all.
21+
//
22+
// It accepts two arguments: attempts - number of sent requests (starting from 0)
23+
// and err - error as ErrAPI struct (with StatusCode and Details)
24+
// It returns info whether to retry the request.
25+
type RetryStrategyFunc func(attempts uint, err error) bool
26+
1927
type api struct {
2028
httpClient *http.Client
2129
clientID string
2230
tokenGetter authorization.TokenGetter
2331
httpRequestGenerator HTTPRequestGenerator
2432
host string
2533
customHeaders http.Header
34+
retryStrategy RetryStrategyFunc
2635
}
2736

2837
// HTTPRequestGenerator is called by each API method to generate api http url.
@@ -59,12 +68,9 @@ func (a *api) Call(action string, reqPayload interface{}, respPayload interface{
5968
if err != nil {
6069
return err
6170
}
62-
token := a.tokenGetter()
63-
if token == nil {
64-
return fmt.Errorf("couldn't get token")
65-
}
66-
if token.Type != authorization.BearerToken && token.Type != authorization.BasicToken {
67-
return fmt.Errorf("unsupported token type")
71+
token, err := a.getToken()
72+
if err != nil {
73+
return err
6874
}
6975

7076
req, err := a.httpRequestGenerator(token, a.host, action)
@@ -93,6 +99,11 @@ func (a *api) SetCustomHeader(key, val string) {
9399
a.customHeaders.Set(key, val)
94100
}
95101

102+
// SetRetryStrategy allows to set a retry strategy that will be performed in every failed request
103+
func (a *api) SetRetryStrategy(f RetryStrategyFunc) {
104+
a.retryStrategy = f
105+
}
106+
96107
type fileUploadAPI struct{ *api }
97108

98109
// NewAPIWithFileUpload returns ready to use raw API client with file upload functionality.
@@ -133,7 +144,7 @@ func (a *fileUploadAPI) UploadFile(filename string, file []byte) (string, error)
133144
req.Body = ioutil.NopCloser(body)
134145

135146
req.Header.Set("Content-Type", writer.FormDataContentType())
136-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
147+
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type, token.AccessToken))
137148
req.Header.Set("User-agent", fmt.Sprintf("GO SDK Application %s", a.clientID))
138149
req.Header.Set("X-Region", token.Region)
139150

@@ -145,28 +156,59 @@ func (a *fileUploadAPI) UploadFile(filename string, file []byte) (string, error)
145156
}
146157

147158
func (a *api) send(req *http.Request, respPayload interface{}) error {
148-
resp, err := a.httpClient.Do(req)
149-
if err != nil {
150-
return err
151-
}
152-
defer resp.Body.Close()
153-
bodyBytes, err := ioutil.ReadAll(resp.Body)
154-
if resp.StatusCode != http.StatusOK {
155-
apiErr := &api_errors.ErrAPI{}
156-
if err := json.Unmarshal(bodyBytes, apiErr); err != nil {
157-
return fmt.Errorf("couldn't unmarshal error response: %s (code: %d, raw body: %s)", err.Error(), resp.StatusCode, string(bodyBytes))
159+
var attempts uint
160+
var do func() error
161+
162+
do = func() error {
163+
resp, err := a.httpClient.Do(req)
164+
if err != nil {
165+
return err
158166
}
159-
if apiErr.Error() == "" {
160-
return fmt.Errorf("couldn't unmarshal error response (code: %d, raw body: %s)", resp.StatusCode, string(bodyBytes))
167+
defer resp.Body.Close()
168+
bodyBytes, err := ioutil.ReadAll(resp.Body)
169+
if resp.StatusCode != http.StatusOK {
170+
apiErr := &api_errors.ErrAPI{}
171+
if err := json.Unmarshal(bodyBytes, apiErr); err != nil {
172+
return fmt.Errorf("couldn't unmarshal error response: %s (code: %d, raw body: %s)", err.Error(), resp.StatusCode, string(bodyBytes))
173+
}
174+
if apiErr.Error() == "" {
175+
return fmt.Errorf("couldn't unmarshal error response (code: %d, raw body: %s)", resp.StatusCode, string(bodyBytes))
176+
}
177+
178+
if a.retryStrategy == nil || !a.retryStrategy(attempts, apiErr) {
179+
return apiErr
180+
}
181+
182+
token, err := a.getToken()
183+
if err != nil {
184+
return err
185+
}
186+
187+
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type, token.AccessToken))
188+
attempts++
189+
return do()
190+
}
191+
192+
if err != nil {
193+
return err
161194
}
162-
return apiErr
195+
196+
return json.Unmarshal(bodyBytes, respPayload)
163197
}
164198

165-
if err != nil {
166-
return err
199+
return do()
200+
}
201+
202+
func (a *api) getToken() (*authorization.Token, error) {
203+
token := a.tokenGetter()
204+
if token == nil {
205+
return nil, fmt.Errorf("couldn't get token")
206+
}
207+
if token.Type != authorization.BearerToken && token.Type != authorization.BasicToken {
208+
return nil, fmt.Errorf("unsupported token type")
167209
}
168210

169-
return json.Unmarshal(bodyBytes, respPayload)
211+
return token, nil
170212
}
171213

172214
// SetCustomHost allows to change API host address. This method is mostly for LiveChat internal testing and should not be used in production environments.

0 commit comments

Comments
 (0)