Skip to content

Commit 88d3724

Browse files
authored
Allow retrying on non-HTTP errors (#529)
787006c added support for retrying HTTP errors, this extend that to retry when an HTTP connection cannot be made. This helps when bazelisk failes to speak HTTP: 2024/01/16 14:31:11 could not download Bazel: HTTP GET https://releases.bazel.build/7.0.0/release/bazel-7.0.0-linux-x86_64 failed: Get "https://releases.bazel.build/7.0.0/release/bazel-7.0.0-linux-x86_64": dial tcp 130.211.22.235:443: i/o timeout Fixes: #432
1 parent 577ec3e commit 88d3724

File tree

3 files changed

+72
-23
lines changed

3 files changed

+72
-23
lines changed

httputil/fake.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,48 @@ func NewFakeTransport() *FakeTransport {
1818
}
1919
}
2020

21-
// AddResponse stores a fake HTTP response for the given URL.
22-
func (ft *FakeTransport) AddResponse(url string, status int, body string, headers map[string]string) {
21+
func (ft *FakeTransport) responseCollection(url string) *responseCollection {
2322
if _, ok := ft.responses[url]; !ok {
2423
ft.responses[url] = &responseCollection{}
2524
}
25+
return ft.responses[url]
26+
}
27+
28+
// AddResponse stores a fake HTTP response for the given URL.
29+
func (ft *FakeTransport) AddResponse(url string, status int, body string, headers map[string]string) {
30+
ft.responseCollection(url).Add(createResponse(status, body, headers), nil)
31+
}
32+
33+
// AddResponse stores a error for the given URL.
34+
func (ft *FakeTransport) AddError(url string, err error) {
35+
ft.responseCollection(url).Add(nil, err)
2636

27-
ft.responses[url].Add(createResponse(status, body, headers))
2837
}
2938

3039
// RoundTrip returns a prerecorded response to the given request, if one exists. Otherwise its response indicates 404 - not found.
3140
func (ft *FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
3241
if responses, ok := ft.responses[req.URL.String()]; ok {
33-
return responses.Next(), nil
42+
return responses.Next()
3443
}
3544
return notFound(), nil
3645
}
3746

3847
type responseCollection struct {
39-
all []*http.Response
48+
all []responseError
4049
next int
4150
}
4251

43-
func (rc *responseCollection) Add(resp *http.Response) {
44-
rc.all = append(rc.all, resp)
52+
func (rc *responseCollection) Add(resp *http.Response, err error) {
53+
rc.all = append(rc.all, responseError{resp: resp, err: err})
4554
}
4655

47-
func (rc *responseCollection) Next() *http.Response {
56+
func (rc *responseCollection) Next() (*http.Response, error) {
4857
if rc.next >= len(rc.all) {
49-
return notFound()
58+
return notFound(), nil
5059
}
5160
rc.next++
52-
return rc.all[rc.next-1]
61+
next := rc.all[rc.next-1]
62+
return next.resp, next.err
5363
}
5464

5565
func createResponse(status int, body string, headers map[string]string) *http.Response {
@@ -71,3 +81,8 @@ func transformHeaders(original map[string]string) http.Header {
7181
func notFound() *http.Response {
7282
return createResponse(http.StatusNotFound, "", nil)
7383
}
84+
85+
type responseError struct {
86+
resp *http.Response
87+
err error
88+
}

httputil/httputil.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,19 @@ func get(url, auth string) (*http.Response, error) {
8989
}
9090
client := &http.Client{Transport: DefaultTransport}
9191
deadline := RetryClock.Now().Add(MaxRequestDuration)
92-
lastStatus := 0
92+
var lastFailure string
9393
for attempt := 0; attempt <= MaxRetries; attempt++ {
9494
res, err := client.Do(req)
95-
// Do not retry on success and permanent/fatal errors
96-
if err != nil || !shouldRetry(res) {
95+
if !shouldRetry(res, err) {
9796
return res, err
9897
}
9998

100-
lastStatus = res.StatusCode
101-
waitFor, err := getWaitPeriod(res, attempt)
99+
if err == nil {
100+
lastFailure = fmt.Sprintf("HTTP %d", res.StatusCode)
101+
} else {
102+
lastFailure = err.Error()
103+
}
104+
waitFor, err := getWaitPeriod(res, err, attempt)
102105
if err != nil {
103106
return nil, err
104107
}
@@ -111,18 +114,25 @@ func get(url, auth string) (*http.Response, error) {
111114
RetryClock.Sleep(waitFor)
112115
}
113116
}
114-
return nil, fmt.Errorf("unable to complete request to %s after %d retries. Most recent status: %d", url, MaxRetries, lastStatus)
117+
return nil, fmt.Errorf("unable to complete request to %s after %d retries. Most recent failure: %s", url, MaxRetries, lastFailure)
115118
}
116119

117-
func shouldRetry(res *http.Response) bool {
120+
func shouldRetry(res *http.Response, err error) bool {
121+
// Retry if the client failed to speak HTTP.
122+
if err != nil {
123+
return true
124+
}
125+
// For HTTP: only retry on permanent/fatal errors.
118126
return res.StatusCode == 429 || (500 <= res.StatusCode && res.StatusCode <= 504)
119127
}
120128

121-
func getWaitPeriod(res *http.Response, attempt int) (time.Duration, error) {
122-
// Check if the server told us when to retry
123-
for _, header := range retryHeaders {
124-
if value := res.Header[header]; len(value) > 0 {
125-
return parseRetryHeader(value[0])
129+
func getWaitPeriod(res *http.Response, err error, attempt int) (time.Duration, error) {
130+
if err == nil {
131+
// If HTTP works, check if the server told us when to retry
132+
for _, header := range retryHeaders {
133+
if value := res.Header[header]; len(value) > 0 {
134+
return parseRetryHeader(value[0])
135+
}
126136
}
127137
}
128138
// Let's just use exponential backoff: 1s + d1, 2s + d2, 4s + d3, 8s + d4 with dx being a random value in [0ms, 500ms]

httputil/httputil_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package httputil
22

33
import (
4+
"errors"
45
"net/http"
56
"strconv"
67
"testing"
@@ -93,6 +94,29 @@ func TestSuccessOnRetry(t *testing.T) {
9394
}
9495
}
9596

97+
func TestSuccessOnRetryNonHTTPError(t *testing.T) {
98+
transport, clock := setUp()
99+
100+
url := "http://foo"
101+
want := "the_body"
102+
transport.AddError(url, errors.New("boom!"))
103+
transport.AddResponse(url, 200, want, nil)
104+
body, _, err := ReadRemoteFile(url, "")
105+
106+
if err != nil {
107+
t.Fatalf("Unexpected error %v", err)
108+
}
109+
110+
got := string(body)
111+
if got != want {
112+
t.Fatalf("Expected body %q, but got %q", want, got)
113+
}
114+
115+
if clock.TimesSlept() != 1 {
116+
t.Fatalf("Expected a single retry, not %d", clock.TimesSlept())
117+
}
118+
}
119+
96120
func TestAllTriesFail(t *testing.T) {
97121
MaxRequestDuration = 100 * time.Second
98122

@@ -106,7 +130,7 @@ func TestAllTriesFail(t *testing.T) {
106130
}
107131

108132
reason := err.Error()
109-
expected := "could not fetch http://bar: unable to complete request to http://bar after 5 retries. Most recent status: 502"
133+
expected := "could not fetch http://bar: unable to complete request to http://bar after 5 retries. Most recent failure: HTTP 502"
110134
if reason != expected {
111135
t.Fatalf("Expected request to fail with %q, but got %q", expected, reason)
112136
}

0 commit comments

Comments
 (0)