Repository: firebase/firebase-admin-java
Pull request #359 (2020-02-03 21:14:51)

Error handling revamp for FirebaseInstanceId API
This is not a OnePlatform API, so we extend the error handler directly from `AbstractHttpErrorHandler` class.


Patch:
diff --git a/pom.xml b/pom.xml
index aa8d3c8..e82439e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>6.12.1-SNAPSHOT</version>
+    <version>6.12.2-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 4c488cf..f7f6231 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -75,12 +75,7 @@ public class FirebaseAuth {
     this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
     this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
     this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
-    this.userManager = threadSafeMemoize(new Supplier<FirebaseUserManager>() {
-      @Override
-      public FirebaseUserManager get() {
-        return new FirebaseUserManager(firebaseApp);
-      }
-    });
+    this.userManager = threadSafeMemoize(builder.userManager);
     this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
   }
 
@@ -1109,10 +1104,10 @@ public class FirebaseAuth {
   }
 
   private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
-    checkNotNull(supplier);
     return Suppliers.memoize(new Supplier<T>() {
       @Override
       public T get() {
+        checkNotNull(supplier);
         synchronized (lock) {
           checkNotDestroyed();
           return supplier.get();
@@ -1155,6 +1150,12 @@ public class FirebaseAuth {
             return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
           }
         })
+        .setUserManager(new Supplier<FirebaseUserManager>() {
+          @Override
+          public FirebaseUserManager get() {
+            return new FirebaseUserManager(app);
+          }
+        })
         .build();
   }
 
@@ -1168,6 +1169,7 @@ public class FirebaseAuth {
     private Supplier<FirebaseTokenFactory> tokenFactory;
     private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
     private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
+    private Supplier<FirebaseUserManager> userManager;
 
     private Builder() { }
 
@@ -1191,6 +1193,11 @@ public class FirebaseAuth {
       return this;
     }
 
+    Builder setUserManager(Supplier<FirebaseUserManager> userManager) {
+      this.userManager = userManager;
+      return this;
+    }
+
     FirebaseAuth build() {
       return new FirebaseAuth(this);
     }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 8298499..03c2813 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -26,7 +26,6 @@ import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponse;
 import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.HttpTransport;
 import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
@@ -45,7 +44,7 @@ import com.google.firebase.auth.internal.GetAccountInfoResponse;
 
 import com.google.firebase.auth.internal.HttpErrorResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
-import com.google.firebase.internal.FirebaseRequestInitializer;
+import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 import com.google.firebase.internal.SdkUtils;
@@ -111,6 +110,10 @@ class FirebaseUserManager {
    * @param app A non-null {@link FirebaseApp}.
    */
   FirebaseUserManager(@NonNull FirebaseApp app) {
+    this(app, null);
+  }
+
+  FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) {
     checkNotNull(app, "FirebaseApp must not be null");
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkArgument(!Strings.isNullOrEmpty(projectId),
@@ -119,8 +122,12 @@ class FirebaseUserManager {
             + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
     this.baseUrl = String.format(ID_TOOLKIT_URL, projectId);
     this.jsonFactory = app.getOptions().getJsonFactory();
-    HttpTransport transport = app.getOptions().getHttpTransport();
-    this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app));
+
+    if (requestFactory == null) {
+      requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
+    }
+
+    this.requestFactory = requestFactory;
   }
 
   @VisibleForTesting
diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
index 7ac5277..63c46f2 100644
--- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
+++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
@@ -17,29 +17,27 @@
 package com.google.firebase.iid;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.api.client.http.GenericUrl;
-import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
-import com.google.api.client.http.HttpResponse;
-import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.HttpTransport;
-import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.JsonObjectParser;
 import com.google.api.core.ApiFuture;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.io.ByteStreams;
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseException;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.IncomingHttpResponse;
+import com.google.firebase.database.annotations.Nullable;
+import com.google.firebase.internal.AbstractHttpErrorHandler;
+import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.CallableOperation;
-import com.google.firebase.internal.FirebaseRequestInitializer;
+import com.google.firebase.internal.ErrorHandlingHttpClient;
 import com.google.firebase.internal.FirebaseService;
+import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.NonNull;
 
-import java.io.IOException;
 import java.util.Map;
 
 /**
@@ -64,22 +62,32 @@ public class FirebaseInstanceId {
       .build();
 
   private final FirebaseApp app;
-  private final HttpRequestFactory requestFactory;
-  private final JsonFactory jsonFactory;
   private final String projectId;
+  private final ErrorHandlingHttpClient<FirebaseInstanceIdException> httpClient;
 
   private HttpResponseInterceptor interceptor;
 
   private FirebaseInstanceId(FirebaseApp app) {
-    HttpTransport httpTransport = app.getOptions().getHttpTransport();
-    this.app = app;
-    this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app));
-    this.jsonFactory = app.getOptions().getJsonFactory();
-    this.projectId = ImplFirebaseTrampolines.getProjectId(app);
+    this(app, null);
+  }
+
+  @VisibleForTesting
+  FirebaseInstanceId(FirebaseApp app, @Nullable HttpRequestFactory requestFactory) {
+    this.app = checkNotNull(app, "app must not be null");
+    String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkArgument(!Strings.isNullOrEmpty(projectId),
         "Project ID is required to access instance ID service. Use a service account credential or "
             + "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
             + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
+    this.projectId = projectId;
+    if (requestFactory == null) {
+      requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
+    }
+
+    this.httpClient = new ErrorHandlingHttpClient<>(
+        requestFactory,
+        app.getOptions().getJsonFactory(),
+        new InstanceIdErrorHandler());
   }
 
   /**
@@ -146,42 +154,46 @@ public class FirebaseInstanceId {
       protected Void execute() throws FirebaseInstanceIdException {
         String url = String.format(
             "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId);
-        HttpResponse response = null;
-        try {
-          HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url));
-          request.setParser(new JsonObjectParser(jsonFactory));
-          request.setResponseInterceptor(interceptor);
-          response = request.execute();
-          ByteStreams.exhaust(response.getContent());
-        } catch (Exception e) {
-          handleError(instanceId, e);
-        } finally {
-          disconnectQuietly(response);
-        }
+        HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url)
+            .setResponseInterceptor(interceptor);
+        httpClient.send(request);
         return null;
       }
     };
   }
 
-  private static void disconnectQuietly(HttpResponse response) {
-    if (response != null) {
-      try {
-        response.disconnect();
-      } catch (IOException ignored) {
-        // ignored
+  private static class InstanceIdErrorHandler
+      extends AbstractHttpErrorHandler<FirebaseInstanceIdException> {
+
+    @Override
+    protected FirebaseInstanceIdException createException(FirebaseException base) {
+      String message = base.getMessage();
+      String customMessage = getCustomMessage(base);
+      if (!Strings.isNullOrEmpty(customMessage)) {
+        message = customMessage;
       }
+
+      return new FirebaseInstanceIdException(base, message);
     }
-  }
 
-  private void handleError(String instanceId, Exception e) throws FirebaseInstanceIdException {
-    String msg = "Error while invoking instance ID service.";
-    if (e instanceof HttpResponseException) {
-      int statusCode = ((HttpResponseException) e).getStatusCode();
-      if (ERROR_CODES.containsKey(statusCode)) {
-        msg = String.format("Instance ID \"%s\": %s", instanceId, ERROR_CODES.get(statusCode));
+    private String getCustomMessage(FirebaseException base) {
+      IncomingHttpResponse response = base.getHttpResponse();
+      if (response != null) {
+        String instanceId = extractInstanceId(response);
+        String description = ERROR_CODES.get(response.getStatusCode());
+        if (description != null) {
+          return String.format("Instance ID \"%s\": %s", instanceId, description);
+        }
       }
+
+      return null;
+    }
+
+    private String extractInstanceId(IncomingHttpResponse response) {
+      String url = response.getRequest().getUrl();
+      int index = url.lastIndexOf('/');
+      return url.substring(index + 1);
     }
-    throw new FirebaseInstanceIdException(msg, e);
   }
 
   private static final String SERVICE_ID = FirebaseInstanceId.class.getName();
diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java
index dfe0087..30553c1 100644
--- a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java
+++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java
@@ -21,9 +21,9 @@ import com.google.firebase.FirebaseException;
 /**
  * Represents an exception encountered while interacting with the Firebase instance ID service.
  */
-public class FirebaseInstanceIdException extends FirebaseException {
+public final class FirebaseInstanceIdException extends FirebaseException {
 
-  FirebaseInstanceIdException(String detailMessage, Throwable cause) {
-    super(detailMessage, cause);
+  FirebaseInstanceIdException(FirebaseException base, String message) {
+    super(base.getErrorCodeNew(), message, base.getCause(), base.getHttpResponse());
   }
 }
diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
index d19bab5..8a29a8c 100644
--- a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
+++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
@@ -63,6 +63,10 @@ public final class HttpRequestInfo {
     return new HttpRequestInfo(HttpMethods.GET, url, null);
   }
 
+  public static HttpRequestInfo buildDeleteRequest(String url) {
+    return new HttpRequestInfo(HttpMethods.DELETE, url, null);
+  }
+
   public static HttpRequestInfo buildPostRequest(String url, HttpContent content) {
     return new HttpRequestInfo(HttpMethods.POST, url, content);
   }


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index cf3b683..1bc0517 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -429,11 +429,11 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForIdTokenVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
-        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
         .setIdTokenVerifier(tokenVerifierSupplier)
-        .setCookieVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
+        .setUserManager(Suppliers.ofInstance(userManager))
         .build();
   }
 
@@ -444,11 +444,11 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForSessionCookieVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
-        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
-        .setIdTokenVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
         .setCookieVerifier(tokenVerifierSupplier)
+        .setUserManager(Suppliers.ofInstance(userManager))
         .build();
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 97ff744..de0b7fa 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -33,6 +33,7 @@ import com.google.api.client.json.JsonFactory;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -550,14 +551,7 @@ public class FirebaseUserManagerTest {
         .build();
 
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-        .setLowLevelHttpResponse(response)
-        .build();
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setProjectId("test-project-id")
-        .setHttpTransport(transport)
-        .build());
+    FirebaseAuth auth = getRetryDisabledAuth(response);
 
     // Test for common HTTP error codes
     for (int code : ImmutableList.of(302, 400, 401, 404, 500)) {
@@ -566,7 +560,7 @@ public class FirebaseUserManagerTest {
         response.setContent("{}");
         response.setStatusCode(code);
         try {
-          operation.call(FirebaseAuth.getInstance());
+          operation.call(auth);
           fail("No error thrown for HTTP error: " + code);
         } catch (ExecutionException e) {
           assertTrue(e.getCause() instanceof FirebaseAuthException);
@@ -584,7 +578,7 @@ public class FirebaseUserManagerTest {
       response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}");
       response.setStatusCode(500);
       try {
-        operation.call(FirebaseAuth.getInstance());
+        operation.call(auth);
         fail("No error thrown for HTTP error");
       }  catch (ExecutionException e) {
         assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException);
@@ -615,16 +609,9 @@ public class FirebaseUserManagerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
     response.setContent("{\"not\" json}");
     response.setStatusCode(500);
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-        .setLowLevelHttpResponse(response)
-        .build();
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setProjectId("test-project-id")
-        .setHttpTransport(transport)
-        .build());
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     try {
-      FirebaseAuth.getInstance().getUserAsync("testuser").get();
+      auth.getUserAsync("testuser").get();
       fail("No error thrown for JSON error");
     }  catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
@@ -1173,15 +1160,10 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testHttpErrorWithCode() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}")
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     FirebaseUserManager userManager = auth.getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
@@ -1194,15 +1176,10 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUnexpectedHttpError() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("{}")
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     FirebaseUserManager userManager = auth.getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
@@ -1231,6 +1208,26 @@ public class FirebaseUserManagerTest {
     return interceptor;
   }
 
+  private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) {
+    final MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(credentials)
+        .setProjectId("test-project-id")
+        .setHttpTransport(transport)
+        .build());
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setUserManager(new Supplier<FirebaseUserManager>() {
+          @Override
+          public FirebaseUserManager get() {
+            return new FirebaseUserManager(app, transport.createRequestFactory());
+          }
+        })
+        .build();
+  }
+
   private static void checkUserRecord(UserRecord userRecord) {
     assertEquals("testuser", userRecord.getUid());
     assertEquals("testuser@example.com", userRecord.getEmail());
diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
index 366bb7e..1038ea4 100644
--- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
+++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
@@ -23,18 +23,25 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.HttpTransport;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
+import com.google.firebase.IncomingHttpResponse;
+import com.google.firebase.OutgoingHttpRequest;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.MockGoogleCredentials;
 import com.google.firebase.testing.GenericFunction;
 import com.google.firebase.testing.TestResponseInterceptor;
+import com.google.firebase.testing.TestUtils;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -43,6 +50,25 @@ import org.junit.Test;
 
 public class FirebaseInstanceIdTest {
 
+  private static final Map<Integer, String> ERROR_MESSAGES = ImmutableMap.of(
+      404, "Instance ID \"test-iid\": Failed to find the instance ID.",
+      409, "Instance ID \"test-iid\": Already deleted.",
+      429, "Instance ID \"test-iid\": Request throttled out by the backend server.",
+      500, "Instance ID \"test-iid\": Internal server error.",
+      501, "Unexpected HTTP response with status: 501\ntest error"
+  );
+
+  private static final Map<Integer, ErrorCode> ERROR_CODES = ImmutableMap.of(
+      404, ErrorCode.NOT_FOUND,
+      409, ErrorCode.CONFLICT,
+      429, ErrorCode.RESOURCE_EXHAUSTED,
+      500, ErrorCode.INTERNAL,
+      501, ErrorCode.UNKNOWN
+  );
+
+  private static final String TEST_URL =
+      "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid";
+
   @After
   public void tearDown() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
@@ -123,7 +149,6 @@ public class FirebaseInstanceIdTest {
         }
     );
 
-    String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid";
     for (GenericFunction<Void> fn : functions) {
       TestResponseInterceptor interceptor = new TestResponseInterceptor();
       instanceId.setInterceptor(interceptor);
@@ -131,54 +156,113 @@ public class FirebaseInstanceIdTest {
 
       assertNotNull(interceptor.getResponse());
       HttpRequest request = interceptor.getResponse().getRequest();
-      assertEquals("DELETE", request.getRequestMethod());
-      assertEquals(url, request.getUrl().toString());
+      assertEquals(HttpMethods.DELETE, request.getRequestMethod());
+      assertEquals(TEST_URL, request.getUrl().toString());
       assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
     }
   }
 
   @Test
   public void testDeleteInstanceIdError() throws Exception {
-    Map<Integer, String> errors = ImmutableMap.of(
-        404, "Instance ID \"test-iid\": Failed to find the instance ID.",
-        429, "Instance ID \"test-iid\": Request throttled out by the backend server.",
-        500, "Instance ID \"test-iid\": Internal server error.",
-        501, "Error while invoking instance ID service."
-    );
+    final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project")
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
 
-    String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid";
-    for (Map.Entry<Integer, String> entry : errors.entrySet()) {
-      MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
-          .setStatusCode(entry.getKey())
-          .setContent("test error");
-      MockHttpTransport transport = new MockHttpTransport.Builder()
-          .setLowLevelHttpResponse(response)
-          .build();
-      FirebaseOptions options = new FirebaseOptions.Builder()
-          .setCredentials(new MockGoogleCredentials("test-token"))
-          .setProjectId("test-project")
-          .setHttpTransport(transport)
-          .build();
-      final FirebaseApp app = FirebaseApp.initializeApp(options);
-
-      FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance();
-      TestResponseInterceptor interceptor = new TestResponseInterceptor();
-      instanceId.setInterceptor(interceptor);
-      try {
-        instanceId.deleteInstanceIdAsync("test-iid").get();
-        fail("No error thrown for HTTP error");
-      } catch (ExecutionException e) {
-        assertTrue(e.getCause() instanceof FirebaseInstanceIdException);
-        assertEquals(entry.getValue(), e.getCause().getMessage());
-        assertTrue(e.getCause().getCause() instanceof HttpResponseException);
-      }
+    // Disable retries by passing a regular HttpRequestFactory.
+    FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory());
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    instanceId.setInterceptor(interceptor);
 
-      assertNotNull(interceptor.getResponse());
-      HttpRequest request = interceptor.getResponse().getRequest();
-      assertEquals("DELETE", request.getRequestMethod());
-      assertEquals(url, request.getUrl().toString());
-      assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
+    try {
+      for (int statusCode : ERROR_CODES.keySet()) {
+        response.setStatusCode(statusCode).setContent("test error");
+
+        try {
+          instanceId.deleteInstanceIdAsync("test-iid").get();
+          fail("No error thrown for HTTP error");
+        } catch (ExecutionException e) {
+          assertTrue(e.getCause() instanceof FirebaseInstanceIdException);
+          checkFirebaseInstanceIdException((FirebaseInstanceIdException) e.getCause(), statusCode);
+        }
+
+        assertNotNull(interceptor.getResponse());
+        HttpRequest request = interceptor.getResponse().getRequest();
+        assertEquals(HttpMethods.DELETE, request.getRequestMethod());
+        assertEquals(TEST_URL, request.getUrl().toString());
+      }
+    } finally {
       app.delete();
     }
   }
+
+  @Test
+  public void testDeleteInstanceIdTransportError() throws Exception {
+    HttpTransport transport = TestUtils.createFaultyHttpTransport();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project")
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    // Disable retries by passing a regular HttpRequestFactory.
+    FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory());
+
+    try {
+      instanceId.deleteInstanceIdAsync("test-iid").get();
+      fail("No error thrown for HTTP error");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseInstanceIdException);
+      FirebaseInstanceIdException error = (FirebaseInstanceIdException) e.getCause();
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error",
+          error.getMessage());
+      assertTrue(error.getCause() instanceof IOException);
+      assertNull(error.getHttpResponse());
+    }
+  }
+
+  @Test
+  public void testDeleteInstanceIdInvalidJsonIgnored() throws Exception {
+    final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project")
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+
+    // Disable retries by passing a regular HttpRequestFactory.
+    FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory());
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    instanceId.setInterceptor(interceptor);
+    response.setContent("not json");
+
+    instanceId.deleteInstanceIdAsync("test-iid").get();
+
+    assertNotNull(interceptor.getResponse());
+  }
+
+  private void checkFirebaseInstanceIdException(FirebaseInstanceIdException error, int statusCode) {
+    assertEquals(ERROR_CODES.get(statusCode), error.getErrorCodeNew());
+    assertEquals(ERROR_MESSAGES.get(statusCode), error.getMessage());
+    assertTrue(error.getCause() instanceof HttpResponseException);
+
+    IncomingHttpResponse httpResponse = error.getHttpResponse();
+    assertNotNull(httpResponse);
+    assertEquals(statusCode, httpResponse.getStatusCode());
+    OutgoingHttpRequest request = httpResponse.getRequest();
+    assertEquals(HttpMethods.DELETE, request.getMethod());
+    assertEquals(TEST_URL, request.getUrl());
+  }
 }