Skip to content

Commit d4ef245

Browse files
Merge pull request #1025 from Checkmarx/feature/saraChen/AddRetryToHttpCall
Add retry mechanism to 502 response (AST-82566)
2 parents 38ff94d + 4216470 commit d4ef245

File tree

6 files changed

+178
-24
lines changed

6 files changed

+178
-24
lines changed

internal/wrappers/client.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ var cachedAccessToken string
7070
var cachedAccessTime time.Time
7171
var Domains = make(map[string]struct{})
7272

73+
func retryHTTPRequest(requestFunc func() (*http.Response, error), retries int, baseDelayInMilliSec time.Duration) (*http.Response, error) {
74+
75+
var resp *http.Response
76+
var err error
77+
78+
for attempt := 0; attempt < retries; attempt++ {
79+
resp, err = requestFunc()
80+
if err != nil {
81+
return nil, err
82+
}
83+
if resp.StatusCode != http.StatusBadGateway {
84+
return resp, nil
85+
}
86+
_ = resp.Body.Close()
87+
time.Sleep(baseDelayInMilliSec * (1 << attempt))
88+
}
89+
return resp, nil
90+
}
91+
7392
func setAgentName(req *http.Request) {
7493
agentStr := viper.GetString(commonParams.AgentNameKey) + "/" + commonParams.Version
7594
req.Header.Set("User-Agent", agentStr)

internal/wrappers/client_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package wrappers
2+
3+
import (
4+
"errors"
5+
"github.com/stretchr/testify/assert"
6+
"net/http"
7+
"testing"
8+
"time"
9+
)
10+
11+
type mockReadCloser struct{}
12+
13+
func (m *mockReadCloser) Read(p []byte) (n int, err error) {
14+
return 0, nil
15+
}
16+
17+
func (m *mockReadCloser) Close() error {
18+
return nil
19+
}
20+
21+
func TestRetryHTTPRequest_Success(t *testing.T) {
22+
fn := func() (*http.Response, error) {
23+
return &http.Response{
24+
StatusCode: http.StatusOK,
25+
Body: &mockReadCloser{},
26+
}, nil
27+
}
28+
29+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
30+
assert.NoError(t, err)
31+
assert.NotNil(t, resp)
32+
assert.Equal(t, http.StatusOK, resp.StatusCode)
33+
}
34+
35+
func TestRetryHTTPRequest_RetryOnBadGateway(t *testing.T) {
36+
attempts := 0
37+
fn := func() (*http.Response, error) {
38+
attempts++
39+
if attempts < retryAttempts {
40+
return &http.Response{
41+
StatusCode: http.StatusBadGateway,
42+
Body: &mockReadCloser{},
43+
}, nil
44+
}
45+
return &http.Response{
46+
StatusCode: http.StatusOK,
47+
Body: &mockReadCloser{},
48+
}, nil
49+
}
50+
51+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
52+
assert.NoError(t, err)
53+
assert.NotNil(t, resp)
54+
assert.Equal(t, http.StatusOK, resp.StatusCode)
55+
assert.Equal(t, retryAttempts, attempts)
56+
}
57+
58+
func TestRetryHTTPRequest_Fail(t *testing.T) {
59+
fn := func() (*http.Response, error) {
60+
return nil, errors.New("network error")
61+
}
62+
63+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
64+
assert.Error(t, err)
65+
assert.Nil(t, resp)
66+
}
67+
68+
func TestRetryHTTPRequest_EndWithBadGateway(t *testing.T) {
69+
fn := func() (*http.Response, error) {
70+
return &http.Response{
71+
StatusCode: http.StatusBadGateway,
72+
Body: &mockReadCloser{},
73+
}, nil
74+
}
75+
76+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
77+
assert.NoError(t, err)
78+
assert.NotNil(t, resp)
79+
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
80+
}

internal/wrappers/constants.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package wrappers
2+
3+
const (
4+
limitValue = "10000"
5+
retryAttempts = 4
6+
retryDelay = 500
7+
)

internal/wrappers/projects-http.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"net/http"
8-
97
"github.com/pkg/errors"
108
"github.com/spf13/viper"
9+
"net/http"
10+
"time"
1111

1212
errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
1313
commonParams "github.com/checkmarx/ast-cli/internal/params"
@@ -30,7 +30,10 @@ func (p *ProjectsHTTPWrapper) Create(model *Project) (*ProjectResponseModel, *Er
3030
return nil, nil, err
3131
}
3232

33-
resp, err := SendHTTPRequest(http.MethodPost, p.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
33+
fn := func() (*http.Response, error) {
34+
return SendHTTPRequest(http.MethodPost, p.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
35+
}
36+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
3437
if err != nil {
3538
return nil, nil, err
3639
}
@@ -49,7 +52,10 @@ func (p *ProjectsHTTPWrapper) Update(projectID string, model *Project) error {
4952
return nil
5053
}
5154

52-
resp, err := SendHTTPRequest(http.MethodPut, fmt.Sprintf("%s/%s", p.path, projectID), bytes.NewBuffer(jsonBytes), true, clientTimeout)
55+
fn := func() (*http.Response, error) {
56+
return SendHTTPRequest(http.MethodPut, fmt.Sprintf("%s/%s", p.path, projectID), bytes.NewBuffer(jsonBytes), true, clientTimeout)
57+
}
58+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
5359
if err != nil {
5460
return err
5561
}
@@ -79,7 +85,10 @@ func (p *ProjectsHTTPWrapper) UpdateConfiguration(projectID string, configuratio
7985
commonParams.ProjectIDFlag: projectID,
8086
}
8187

82-
resp, err := SendHTTPRequestWithQueryParams(http.MethodPatch, "api/configuration/project", params, bytes.NewBuffer(jsonBytes), clientTimeout)
88+
fn := func() (*http.Response, error) {
89+
return SendHTTPRequestWithQueryParams(http.MethodPatch, "api/configuration/project", params, bytes.NewBuffer(jsonBytes), clientTimeout)
90+
}
91+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
8392
if err != nil {
8493
return nil, err
8594
}
@@ -100,7 +109,10 @@ func (p *ProjectsHTTPWrapper) Get(params map[string]string) (
100109
params[limit] = limitValue
101110
}
102111

103-
resp, err := SendHTTPRequestWithQueryParams(http.MethodGet, p.path, params, nil, clientTimeout)
112+
fn := func() (*http.Response, error) {
113+
return SendHTTPRequestWithQueryParams(http.MethodGet, p.path, params, nil, clientTimeout)
114+
}
115+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
104116
if err != nil {
105117
return nil, nil, err
106118
}
@@ -137,7 +149,11 @@ func (p *ProjectsHTTPWrapper) GetByID(projectID string) (
137149
*ErrorModel,
138150
error) {
139151
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
140-
resp, err := SendHTTPRequest(http.MethodGet, p.path+"/"+projectID, http.NoBody, true, clientTimeout)
152+
153+
fn := func() (*http.Response, error) {
154+
return SendHTTPRequest(http.MethodGet, p.path+"/"+projectID, http.NoBody, true, clientTimeout)
155+
}
156+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
141157
if err != nil {
142158
return nil, nil, err
143159
}
@@ -180,7 +196,11 @@ func (p *ProjectsHTTPWrapper) GetBranchesByID(projectID string, params map[strin
180196
var request = "/branches?project-id=" + projectID
181197

182198
params["limit"] = limitValue
183-
resp, err := SendHTTPRequestWithQueryParams(http.MethodGet, p.path+request, params, nil, clientTimeout)
199+
200+
fn := func() (*http.Response, error) {
201+
return SendHTTPRequestWithQueryParams(http.MethodGet, p.path+request, params, nil, clientTimeout)
202+
}
203+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
184204
if err != nil {
185205
return nil, nil, err
186206
}
@@ -215,7 +235,10 @@ func (p *ProjectsHTTPWrapper) GetBranchesByID(projectID string, params map[strin
215235

216236
func (p *ProjectsHTTPWrapper) Delete(projectID string) (*ErrorModel, error) {
217237
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
218-
resp, err := SendHTTPRequest(http.MethodDelete, p.path+"/"+projectID, http.NoBody, true, clientTimeout)
238+
fn := func() (*http.Response, error) {
239+
return SendHTTPRequest(http.MethodDelete, p.path+"/"+projectID, http.NoBody, true, clientTimeout)
240+
}
241+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
219242
if err != nil {
220243
return nil, err
221244
}
@@ -232,7 +255,11 @@ func (p *ProjectsHTTPWrapper) Tags() (
232255
*ErrorModel,
233256
error) {
234257
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
235-
resp, err := SendHTTPRequest(http.MethodGet, p.path+"/tags", http.NoBody, true, clientTimeout)
258+
259+
fn := func() (*http.Response, error) {
260+
return SendHTTPRequest(http.MethodGet, p.path+"/tags", http.NoBody, true, clientTimeout)
261+
}
262+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay*time.Millisecond)
236263
if err != nil {
237264
return nil, nil, err
238265
}

internal/wrappers/scans-http.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"net/http"
8-
97
commonParams "github.com/checkmarx/ast-cli/internal/params"
108
"github.com/pkg/errors"
119
"github.com/spf13/viper"
10+
"net/http"
1211
)
1312

1413
const (
@@ -35,7 +34,11 @@ func (s *ScansHTTPWrapper) Create(model *Scan) (*ScanResponseModel, *ErrorModel,
3534
if err != nil {
3635
return nil, nil, err
3736
}
38-
resp, err := SendHTTPRequest(http.MethodPost, s.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
37+
38+
fn := func() (*http.Response, error) {
39+
return SendHTTPRequest(http.MethodPost, s.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
40+
}
41+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
3942
if err != nil {
4043
return nil, nil, err
4144
}
@@ -49,7 +52,11 @@ func (s *ScansHTTPWrapper) Create(model *Scan) (*ScanResponseModel, *ErrorModel,
4952

5053
func (s *ScansHTTPWrapper) Get(params map[string]string) (*ScansCollectionResponseModel, *ErrorModel, error) {
5154
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
52-
resp, err := SendHTTPRequestWithQueryParams(http.MethodGet, s.path, params, nil, clientTimeout)
55+
56+
fn := func() (*http.Response, error) {
57+
return SendHTTPRequestWithQueryParams(http.MethodGet, s.path, params, nil, clientTimeout)
58+
}
59+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
5360
if err != nil {
5461
return nil, nil, err
5562
}
@@ -85,7 +92,11 @@ func (s *ScansHTTPWrapper) Get(params map[string]string) (*ScansCollectionRespon
8592

8693
func (s *ScansHTTPWrapper) GetByID(scanID string) (*ScanResponseModel, *ErrorModel, error) {
8794
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
88-
resp, err := SendHTTPRequest(http.MethodGet, s.path+"/"+scanID, http.NoBody, true, clientTimeout)
95+
96+
fn := func() (*http.Response, error) {
97+
return SendHTTPRequest(http.MethodGet, s.path+"/"+scanID, http.NoBody, true, clientTimeout)
98+
}
99+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
89100
if err != nil {
90101
return nil, nil, err
91102
}
@@ -100,7 +111,11 @@ func (s *ScansHTTPWrapper) GetByID(scanID string) (*ScanResponseModel, *ErrorMod
100111
func (s *ScansHTTPWrapper) GetWorkflowByID(scanID string) ([]*ScanTaskResponseModel, *ErrorModel, error) {
101112
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
102113
path := fmt.Sprintf("%s/%s/workflow", s.path, scanID)
103-
resp, err := SendHTTPRequest(http.MethodGet, path, http.NoBody, true, clientTimeout)
114+
115+
fn := func() (*http.Response, error) {
116+
return SendHTTPRequest(http.MethodGet, path, http.NoBody, true, clientTimeout)
117+
}
118+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
104119
if err != nil {
105120
return nil, nil, err
106121
}
@@ -141,7 +156,11 @@ func handleWorkflowResponseWithBody(resp *http.Response, err error) ([]*ScanTask
141156

142157
func (s *ScansHTTPWrapper) Delete(scanID string) (*ErrorModel, error) {
143158
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
144-
resp, err := SendHTTPRequest(http.MethodDelete, s.path+"/"+scanID, http.NoBody, true, clientTimeout)
159+
160+
fn := func() (*http.Response, error) {
161+
return SendHTTPRequest(http.MethodDelete, s.path+"/"+scanID, http.NoBody, true, clientTimeout)
162+
}
163+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
145164
if err != nil {
146165
return nil, err
147166
}
@@ -162,7 +181,10 @@ func (s *ScansHTTPWrapper) Cancel(scanID string) (*ErrorModel, error) {
162181
return nil, err
163182
}
164183

165-
resp, err := SendHTTPRequest(http.MethodPatch, s.path+"/"+scanID, bytes.NewBuffer(b), true, clientTimeout)
184+
fn := func() (*http.Response, error) {
185+
return SendHTTPRequest(http.MethodPatch, s.path+"/"+scanID, bytes.NewBuffer(b), true, clientTimeout)
186+
}
187+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
166188
if err != nil {
167189
return nil, err
168190
}
@@ -176,7 +198,11 @@ func (s *ScansHTTPWrapper) Cancel(scanID string) (*ErrorModel, error) {
176198

177199
func (s *ScansHTTPWrapper) Tags() (map[string][]string, *ErrorModel, error) {
178200
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
179-
resp, err := SendHTTPRequest(http.MethodGet, s.path+"/tags", http.NoBody, true, clientTimeout)
201+
202+
fn := func() (*http.Response, error) {
203+
return SendHTTPRequest(http.MethodGet, s.path+"/tags", http.NoBody, true, clientTimeout)
204+
}
205+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
180206
if err != nil {
181207
return nil, nil, err
182208
}

internal/wrappers/wrapper-constants.go

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)