Skip to content

Commit 6b4d41c

Browse files
retry http requests (#197)
* retry http requests Implement simple request retrying to redue the probabiliyt that a single GET receiving a 503 will cause a whole CI job to need to be retried. Use hashicorp/go-retryablehttp since it fits this very simple use case well. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * lint fix: remove unused nolints Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> --------- Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
1 parent fa556ca commit 6b4d41c

File tree

7 files changed

+77
-25
lines changed

7 files changed

+77
-25
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
2424
github.com/google/yamlfmt v0.20.0
2525
github.com/hashicorp/go-multierror v1.1.1
26+
github.com/hashicorp/go-retryablehttp v0.7.8
2627
github.com/itchyny/gojq v0.12.17
2728
github.com/jedib0t/go-pretty v4.3.0+incompatible
2829
github.com/mholt/archiver/v3 v3.5.1
@@ -78,6 +79,7 @@ require (
7879
github.com/google/uuid v1.6.0 // indirect
7980
github.com/gookit/color v1.5.4 // indirect
8081
github.com/hashicorp/errwrap v1.1.0 // indirect
82+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
8183
github.com/hashicorp/hcl v1.0.0 // indirect
8284
github.com/huandu/xstrings v1.5.0 // indirect
8385
github.com/iancoleman/strcase v0.3.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
131131
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
132132
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
133133
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
134+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
135+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
134136
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
135137
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
136138
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
@@ -244,9 +246,15 @@ github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/Q
244246
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
245247
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
246248
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
249+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
250+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
251+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
252+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
247253
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
248254
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
249255
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
256+
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
257+
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
250258
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
251259
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
252260
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=

internal/download_file.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"github.com/go-git/go-git/v5/plumbing/hash"
15+
"github.com/hashicorp/go-retryablehttp"
1516

1617
"github.com/anchore/go-logger"
1718
)
@@ -53,7 +54,7 @@ func DownloadFile(lgr logger.Logger, url string, filepath string, checksum strin
5354
}
5455

5556
func DownloadURL(lgr logger.Logger, url string) (io.ReadCloser, error) {
56-
resp, err := http.Get(url) //nolint: gosec // we must be able to get arbitrary URLs
57+
resp, err := retryablehttp.Get(url)
5758
if err != nil {
5859
return nil, fmt.Errorf("unable to download %q: %w", url, err)
5960
}

tool/githubrelease/installer.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/scylladb/go-set/strset"
1818
"github.com/shurcooL/githubv4"
1919
"golang.org/x/net/html"
20-
"golang.org/x/oauth2"
2120

2221
"github.com/anchore/binny"
2322
"github.com/anchore/binny/internal"
@@ -822,18 +821,13 @@ func processExpandedAssets(lgr logger.Logger, reader io.Reader, from string) []g
822821
return assets
823822
}
824823

825-
//nolint:funlen
826824
func fetchReleaseGithubV4API(user, repo, tag string) (*ghRelease, error) {
827825
token := os.Getenv("GITHUB_TOKEN")
828826
if token == "" {
829827
return nil, fmt.Errorf("GITHUB_TOKEN environment variable not set but is required to use the GitHub v4 API")
830828
}
831-
src := oauth2.StaticTokenSource(
832-
// TODO: DI this
833-
&oauth2.Token{AccessToken: token},
834-
)
835-
httpClient := oauth2.NewClient(context.Background(), src)
836-
client := githubv4.NewClient(httpClient)
829+
830+
client := githubv4.NewClient(newRetryableGitHubClient(token))
837831

838832
// TODO: act on hitting a rate limit
839833
type rateLimit struct {

tool/githubrelease/retry_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package githubrelease
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNewRetryableGitHubClient_RetriesWithAuthHeader(t *testing.T) {
13+
var requests int
14+
var authHeaders []string
15+
16+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
requests++
18+
authHeaders = append(authHeaders, r.Header.Get("Authorization"))
19+
20+
if requests < 3 {
21+
w.WriteHeader(http.StatusServiceUnavailable)
22+
return
23+
}
24+
w.WriteHeader(http.StatusOK)
25+
}))
26+
defer server.Close()
27+
28+
token := "test-token-12345"
29+
client := newRetryableGitHubClient(token)
30+
31+
resp, err := client.Get(server.URL)
32+
require.NoError(t, err)
33+
defer resp.Body.Close()
34+
35+
assert.Equal(t, 3, requests, "expected 3 requests (2 retries)")
36+
expectedAuth := "Bearer " + token
37+
for i, auth := range authHeaders {
38+
assert.Equal(t, expectedAuth, auth, "request %d missing auth header", i+1)
39+
}
40+
}

