Skip to content

Commit a49355c

Browse files
Alex Vaghinbradfitz
Alex Vaghin
authored andcommittedJun 21, 2018
acme: consistently return original errors from retries
The retry logic returns an "acme: no more retries for ..." error in some cases, while *Error type in others. This change makes retries always return the last error as received from the CA server, if available. No change in returned values of successful requests. Change-Id: I3df2cb332a3e2739bba457c0ee50d7ca5bd836d9 Reviewed-on: https://go-review.googlesource.com/119975 Reviewed-by: Maciej Dębski <maciejd@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Run-TryBot: Alex Vaghin <ddos@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
1 parent 7f39a6f commit a49355c

File tree

2 files changed

+67
-11
lines changed

2 files changed

+67
-11
lines changed
 

‎acme/http.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,13 @@ func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Respons
140140
case ok(res):
141141
return res, nil
142142
case isRetriable(res.StatusCode):
143-
res.Body.Close()
144143
retry.inc()
145-
if err := retry.backoff(ctx, req, res); err != nil {
146-
return nil, err
144+
resErr := responseError(res)
145+
res.Body.Close()
146+
// Ignore the error value from retry.backoff
147+
// and return the one from last retry, as received from the CA.
148+
if retry.backoff(ctx, req, res) != nil {
149+
return nil, resErr
147150
}
148151
default:
149152
defer res.Body.Close()
@@ -169,20 +172,22 @@ func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body i
169172
if ok(res) {
170173
return res, nil
171174
}
172-
err = responseError(res)
175+
resErr := responseError(res)
173176
res.Body.Close()
174177
switch {
175178
// Check for bad nonce before isRetriable because it may have been returned
176179
// with an unretriable response code such as 400 Bad Request.
177-
case isBadNonce(err):
180+
case isBadNonce(resErr):
178181
// Consider any previously stored nonce values to be invalid.
179182
c.clearNonces()
180183
case !isRetriable(res.StatusCode):
181-
return nil, err
184+
return nil, resErr
182185
}
183186
retry.inc()
187+
// Ignore the error value from retry.backoff
188+
// and return the one from last retry, as received from the CA.
184189
if err := retry.backoff(ctx, req, res); err != nil {
185-
return nil, err
190+
return nil, resErr
186191
}
187192
}
188193
}

‎acme/http_test.go

+55-4
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,14 @@ func TestPostWithRetries(t *testing.T) {
8888
}
8989

9090
head, err := decodeJWSHead(r)
91-
if err != nil {
91+
switch {
92+
case err != nil:
9293
t.Errorf("decodeJWSHead: %v", err)
93-
} else if head.Nonce == "" {
94+
case head.Nonce == "":
9495
t.Error("head.Nonce is empty")
95-
} else if head.Nonce == "nonce1" {
96-
// return a badNonce error to force the call to retry
96+
case head.Nonce == "nonce1":
97+
// Return a badNonce error to force the call to retry.
98+
w.Header().Set("Retry-After", "0")
9799
w.WriteHeader(http.StatusBadRequest)
98100
w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
99101
return
@@ -114,6 +116,55 @@ func TestPostWithRetries(t *testing.T) {
114116
}
115117
}
116118

119+
func TestRetryErrorType(t *testing.T) {
120+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121+
w.Header().Set("Replay-Nonce", "nonce")
122+
w.WriteHeader(http.StatusTooManyRequests)
123+
w.Write([]byte(`{"type":"rateLimited"}`))
124+
}))
125+
defer ts.Close()
126+
127+
client := &Client{
128+
Key: testKey,
129+
RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
130+
// Do no retries.
131+
return 0
132+
},
133+
dir: &Directory{AuthzURL: ts.URL},
134+
}
135+
136+
t.Run("post", func(t *testing.T) {
137+
testRetryErrorType(t, func() error {
138+
_, err := client.Authorize(context.Background(), "example.com")
139+
return err
140+
})
141+
})
142+
t.Run("get", func(t *testing.T) {
143+
testRetryErrorType(t, func() error {
144+
_, err := client.GetAuthorization(context.Background(), ts.URL)
145+
return err
146+
})
147+
})
148+
}
149+
150+
func testRetryErrorType(t *testing.T, callClient func() error) {
151+
t.Helper()
152+
err := callClient()
153+
if err == nil {
154+
t.Fatal("client.Authorize returned nil error")
155+
}
156+
acmeErr, ok := err.(*Error)
157+
if !ok {
158+
t.Fatalf("err is %v (%T); want *Error", err, err)
159+
}
160+
if acmeErr.StatusCode != http.StatusTooManyRequests {
161+
t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
162+
}
163+
if acmeErr.ProblemType != "rateLimited" {
164+
t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
165+
}
166+
}
167+
117168
func TestRetryBackoffArgs(t *testing.T) {
118169
const resCode = http.StatusInternalServerError
119170
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)
Please sign in to comment.