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>
diff --git a/acme/http.go b/acme/http.go
index 56ba53a..a43ce6a 100644
--- a/acme/http.go
+++ b/acme/http.go
@@ -140,10 +140,13 @@
 		case ok(res):
 			return res, nil
 		case isRetriable(res.StatusCode):
-			res.Body.Close()
 			retry.inc()
-			if err := retry.backoff(ctx, req, res); err != nil {
-				return nil, err
+			resErr := responseError(res)
+			res.Body.Close()
+			// Ignore the error value from retry.backoff
+			// and return the one from last retry, as received from the CA.
+			if retry.backoff(ctx, req, res) != nil {
+				return nil, resErr
 			}
 		default:
 			defer res.Body.Close()
@@ -169,20 +172,22 @@
 		if ok(res) {
 			return res, nil
 		}
-		err = responseError(res)
+		resErr := responseError(res)
 		res.Body.Close()
 		switch {
 		// Check for bad nonce before isRetriable because it may have been returned
 		// with an unretriable response code such as 400 Bad Request.
-		case isBadNonce(err):
+		case isBadNonce(resErr):
 			// Consider any previously stored nonce values to be invalid.
 			c.clearNonces()
 		case !isRetriable(res.StatusCode):
-			return nil, err
+			return nil, resErr
 		}
 		retry.inc()
+		// Ignore the error value from retry.backoff
+		// and return the one from last retry, as received from the CA.
 		if err := retry.backoff(ctx, req, res); err != nil {
-			return nil, err
+			return nil, resErr
 		}
 	}
 }
diff --git a/acme/http_test.go b/acme/http_test.go
index c9ce1d6..15e401b 100644
--- a/acme/http_test.go
+++ b/acme/http_test.go
@@ -88,12 +88,14 @@
 		}
 
 		head, err := decodeJWSHead(r)
-		if err != nil {
+		switch {
+		case err != nil:
 			t.Errorf("decodeJWSHead: %v", err)
-		} else if head.Nonce == "" {
+		case head.Nonce == "":
 			t.Error("head.Nonce is empty")
-		} else if head.Nonce == "nonce1" {
-			// return a badNonce error to force the call to retry
+		case head.Nonce == "nonce1":
+			// Return a badNonce error to force the call to retry.
+			w.Header().Set("Retry-After", "0")
 			w.WriteHeader(http.StatusBadRequest)
 			w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
 			return
@@ -114,6 +116,55 @@
 	}
 }
 
+func TestRetryErrorType(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Replay-Nonce", "nonce")
+		w.WriteHeader(http.StatusTooManyRequests)
+		w.Write([]byte(`{"type":"rateLimited"}`))
+	}))
+	defer ts.Close()
+
+	client := &Client{
+		Key: testKey,
+		RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
+			// Do no retries.
+			return 0
+		},
+		dir: &Directory{AuthzURL: ts.URL},
+	}
+
+	t.Run("post", func(t *testing.T) {
+		testRetryErrorType(t, func() error {
+			_, err := client.Authorize(context.Background(), "example.com")
+			return err
+		})
+	})
+	t.Run("get", func(t *testing.T) {
+		testRetryErrorType(t, func() error {
+			_, err := client.GetAuthorization(context.Background(), ts.URL)
+			return err
+		})
+	})
+}
+
+func testRetryErrorType(t *testing.T, callClient func() error) {
+	t.Helper()
+	err := callClient()
+	if err == nil {
+		t.Fatal("client.Authorize returned nil error")
+	}
+	acmeErr, ok := err.(*Error)
+	if !ok {
+		t.Fatalf("err is %v (%T); want *Error", err, err)
+	}
+	if acmeErr.StatusCode != http.StatusTooManyRequests {
+		t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
+	}
+	if acmeErr.ProblemType != "rateLimited" {
+		t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
+	}
+}
+
 func TestRetryBackoffArgs(t *testing.T) {
 	const resCode = http.StatusInternalServerError
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {