Repository: googleapis/google-http-java-client
Pull request #1505 (2021-11-18 10:40:42)

feat: add AttemptCount to HttpResponseException
This adds number of requests attempted to httpRequestException. This makes sense to know how many attempts were made since HttpRequest supports automated retries. We want to use it to improve user experience of Auth library, that leverages HttpRequest retry functionality. 

For more details on the feature: go/auth-correct-retry


Patch:
diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
index 312702b9..78f15d86 100644
--- a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
+++ b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
@@ -1113,7 +1113,9 @@ public final class HttpRequest {
     // throw an exception if unsuccessful response
     if (throwExceptionOnExecuteError && !response.isSuccessStatusCode()) {
       try {
-        throw new HttpResponseException(response);
+        throw new HttpResponseException.Builder(response)
+            .setAttemptCount(numRetries - retriesRemaining)
+            .build();
       } finally {
         response.disconnect();
       }
diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpResponseException.java b/google-http-client/src/main/java/com/google/api/client/http/HttpResponseException.java
index a9d80a4f..63bf6fe4 100644
--- a/google-http-client/src/main/java/com/google/api/client/http/HttpResponseException.java
+++ b/google-http-client/src/main/java/com/google/api/client/http/HttpResponseException.java
@@ -42,6 +42,9 @@ public class HttpResponseException extends IOException {
   /** HTTP response content or {@code null} for none. */
   private final String content;
 
+  /** Number of attempts performed */
+  private final int attemptCount;
+
   /**
    * Constructor that constructs a detail message from the given HTTP response that includes the
    * status code, status message and HTTP response content.
@@ -73,6 +76,7 @@ public class HttpResponseException extends IOException {
     statusMessage = builder.statusMessage;
     headers = builder.headers;
     content = builder.content;
+    attemptCount = builder.attemptCount;
   }
 
   /**
@@ -121,6 +125,15 @@ public class HttpResponseException extends IOException {
     return content;
   }
 
+  /**
+   * Returns the attempt count
+   *
+   * @since 1.41
+   */
+  public final int getAttemptCount() {
+    return attemptCount;
+  }
+
   /**
    * Builder.
    *
@@ -145,6 +158,9 @@ public class HttpResponseException extends IOException {
     /** Detail message to use or {@code null} for none. */
     String message;
 
+    /** Number of attempts performed */
+    int attemptCount;
+
     /**
      * @param statusCode HTTP status code
      * @param statusMessage status message or {@code null}
@@ -260,6 +276,18 @@ public class HttpResponseException extends IOException {
       return this;
     }
 
+    /** Returns the request attempt count */
+    public final int getAttemptCount() {
+      return attemptCount;
+    }
+
+    /** Sets the attempt count for the related HTTP request execution. */
+    public Builder setAttemptCount(int attemptCount) {
+      Preconditions.checkArgument(attemptCount >= 0);
+      this.attemptCount = attemptCount;
+      return this;
+    }
+
     /** Returns a new instance of {@link HttpResponseException} based on this builder. */
     public HttpResponseException build() {
       return new HttpResponseException(this);


Test patch:
diff --git a/google-http-client/src/test/java/com/google/api/client/http/HttpResponseExceptionTest.java b/google-http-client/src/test/java/com/google/api/client/http/HttpResponseExceptionTest.java
index 9066e9d7..cbe6e6a0 100644
--- a/google-http-client/src/test/java/com/google/api/client/http/HttpResponseExceptionTest.java
+++ b/google-http-client/src/test/java/com/google/api/client/http/HttpResponseExceptionTest.java
@@ -23,6 +23,7 @@ import com.google.api.client.http.HttpResponseException.Builder;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpRequest;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.api.client.util.ExponentialBackOff;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -30,6 +31,7 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
 import junit.framework.TestCase;
+import org.junit.Assert;
 import org.junit.function.ThrowingRunnable;
 
 /**
@@ -208,6 +210,8 @@ public class HttpResponseExceptionTest extends TestCase {
                 + SIMPLE_GENERIC_URL
                 + LINE_SEPARATOR
                 + "Unable to find resource");
+    // no retries expected
+    assertEquals(1, responseException.getAttemptCount());
   }
 
   public void testInvalidCharset() throws Exception {
@@ -245,6 +249,50 @@ public class HttpResponseExceptionTest extends TestCase {
         .isEqualTo("404 Not Found\nGET " + SIMPLE_GENERIC_URL);
   }
 
+  public void testAttemptCountWithBackOff() throws Exception {
+    HttpTransport fakeTransport =
+        new MockHttpTransport() {
+          @Override
+          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+            return new MockLowLevelHttpRequest() {
+              @Override
+              public LowLevelHttpResponse execute() throws IOException {
+                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
+                result.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
+                result.setReasonPhrase("Error");
+                result.setContent("Unknown Error");
+                return result;
+              }
+            };
+          }
+        };
+    ExponentialBackOff backoff = new ExponentialBackOff.Builder().build();
+    final HttpRequest request =
+        fakeTransport.createRequestFactory().buildGetRequest(new GenericUrl("http://not/used"));
+    request.setUnsuccessfulResponseHandler(
+        new HttpBackOffUnsuccessfulResponseHandler(backoff)
+            .setBackOffRequired(
+                new HttpBackOffUnsuccessfulResponseHandler.BackOffRequired() {
+                  public boolean isRequired(HttpResponse response) {
+                    return true;
+                  }
+                }));
+    request.setNumberOfRetries(1);
+    HttpResponseException responseException =
+        assertThrows(
+            HttpResponseException.class,
+            new ThrowingRunnable() {
+              @Override
+              public void run() throws Throwable {
+                request.execute();
+              }
+            });
+
+    Assert.assertEquals(500, responseException.getStatusCode());
+    // original request and 1 retry - total 2
+    assertEquals(2, responseException.getAttemptCount());
+  }
+
   public void testUnsupportedCharset() throws Exception {
     HttpTransport transport =
         new MockHttpTransport() {