tool/githubrelease/version_resolver.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/Masterminds/semver/v3"
15+
"github.com/hashicorp/go-retryablehttp"
1516
"github.com/shurcooL/githubv4"
1617
"golang.org/x/oauth2"
1718

@@ -209,19 +210,15 @@ func fetchLatestReleaseFromGithubFacade(user, repo string) (*ghRelease, error) {
209210
}
210211

211212
func downloadJSON(url string) (*http.Response, error) {
212-
headers := map[string]string{"Accept": "application/json"}
213+
client := retryablehttp.NewClient()
214+
client.HTTPClient.Timeout = 10 * time.Second
215+
client.Logger = nil
213216

214-
client := &http.Client{
215-
Timeout: time.Second * 10,
216-
}
217-
req, err := http.NewRequest(http.MethodGet, url, nil)
217+
req, err := retryablehttp.NewRequest(http.MethodGet, url, nil)
218218
if err != nil {
219219
return nil, err
220220
}
221-
222-
for key, value := range headers {
223-
req.Header.Set(key, value)
224-
}
221+
req.Header.Set("Accept", "application/json")
225222

226223
resp, err := client.Do(req)
227224
if err != nil {
@@ -233,18 +230,26 @@ func downloadJSON(url string) (*http.Response, error) {
233230
return resp, nil
234231
}
235232

233+
// newRetryableGitHubClient creates an HTTP client with OAuth2 authentication and retry logic.
234+
func newRetryableGitHubClient(token string) *http.Client {
235+
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
236+
oauth2Client := oauth2.NewClient(context.Background(), src)
237+
238+
retryClient := retryablehttp.NewClient()
239+
retryClient.HTTPClient.Transport = oauth2Client.Transport
240+
retryClient.Logger = nil
241+
242+
return retryClient.StandardClient()
243+
}
244+
236245
//nolint:funlen
237246
func fetchAllReleasesFromGithubV4API(user, repo string) ([]ghRelease, error) {
238247
token := os.Getenv("GITHUB_TOKEN")
239248
if token == "" {
240249
return nil, fmt.Errorf("GITHUB_TOKEN environment variable not set but is required to use the GitHub v4 API")
241250
}
242-
src := oauth2.StaticTokenSource(
243-
// TODO: DI this
244-
&oauth2.Token{AccessToken: token},
245-
)
246-
httpClient := oauth2.NewClient(context.Background(), src)
247-
client := githubv4.NewClient(httpClient)
251+
252+
client := githubv4.NewClient(newRetryableGitHubClient(token))
248253
var allReleases []ghRelease
249254

250255
// Query some details about a repository, an ghIssue in it, and its comments.

tool/goproxy/version_resolver.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"strings"
88

9+
"github.com/hashicorp/go-retryablehttp"
10+
911
"github.com/anchore/binny"
1012
"github.com/anchore/binny/internal"
1113
"github.com/anchore/binny/internal/log"
@@ -102,7 +104,7 @@ func availableVersionsFetcher(url string) ([]string, error) {
102104

103105
log.WithFields("url", url).Trace("requesting latest version")
104106

105-
resp, err := http.Get(url) //nolint:gosec
107+
resp, err := retryablehttp.Get(url)
106108
if err != nil {
107109
return nil, err
108110
}

0 commit comments

Comments
 (0)