Repository: firebase/firebase-admin-java
Pull request #458 (2020-07-24 22:36:38)

feat: Added new error codes for IdP management and multitenancy
* Added `CONFIGURATION_NOT_FOUND` error code. Also note that `TENANT_NOT_FOUND` error code was added while updating the v7 branch in #456.
* Made it possible to test `TenantManager` and `FirebaseTenantClient` with better mock responses.
* Updated error handling test cases to check for new error codes. I also updated some of the existing error handling tests to mock with more realistic errors (e.g. create operations do not usually fail with a NOT_FOUND error). 


Patch:
diff --git a/pom.xml b/pom.xml
index 33e235d..e61a57c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,7 +198,7 @@
                     <plugin>
                         <groupId>org.jacoco</groupId>
                         <artifactId>jacoco-maven-plugin</artifactId>
-                        <version>0.7.9</version>
+                        <version>0.8.5</version>
                         <executions>
                             <execution>
                                 <id>pre-unit-test</id>
@@ -289,7 +289,7 @@
             <!-- Test Phase -->
             <plugin>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.19.1</version>
+                <version>2.22.0</version>
                 <configuration>
                     <skipTests>${skipUTs}</skipTests>
                 </configuration>
diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
index c162518..bea067e 100644
--- a/src/main/java/com/google/firebase/auth/AuthErrorCode.java
+++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
@@ -26,6 +26,11 @@ public enum AuthErrorCode {
    */
   CERTIFICATE_FETCH_FAILED,
 
+  /**
+   * No IdP configuration found for the given identifier.
+   */
+  CONFIGURATION_NOT_FOUND,
+
   /**
    * A user already exists with the provided email.
    */
@@ -71,8 +76,16 @@ public enum AuthErrorCode {
    */
   REVOKED_SESSION_COOKIE,
 
+  /**
+   * Tenant ID in the JWT does not match.
+   */
   TENANT_ID_MISMATCH,
 
+  /**
+   * No tenant found for the given identifier.
+   */
+  TENANT_NOT_FOUND,
+
   /**
    * A user already exists with the provided UID.
    */
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 55b854e..d00cd24 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -80,7 +80,7 @@ class FirebaseUserManager {
   private final JsonFactory jsonFactory;
   private final AuthHttpClient httpClient;
 
-  FirebaseUserManager(Builder builder) {
+  private FirebaseUserManager(Builder builder) {
     FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null");
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkArgument(!Strings.isNullOrEmpty(projectId),
diff --git a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java
index 0fc5ef3..e989114 100644
--- a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java
+++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java
@@ -36,6 +36,12 @@ final class AuthErrorHandler extends AbstractHttpErrorHandler<FirebaseAuthExcept
 
   private static final Map<String, AuthError> ERROR_CODES =
       ImmutableMap.<String, AuthError>builder()
+          .put(
+              "CONFIGURATION_NOT_FOUND",
+              new AuthError(
+                  ErrorCode.NOT_FOUND,
+                  "No IdP configuration found corresponding to the provided identifier",
+                  AuthErrorCode.CONFIGURATION_NOT_FOUND))
           .put(
               "DUPLICATE_EMAIL",
               new AuthError(
@@ -67,6 +73,12 @@ final class AuthErrorHandler extends AbstractHttpErrorHandler<FirebaseAuthExcept
                   ErrorCode.ALREADY_EXISTS,
                   "The user with the provided phone number already exists",
                   AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS))
+          .put(
+              "TENANT_NOT_FOUND",
+              new AuthError(
+                  ErrorCode.NOT_FOUND,
+                  "No tenant found for the given identifier",
+                  AuthErrorCode.TENANT_NOT_FOUND))
           .put(
               "UNAUTHORIZED_DOMAIN",
               new AuthError(
diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java
index 70f9495..76e118b 100644
--- a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java
+++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java
@@ -16,16 +16,10 @@
 
 package com.google.firebase.auth.internal;
 
-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.HttpContent;
 import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.JsonFactory;
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.firebase.IncomingHttpResponse;
 import com.google.firebase.auth.FirebaseAuthException;
@@ -45,15 +39,13 @@ public final class AuthHttpClient {
 
   private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion();
 
-  private final JsonFactory jsonFactory;
   private final ErrorHandlingHttpClient<FirebaseAuthException> httpClient;
-
-  private HttpResponseInterceptor interceptor;
+  private final JsonFactory jsonFactory;
 
   public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) {
-    this.jsonFactory = jsonFactory;
     AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory);
     this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler);
+    this.jsonFactory = jsonFactory;
   }
 
   public static Set<String> generateMask(Map<String, Object> properties) {
@@ -72,28 +64,21 @@ public final class AuthHttpClient {
   }
 
   public void setInterceptor(HttpResponseInterceptor interceptor) {
-    this.interceptor = interceptor;
+    this.httpClient.setInterceptor(interceptor);
   }
 
   public IncomingHttpResponse sendRequest(
       String method, GenericUrl url, @Nullable Object content) throws FirebaseAuthException {
-    checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty");
-    checkNotNull(url, "url must not be null");
-    String httpMethod = method.equals("PATCH") ? "POST" : method;
-    HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
-    HttpRequestInfo request = HttpRequestInfo.buildRequest(httpMethod, url, httpContent)
-        .addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION)
-        .setResponseInterceptor(interceptor);
-    if (method.equals("PATCH")) {
-      request.addHeader("X-HTTP-Method-Override", "PATCH");
-    }
-
+    HttpRequestInfo request = HttpRequestInfo.buildJsonRequest(method, url, content)
+        .addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION);
     return httpClient.send(request);
   }
 
   public <T> T sendRequest(
-      String method, GenericUrl url,
-      @Nullable Object content, Class<T> clazz) throws FirebaseAuthException {
+      String method,
+      GenericUrl url,
+      @Nullable Object content,
+      Class<T> clazz) throws FirebaseAuthException {
 
     IncomingHttpResponse response = this.sendRequest(method, url, content);
     return this.parse(response, clazz);
diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
index c20c82a..7bc54af 100644
--- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
+++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
@@ -8,7 +8,6 @@ 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.HttpResponseInterceptor;
-import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.util.StringUtils;
@@ -75,9 +74,7 @@ public class CryptoSigners {
         "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
 
     private final String serviceAccount;
-    private final JsonFactory jsonFactory;
     private final ErrorHandlingHttpClient<FirebaseAuthException> httpClient;
-    private HttpResponseInterceptor interceptor;
 
     IAMCryptoSigner(
         @NonNull HttpRequestFactory requestFactory,
@@ -85,7 +82,6 @@ public class CryptoSigners {
         @NonNull String serviceAccount) {
       checkArgument(!Strings.isNullOrEmpty(serviceAccount));
       this.serviceAccount = serviceAccount;
-      this.jsonFactory = checkNotNull(jsonFactory);
       this.httpClient = new ErrorHandlingHttpClient<>(
           requestFactory,
           jsonFactory,
@@ -93,7 +89,7 @@ public class CryptoSigners {
     }
 
     void setInterceptor(HttpResponseInterceptor interceptor) {
-      this.interceptor = interceptor;
+      httpClient.setInterceptor(interceptor);
     }
 
     @Override
@@ -101,9 +97,7 @@ public class CryptoSigners {
       String encodedPayload = BaseEncoding.base64().encode(payload);
       Map<String, String> content = ImmutableMap.of("bytesToSign", encodedPayload);
       String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount);
-      HttpRequestInfo requestInfo = HttpRequestInfo
-          .buildPostRequest(encodedUrl, new JsonHttpContent(jsonFactory, content))
-          .setResponseInterceptor(interceptor);
+      HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(encodedUrl, content);
       GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class);
       return BaseEncoding.base64().decode((String) parsed.get("signature"));
     }
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
index 1f6256c..640cc2a 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
@@ -46,15 +46,19 @@ final class FirebaseTenantClient {
   private final AuthHttpClient httpClient;
 
   FirebaseTenantClient(FirebaseApp app) {
-    checkNotNull(app, "FirebaseApp must not be null");
-    String projectId = ImplFirebaseTrampolines.getProjectId(app);
+    this(
+        ImplFirebaseTrampolines.getProjectId(checkNotNull(app)),
+        app.getOptions().getJsonFactory(),
+        ApiClientUtils.newAuthorizedRequestFactory(app));
+  }
+
+  FirebaseTenantClient(
+      String projectId, JsonFactory jsonFactory, HttpRequestFactory requestFactory) {
     checkArgument(!Strings.isNullOrEmpty(projectId),
         "Project ID is required to access the auth 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.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
-    JsonFactory jsonFactory = app.getOptions().getJsonFactory();
-    HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
     this.httpClient = new AuthHttpClient(jsonFactory, requestFactory);
   }
 
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
index 11f26b0..febe666 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
@@ -54,8 +54,13 @@ public final class TenantManager {
    * @hide
    */
   public TenantManager(FirebaseApp firebaseApp) {
-    this.firebaseApp = firebaseApp;
-    this.tenantClient = new FirebaseTenantClient(firebaseApp);
+    this(firebaseApp, new FirebaseTenantClient(firebaseApp));
+  }
+
+  @VisibleForTesting
+  TenantManager(FirebaseApp firebaseApp, FirebaseTenantClient tenantClient) {
+    this.firebaseApp = checkNotNull(firebaseApp);
+    this.tenantClient = checkNotNull(tenantClient);
     this.tenantAwareAuths = new HashMap<>();
   }
 
diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
index 63c46f2..8226544 100644
--- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
+++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
@@ -65,8 +65,6 @@ public class FirebaseInstanceId {
   private final String projectId;
   private final ErrorHandlingHttpClient<FirebaseInstanceIdException> httpClient;
 
-  private HttpResponseInterceptor interceptor;
-
   private FirebaseInstanceId(FirebaseApp app) {
     this(app, null);
   }
@@ -115,7 +113,7 @@ public class FirebaseInstanceId {
 
   @VisibleForTesting
   void setInterceptor(HttpResponseInterceptor interceptor) {
-    this.interceptor = interceptor;
+    httpClient.setInterceptor(interceptor);
   }
 
   /**
@@ -154,8 +152,7 @@ public class FirebaseInstanceId {
       protected Void execute() throws FirebaseInstanceIdException {
         String url = String.format(
             "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId);
-        HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url)
-            .setResponseInterceptor(interceptor);
+        HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url);
         httpClient.send(request);
         return null;
       }
diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
index f533f13..e449568 100644
--- a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
+++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
@@ -22,6 +22,7 @@ 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.json.JsonFactory;
 import com.google.api.client.json.JsonObjectParser;
 import com.google.api.client.json.JsonParser;
@@ -41,6 +42,9 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
   private final HttpRequestFactory requestFactory;
   private final JsonFactory jsonFactory;
   private final HttpErrorHandler<T> errorHandler;
+  private final JsonObjectParser jsonParser;
+
+  private HttpResponseInterceptor interceptor;
 
   public ErrorHandlingHttpClient(
       HttpRequestFactory requestFactory,
@@ -49,6 +53,12 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
     this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null");
     this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
     this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null");
+    this.jsonParser = new JsonObjectParser(jsonFactory);
+  }
+
+  public ErrorHandlingHttpClient<T> setInterceptor(HttpResponseInterceptor interceptor) {
+    this.interceptor = interceptor;
+    return this;
   }
 
   /**
@@ -127,8 +137,9 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
 
   private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T {
     try {
-      return requestInfo.newHttpRequest(requestFactory)
-          .setParser(new JsonObjectParser(jsonFactory));
+      return requestInfo.newHttpRequest(requestFactory, jsonFactory)
+          .setParser(jsonParser)
+          .setResponseInterceptor(interceptor);
     } catch (IOException e) {
       // Handle request initialization errors (credential loading and other config errors)
       throw errorHandler.handleIOException(e);
diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
index e92a76b..cb687b7 100644
--- a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
+++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
@@ -24,7 +24,8 @@ import com.google.api.client.http.HttpContent;
 import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
-import com.google.api.client.http.HttpResponseInterceptor;
+import com.google.api.client.http.json.JsonHttpContent;
+import com.google.api.client.json.JsonFactory;
 import com.google.common.base.Strings;
 import java.io.IOException;
 import java.util.HashMap;
@@ -39,15 +40,15 @@ public final class HttpRequestInfo {
   private final String method;
   private final GenericUrl url;
   private final HttpContent content;
+  private final Object jsonContent;
   private final Map<String, String> headers = new HashMap<>();
-  private HttpResponseInterceptor interceptor;
 
-  private HttpRequestInfo(String method, GenericUrl url, HttpContent content) {
+  private HttpRequestInfo(String method, GenericUrl url, HttpContent content, Object jsonContent) {
     checkArgument(!Strings.isNullOrEmpty(method), "method must not be null");
-    checkNotNull(url, "url must not be null");
     this.method = method;
-    this.url = url;
+    this.url = checkNotNull(url, "url must not be null");
     this.content = content;
+    this.jsonContent = jsonContent;
   }
 
   public HttpRequestInfo addHeader(String name, String value) {
@@ -60,11 +61,6 @@ public final class HttpRequestInfo {
     return this;
   }
 
-  public HttpRequestInfo setResponseInterceptor(HttpResponseInterceptor interceptor) {
-    this.interceptor = interceptor;
-    return this;
-  }
-
   public static HttpRequestInfo buildGetRequest(String url) {
     return buildRequest(HttpMethods.GET, url, null);
   }
@@ -73,25 +69,60 @@ public final class HttpRequestInfo {
     return buildRequest(HttpMethods.DELETE, url, null);
   }
 
-  public static HttpRequestInfo buildPostRequest(String url, HttpContent content) {
-    return buildRequest(HttpMethods.POST, url, content);
+  public static HttpRequestInfo buildRequest(
+      String method, String url, @Nullable HttpContent content) {
+    return buildRequest(method, new GenericUrl(url), content);
   }
 
-  public static HttpRequestInfo buildRequest(String method, String url, HttpContent content) {
-    return buildRequest(method, new GenericUrl(url), content);
+  public static HttpRequestInfo buildRequest(
+      String method, GenericUrl url, @Nullable HttpContent content) {
+    return new HttpRequestInfo(method, url, content, null);
+  }
+
+  public static HttpRequestInfo buildJsonPostRequest(String url, @Nullable Object content) {
+    return buildJsonRequest(HttpMethods.POST, url, content);
+  }
+
+  public static HttpRequestInfo buildJsonRequest(
+      String method, String url, @Nullable Object content) {
+    return buildJsonRequest(method, new GenericUrl(url), content);
   }
 
-  public static HttpRequestInfo buildRequest(String method, GenericUrl url, HttpContent content) {
-    return new HttpRequestInfo(method, url, content);
+  public static HttpRequestInfo buildJsonRequest(
+      String method, GenericUrl url, @Nullable Object content) {
+    return new HttpRequestInfo(method, url, null, content);
   }
 
-  HttpRequest newHttpRequest(HttpRequestFactory factory) throws IOException {
-    HttpRequest request = factory.buildRequest(method, url, content);
+  HttpRequest newHttpRequest(
+      HttpRequestFactory factory, JsonFactory jsonFactory) throws IOException {
+    HttpRequest request;
+    HttpContent httpContent = getContent(jsonFactory);
+    if (factory.getTransport().supportsMethod(method)) {
+      request = factory.buildRequest(method, url, httpContent);
+    } else {
+      // Some HttpTransport implementations (notably NetHttpTransport) don't support new methods
+      // like PATCH. We try to emulate such requests over POST by setting the method override
+      // header, which is recognized by most Google backend APIs.
+      request = factory.buildPostRequest(url, httpContent);
+      request.getHeaders().set("X-HTTP-Method-Override", method);
+    }
+
     for (Map.Entry<String, String> entry : headers.entrySet()) {
       request.getHeaders().set(entry.getKey(), entry.getValue());
     }
-    request.setResponseInterceptor(interceptor);
 
     return request;
   }
+
+  private HttpContent getContent(JsonFactory jsonFactory) {
+    if (content != null) {
+      return content;
+    }
+
+    if (jsonContent != null) {
+      return new JsonHttpContent(jsonFactory, jsonContent);
+    }
+
+    return null;
+  }
 }
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
index e5bc94b..43b9b34 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.client.googleapis.batch.BatchCallback;
 import com.google.api.client.googleapis.batch.BatchRequest;
+import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient;
 import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpHeaders;
 import com.google.api.client.http.HttpMethods;
@@ -29,6 +30,7 @@ import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpRequestInitializer;
 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.JsonFactory;
 import com.google.api.client.json.JsonObjectParser;
@@ -60,16 +62,10 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
 
   private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send";
 
-  private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch";
-
-  private static final String API_FORMAT_VERSION_HEADER = "X-GOOG-API-FORMAT-VERSION";
-
-  private static final String CLIENT_VERSION_HEADER = "X-Firebase-Client";
-
   private static final Map<String, String> COMMON_HEADERS =
       ImmutableMap.of(
-          API_FORMAT_VERSION_HEADER, "2",
-          CLIENT_VERSION_HEADER, "fire-admin-java/" + SdkUtils.getVersion());
+          "X-GOOG-API-FORMAT-VERSION", "2",
+          "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion());
 
   private final String fcmSendUrl;
   private final HttpRequestFactory requestFactory;
@@ -78,6 +74,7 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
   private final HttpResponseInterceptor responseInterceptor;
   private final MessagingErrorHandler errorHandler;
   private final ErrorHandlingHttpClient<FirebaseMessagingException> httpClient;
+  private final MessagingBatchClient batchClient;
 
   private FirebaseMessagingClientImpl(Builder builder) {
     checkArgument(!Strings.isNullOrEmpty(builder.projectId));
@@ -87,8 +84,9 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     this.jsonFactory = checkNotNull(builder.jsonFactory);
     this.responseInterceptor = builder.responseInterceptor;
     this.errorHandler = new MessagingErrorHandler(this.jsonFactory);
-    this.httpClient = new ErrorHandlingHttpClient<>(
-        this.requestFactory, this.jsonFactory, this.errorHandler);
+    this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler)
+      .setInterceptor(responseInterceptor);
+    this.batchClient = new MessagingBatchClient(requestFactory.getTransport(), jsonFactory);
   }
 
   @VisibleForTesting
@@ -123,10 +121,9 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
   private String sendSingleRequest(
       Message message, boolean dryRun) throws FirebaseMessagingException {
     HttpRequestInfo request =
-        HttpRequestInfo.buildPostRequest(
-            fcmSendUrl, new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun)))
-            .addAllHeaders(COMMON_HEADERS)
-            .setResponseInterceptor(responseInterceptor);
+        HttpRequestInfo.buildJsonPostRequest(
+            fcmSendUrl, message.wrapForTransport(dryRun))
+            .addAllHeaders(COMMON_HEADERS);
     MessagingServiceResponse parsed = httpClient.sendAndParse(
         request, MessagingServiceResponse.class);
     return parsed.getMessageId();
@@ -141,7 +138,8 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
       batch.execute();
       return new BatchResponseImpl(callback.getResponses());
     } catch (HttpResponseException e) {
-      OutgoingHttpRequest req = new OutgoingHttpRequest(HttpMethods.POST, FCM_BATCH_URL);
+      OutgoingHttpRequest req = new OutgoingHttpRequest(
+          HttpMethods.POST, MessagingBatchClient.FCM_BATCH_URL);
       IncomingHttpResponse resp = new IncomingHttpResponse(e, req);
       throw errorHandler.handleHttpResponseException(e, resp);
     } catch (IOException e) {
@@ -152,10 +150,7 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
   private BatchRequest newBatchRequest(
       List<Message> messages, boolean dryRun, MessagingBatchCallback callback) throws IOException {
 
-    BatchRequest batch = new BatchRequest(
-        requestFactory.getTransport(), getBatchRequestInitializer());
-    batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL));
-
+    BatchRequest batch = batchClient.batch(getBatchRequestInitializer());
     final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory);
     final GenericUrl sendUrl = new GenericUrl(fcmSendUrl);
     for (Message message : messages) {
@@ -176,6 +171,8 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     return new HttpRequestInitializer() {
       @Override
       public void initialize(HttpRequest request) throws IOException {
+        // Batch requests are not executed on the ErrorHandlingHttpClient. Therefore, they
+        // require some special handling at initialization.
         HttpRequestInitializer initializer = requestFactory.getInitializer();
         if (initializer != null) {
           initializer.initialize(request);
@@ -319,4 +316,33 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
       return new MessagingServiceErrorResponse();
     }
   }
+
+  private static class MessagingBatchClient extends AbstractGoogleJsonClient {
+
+    private static final String FCM_ROOT_URL = "https://fcm.googleapis.com";
+    private static final String FCM_BATCH_PATH = "batch";
+    private static final String FCM_BATCH_URL = String.format(
+        "%s/%s", FCM_ROOT_URL, FCM_BATCH_PATH);
+
+    MessagingBatchClient(HttpTransport transport, JsonFactory jsonFactory) {
+      super(new Builder(transport, jsonFactory));
+    }
+
+    private MessagingBatchClient(Builder builder) {
+      super(builder);
+    }
+
+    private static class Builder extends AbstractGoogleJsonClient.Builder {
+      Builder(HttpTransport transport, JsonFactory jsonFactory) {
+        super(transport, jsonFactory, FCM_ROOT_URL, "", null, false);
+        setBatchPath(FCM_BATCH_PATH);
+        setApplicationName("fire-admin-java");
+      }
+
+      @Override
+      public AbstractGoogleJsonClient build() {
+        return new MessagingBatchClient(this);
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
index 81e953f..5648fcf 100644
--- a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
+++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
@@ -16,15 +16,11 @@
 
 package com.google.firebase.messaging;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.util.Key;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
@@ -34,7 +30,6 @@ import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.ErrorHandlingHttpClient;
 import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.Nullable;
-
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -52,8 +47,6 @@ final class InstanceIdClientImpl implements InstanceIdClient {
   private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove";
 
   private final ErrorHandlingHttpClient<FirebaseMessagingException> requestFactory;
-  private final JsonFactory jsonFactory;
-  private final HttpResponseInterceptor responseInterceptor;
 
   InstanceIdClientImpl(HttpRequestFactory requestFactory, JsonFactory jsonFactory) {
     this(requestFactory, jsonFactory, null);
@@ -63,10 +56,9 @@ final class InstanceIdClientImpl implements InstanceIdClient {
       HttpRequestFactory requestFactory,
       JsonFactory jsonFactory,
       @Nullable HttpResponseInterceptor responseInterceptor) {
-    this.requestFactory = new ErrorHandlingHttpClient<>(
-        requestFactory, jsonFactory, new InstanceIdErrorHandler(jsonFactory));
-    this.jsonFactory = checkNotNull(jsonFactory);
-    this.responseInterceptor = responseInterceptor;
+    InstanceIdErrorHandler errorHandler = new InstanceIdErrorHandler(jsonFactory);
+    this.requestFactory = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler)
+      .setInterceptor(responseInterceptor);
   }
 
   static InstanceIdClientImpl fromApp(FirebaseApp app) {
@@ -75,11 +67,6 @@ final class InstanceIdClientImpl implements InstanceIdClient {
         app.getOptions().getJsonFactory());
   }
 
-  @VisibleForTesting
-  JsonFactory getJsonFactory() {
-    return jsonFactory;
-  }
-
   public TopicManagementResponse subscribeToTopic(
       String topic, List<String> registrationTokens) throws FirebaseMessagingException {
     return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH);
@@ -101,10 +88,8 @@ final class InstanceIdClientImpl implements InstanceIdClient {
         "registration_tokens", registrationTokens
     );
 
-    HttpRequestInfo request =
-        HttpRequestInfo.buildPostRequest(url, new JsonHttpContent(jsonFactory, payload))
-            .addHeader("access_token_auth", "true")
-            .setResponseInterceptor(responseInterceptor);
+    HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(url, payload)
+        .addHeader("access_token_auth", "true");
     InstanceIdServiceResponse response = new InstanceIdServiceResponse();
     requestFactory.sendAndParse(request, response);
     return new TopicManagementResponse(response.results);
diff --git a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java
index a9282ea..f9ec62a 100644
--- a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java
+++ b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java
@@ -16,11 +16,10 @@
 
 package com.google.firebase.projectmanagement;
 
+import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.JsonFactory;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.IncomingHttpResponse;
 import com.google.firebase.internal.AbstractPlatformErrorHandler;
@@ -30,24 +29,19 @@ import com.google.firebase.internal.SdkUtils;
 
 final class HttpHelper {
 
-  @VisibleForTesting static final String PATCH_OVERRIDE_KEY = "X-HTTP-Method-Override";
-  @VisibleForTesting static final String PATCH_OVERRIDE_VALUE = "PATCH";
   private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
 
-  private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
-  private final JsonFactory jsonFactory;
-  private final ErrorHandlingHttpClient<FirebaseProjectManagementException> httpClient;
+  private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion();
 
-  private HttpResponseInterceptor interceptor;
+  private final ErrorHandlingHttpClient<FirebaseProjectManagementException> httpClient;
 
   HttpHelper(JsonFactory jsonFactory, HttpRequestFactory requestFactory) {
-    this.jsonFactory = jsonFactory;
     ProjectManagementErrorHandler errorHandler = new ProjectManagementErrorHandler(jsonFactory);
     this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler);
   }
 
   void setInterceptor(HttpResponseInterceptor interceptor) {
-    this.interceptor = interceptor;
+    httpClient.setInterceptor(interceptor);
   }
 
   <T> IncomingHttpResponse makeGetRequest(
@@ -69,7 +63,7 @@ final class HttpHelper {
       String requestIdentifier,
       String requestIdentifierDescription) throws FirebaseProjectManagementException {
     return makeRequest(
-        HttpRequestInfo.buildPostRequest(url, new JsonHttpContent(jsonFactory, payload)),
+        HttpRequestInfo.buildJsonPostRequest(url, payload),
         parsedResponseInstance,
         requestIdentifier,
         requestIdentifierDescription);
@@ -81,11 +75,11 @@ final class HttpHelper {
       T parsedResponseInstance,
       String requestIdentifier,
       String requestIdentifierDescription) throws FirebaseProjectManagementException {
-    HttpRequestInfo baseRequest = HttpRequestInfo.buildPostRequest(
-        url, new JsonHttpContent(jsonFactory, payload));
-    baseRequest.addHeader(PATCH_OVERRIDE_KEY, PATCH_OVERRIDE_VALUE);
     makeRequest(
-        baseRequest, parsedResponseInstance, requestIdentifier, requestIdentifierDescription);
+        HttpRequestInfo.buildJsonRequest(HttpMethods.PATCH, url, payload),
+        parsedResponseInstance,
+        requestIdentifier,
+        requestIdentifierDescription);
   }
 
   <T> void makeDeleteRequest(
@@ -106,8 +100,7 @@ final class HttpHelper {
       String requestIdentifier,
       String requestIdentifierDescription) throws FirebaseProjectManagementException {
     try {
-      baseRequest.addHeader(CLIENT_VERSION_HEADER, clientVersion);
-      baseRequest.setResponseInterceptor(interceptor);
+      baseRequest.addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION);
       IncomingHttpResponse response = httpClient.send(baseRequest);
       httpClient.parse(response, parsedResponseInstance);
       return response;


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index a71280b..5f4cc15 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -45,7 +45,6 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.FirebaseUserManager.EmailLinkType;
-import com.google.firebase.auth.internal.AuthHttpClient;
 import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth;
 import com.google.firebase.auth.multitenancy.TenantManager;
 import com.google.firebase.internal.SdkUtils;
@@ -92,6 +91,10 @@ public class FirebaseUserManagerTest {
 
   private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants";
 
+  private static final String SAML_RESPONSE = TestUtils.loadResource("saml.json");
+
+  private static final String OIDC_RESPONSE = TestUtils.loadResource("oidc.json");
+
   @After
   public void tearDown() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
@@ -279,7 +282,7 @@ public class FirebaseUserManagerTest {
         ).replace("'", "\""));
 
     UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist");
-    List<UserIdentifier> ids = ImmutableList.<UserIdentifier>of(
+    List<UserIdentifier> ids = ImmutableList.of(
         new UidIdentifier("uid1"),
         new EmailIdentifier("user2@example.com"),
         new PhoneIdentifier("+15555550003"),
@@ -301,7 +304,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testInvalidUidIdentifier() throws Exception {
+  public void testInvalidUidIdentifier() {
     try {
       new UidIdentifier("too long " + Strings.repeat(".", 128));
       fail("No error thrown for invalid uid");
@@ -311,7 +314,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testInvalidEmailIdentifier() throws Exception {
+  public void testInvalidEmailIdentifier() {
     try {
       new EmailIdentifier("invalid email addr");
       fail("No error thrown for invalid email");
@@ -321,7 +324,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testInvalidPhoneIdentifier() throws Exception {
+  public void testInvalidPhoneIdentifier() {
     try {
       new PhoneIdentifier("invalid phone number");
       fail("No error thrown for invalid phone number");
@@ -331,7 +334,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testInvalidProviderIdentifier() throws Exception {
+  public void testInvalidProviderIdentifier() {
     try {
       new ProviderIdentifier("", "valid-uid");
       fail("No error thrown for invalid provider id");
@@ -454,7 +457,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testDeleteUsersExceeds1000() throws Exception {
+  public void testDeleteUsersExceeds1000() {
     FirebaseApp.initializeApp(new FirebaseOptions.Builder()
             .setCredentials(credentials)
             .build());
@@ -471,7 +474,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testDeleteUsersInvalidId() throws Exception {
+  public void testDeleteUsersInvalidId() {
     FirebaseApp.initializeApp(new FirebaseOptions.Builder()
             .setCredentials(credentials)
             .build());
@@ -1481,8 +1484,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateOidcProvider() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setProviderId("oidc.provider-id")
@@ -1507,8 +1509,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateOidcProviderAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setProviderId("oidc.provider-id")
@@ -1534,8 +1535,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateOidcProviderMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config
     // creation request.
     OidcProviderConfig.CreateRequest createRequest =
@@ -1557,25 +1557,32 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testCreateOidcProviderError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+  public void testCreateOidcProviderError() {
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id");
+
     try {
-      FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
+      auth.createOidcProviderConfig(createRequest);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-
-    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
   }
 
   @Test
   public void testCreateOidcProviderMissingId() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setDisplayName("DISPLAY_NAME")
@@ -1593,8 +1600,7 @@ public class FirebaseUserManagerTest {
   @Test
   public void testTenantAwareCreateOidcProvider() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("oidc.json"));
+        "TENANT_ID", OIDC_RESPONSE);
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setProviderId("oidc.provider-id")
@@ -1613,8 +1619,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProvider() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.UpdateRequest request =
         new OidcProviderConfig.UpdateRequest("oidc.provider-id")
             .setDisplayName("DISPLAY_NAME")
@@ -1638,8 +1643,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProviderAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.UpdateRequest request =
         new OidcProviderConfig.UpdateRequest("oidc.provider-id")
             .setDisplayName("DISPLAY_NAME")
@@ -1664,8 +1668,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProviderMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
     OidcProviderConfig.UpdateRequest request =
         new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME");
 
@@ -1683,7 +1686,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProviderConfigNoValues() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(OIDC_RESPONSE);
     try {
       FirebaseAuth.getInstance().updateOidcProviderConfig(
           new OidcProviderConfig.UpdateRequest("oidc.provider-id"));
@@ -1695,25 +1698,32 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProviderConfigError() {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     OidcProviderConfig.UpdateRequest request =
         new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME");
+
     try {
-      FirebaseAuth.getInstance().updateOidcProviderConfig(request);
+      auth.updateOidcProviderConfig(request);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
   }
 
   @Test
   public void testTenantAwareUpdateOidcProvider() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("oidc.json"));
+        "TENANT_ID", OIDC_RESPONSE);
     TenantAwareFirebaseAuth tenantAwareAuth =
         FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
     OidcProviderConfig.UpdateRequest request =
@@ -1740,8 +1750,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfig() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
 
     OidcProviderConfig config =
         FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id");
@@ -1753,8 +1762,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfigAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
 
     OidcProviderConfig config =
         FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get();
@@ -1766,7 +1774,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfigMissingId() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(OIDC_RESPONSE);
 
     try {
       FirebaseAuth.getInstance().getOidcProviderConfig(null);
@@ -1778,7 +1786,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfigInvalidId() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(OIDC_RESPONSE);
 
     try {
       FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id");
@@ -1789,7 +1797,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testGetOidcProviderConfigWithNotFoundError() throws Exception {
+  public void testGetOidcProviderConfigWithNotFoundError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
@@ -1798,6 +1806,13 @@ public class FirebaseUserManagerTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(
+          "No IdP configuration found corresponding to the provided identifier "
+              + "(CONFIGURATION_NOT_FOUND).",
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
   }
@@ -1805,8 +1820,7 @@ public class FirebaseUserManagerTest {
   @Test
   public void testGetTenantAwareOidcProviderConfig() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("oidc.json"));
+        "TENANT_ID", OIDC_RESPONSE);
     TenantAwareFirebaseAuth tenantAwareAuth =
         FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
 
@@ -1856,18 +1870,25 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testListOidcProviderConfigsError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+  public void testListOidcProviderConfigsError() {
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
 
     try {
-      FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99);
+      auth.listOidcProviderConfigs(null, 99);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs");
   }
 
   @Test
@@ -1975,6 +1996,13 @@ public class FirebaseUserManagerTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(
+          "No IdP configuration found corresponding to the provided identifier "
+              + "(CONFIGURATION_NOT_FOUND).",
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN");
   }
@@ -1996,8 +2024,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateSamlProvider() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest()
           .setProviderId("saml.provider-id")
@@ -2042,8 +2069,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateSamlProviderAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest()
           .setProviderId("saml.provider-id")
@@ -2089,8 +2115,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateSamlProviderMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML
     // provider config creation request.
     SamlProviderConfig.CreateRequest createRequest =
@@ -2130,23 +2155,31 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testCreateSamlProviderError() {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id");
+
     try {
-      FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
+      auth.createSamlProviderConfig(createRequest);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs");
   }
 
   @Test
   public void testCreateSamlProviderMissingId() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest()
           .setDisplayName("DISPLAY_NAME")
@@ -2168,8 +2201,7 @@ public class FirebaseUserManagerTest {
   @Test
   public void testTenantAwareCreateSamlProvider() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("saml.json"));
+        "TENANT_ID", SAML_RESPONSE);
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest()
           .setProviderId("saml.provider-id")
@@ -2192,8 +2224,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateSamlProvider() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.UpdateRequest updateRequest =
         new SamlProviderConfig.UpdateRequest("saml.provider-id")
           .setDisplayName("DISPLAY_NAME")
@@ -2240,8 +2271,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateSamlProviderAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.UpdateRequest updateRequest =
         new SamlProviderConfig.UpdateRequest("saml.provider-id")
           .setDisplayName("DISPLAY_NAME")
@@ -2289,8 +2319,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateSamlProviderMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
     SamlProviderConfig.UpdateRequest request =
         new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME");
 
@@ -2308,7 +2337,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateSamlProviderConfigNoValues() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(SAML_RESPONSE);
     try {
       FirebaseAuth.getInstance().updateSamlProviderConfig(
           new SamlProviderConfig.UpdateRequest("saml.provider-id"));
@@ -2319,26 +2348,33 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testUpdateSamlProviderConfigError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+  public void testUpdateSamlProviderConfigError() {
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     SamlProviderConfig.UpdateRequest request =
         new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME");
+
     try {
-      FirebaseAuth.getInstance().updateSamlProviderConfig(request);
+      auth.updateSamlProviderConfig(request);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
   }
 
   @Test
   public void testTenantAwareUpdateSamlProvider() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("saml.json"));
+        "TENANT_ID", SAML_RESPONSE);
     TenantAwareFirebaseAuth tenantAwareAuth =
         FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
     SamlProviderConfig.UpdateRequest updateRequest =
@@ -2370,8 +2406,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetSamlProviderConfig() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
 
     SamlProviderConfig config =
         FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id");
@@ -2383,8 +2418,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetSamlProviderConfigAsync() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE);
 
     SamlProviderConfig config =
         FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get();
@@ -2408,7 +2442,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetSamlProviderConfigInvalidId() throws Exception {
-    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(SAML_RESPONSE);
 
     try {
       FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id");
@@ -2428,6 +2462,13 @@ public class FirebaseUserManagerTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(
+          "No IdP configuration found corresponding to the provided identifier "
+              + "(CONFIGURATION_NOT_FOUND).",
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
   }
@@ -2435,8 +2476,7 @@ public class FirebaseUserManagerTest {
   @Test
   public void testGetTenantAwareSamlProviderConfig() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
-        "TENANT_ID",
-        TestUtils.loadResource("saml.json"));
+        "TENANT_ID", SAML_RESPONSE);
     TenantAwareFirebaseAuth tenantAwareAuth =
         FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
 
@@ -2487,18 +2527,25 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testListSamlProviderConfigsError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+  public void testListSamlProviderConfigsError() {
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(message)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
 
     try {
-      FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99);
+      auth.listSamlProviderConfigs(null, 99);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals(
+          "Unexpected HTTP response with status: 500\n" + message,
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs");
   }
 
   @Test
@@ -2606,6 +2653,13 @@ public class FirebaseUserManagerTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(
+          "No IdP configuration found corresponding to the provided identifier "
+              + "(CONFIGURATION_NOT_FOUND).",
+          e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN");
   }
@@ -2765,13 +2819,7 @@ public class FirebaseUserManagerTest {
 
   private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) {
     HttpRequest request = interceptor.getResponse().getRequest();
-    if (method.equals("PATCH")) {
-      assertEquals("PATCH",
-          request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override"));
-      assertEquals("POST", request.getRequestMethod());
-    } else {
-      assertEquals(method, request.getRequestMethod());
-    }
+    assertEquals(method, request.getRequestMethod());
     assertEquals(url, request.getUrl().toString().split("\\?")[0]);
   }
 
diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
index 588ce33..e027882 100644
--- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
+++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
@@ -36,8 +36,9 @@ public class ProviderConfigTestUtils {
       fail("No error thrown for getting a deleted OIDC provider config.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(ErrorCode.NOT_FOUND,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -48,8 +49,9 @@ public class ProviderConfigTestUtils {
       fail("No error thrown for getting a deleted SAML provider config.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(ErrorCode.NOT_FOUND,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode());
+      assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
index 86eb6b4..c03d205 100644
--- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth.multitenancy;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -26,6 +27,7 @@ import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpHeaders;
 import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.testing.http.MockHttpTransport;
@@ -37,6 +39,7 @@ import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.AuthErrorCode;
 import com.google.firebase.auth.FirebaseAuth;
 import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.MockGoogleCredentials;
@@ -91,6 +94,11 @@ public class FirebaseTenantClientTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(
+          "No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.TENANT_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN");
   }
@@ -183,16 +191,21 @@ public class FirebaseTenantClientTest {
 
   @Test
   public void testCreateTenantError() {
-    TestResponseInterceptor interceptor =
-        initializeAppForTenantManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse()
+        .setStatusCode(500)
+        .setContent(message));
+
     try {
-      FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest());
+      tenantManager.createTenant(new Tenant.CreateRequest());
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
   }
 
   @Test
@@ -252,18 +265,23 @@ public class FirebaseTenantClientTest {
 
   @Test
   public void testUpdateTenantError() {
-    TestResponseInterceptor interceptor =
-        initializeAppForTenantManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}";
+    TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse()
+        .setStatusCode(500)
+        .setContent(message));
     Tenant.UpdateRequest request =
         new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME");
+
     try {
-      FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
+      tenantManager.updateTenant(request);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCode());
+      assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
-    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
   }
 
   @Test
@@ -286,6 +304,10 @@ public class FirebaseTenantClientTest {
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode());
+      assertEquals("No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.TENANT_NOT_FOUND, e.getAuthErrorCode());
     }
     checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN");
   }
@@ -308,18 +330,22 @@ public class FirebaseTenantClientTest {
 
   private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) {
     HttpRequest request = interceptor.getResponse().getRequest();
-    if (method.equals("PATCH")) {
-      assertEquals("PATCH",
-          request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override"));
-      assertEquals("POST", request.getRequestMethod());
-    } else {
-      assertEquals(method, request.getRequestMethod());
-    }
+    assertEquals(method, request.getRequestMethod());
     assertEquals(url, request.getUrl().toString().split("\\?")[0]);
   }
 
   private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) {
-    initializeAppWithResponses(responses);
+    List<MockLowLevelHttpResponse> mocks = new ArrayList<>();
+    for (String response : responses) {
+      mocks.add(new MockLowLevelHttpResponse().setContent(response));
+    }
+    MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks);
+    FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(credentials)
+        .setHttpTransport(transport)
+        .setProjectId("test-project-id")
+        .build());
+
     TestResponseInterceptor interceptor = new TestResponseInterceptor();
     FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor);
     return interceptor;
@@ -327,7 +353,7 @@ public class FirebaseTenantClientTest {
 
   private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode(
       int statusCode, String response) {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+    FirebaseApp.initializeApp(FirebaseOptions.builder()
         .setCredentials(credentials)
         .setHttpTransport(
             new MockHttpTransport.Builder()
@@ -341,17 +367,16 @@ public class FirebaseTenantClientTest {
     return interceptor;
   }
 
-  private static void initializeAppWithResponses(String... responses) {
-    List<MockLowLevelHttpResponse> mocks = new ArrayList<>();
-    for (String response : responses) {
-      mocks.add(new MockLowLevelHttpResponse().setContent(response));
-    }
-    MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks);
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+  private static TenantManager createRetryDisabledTenantManager(MockLowLevelHttpResponse response) {
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
         .setCredentials(credentials)
-        .setHttpTransport(transport)
-        .setProjectId("test-project-id")
         .build());
+    FirebaseTenantClient tenantClient = new FirebaseTenantClient(
+        "test-project-id", Utils.getDefaultJsonFactory(), transport.createRequestFactory());
+    return new TenantManager(app, tenantClient);
   }
 
   private static GenericJson parseRequestContent(TestResponseInterceptor interceptor)
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
index 1949dc5..44d75e8 100644
--- a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
@@ -28,6 +28,7 @@ import com.google.api.core.ApiFutureCallback;
 import com.google.api.core.ApiFutures;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.ErrorCode;
+import com.google.firebase.auth.AuthErrorCode;
 import com.google.firebase.auth.FirebaseAuth;
 import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.testing.IntegrationTestUtils;
@@ -81,8 +82,9 @@ public class TenantManagerIT {
       fail("No error thrown for getting a deleted tenant");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(ErrorCode.NOT_FOUND,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode());
+      assertEquals(AuthErrorCode.TENANT_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
diff --git a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
index 31cc1d0..5919140 100644
--- a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
+++ b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
@@ -23,13 +23,14 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.ByteArrayContent;
+import com.google.api.client.http.HttpContent;
 import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpStatusCodes;
 import com.google.api.client.http.LowLevelHttpRequest;
-import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
@@ -38,6 +39,7 @@ import com.google.api.client.util.GenericData;
 import com.google.auth.oauth2.AccessToken;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseException;
@@ -108,15 +110,15 @@ public class ErrorHandlingHttpClientTest {
   public void testSuccessfulRequestWithHeadersAndBody() throws FirebaseException, IOException {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setContent("{\"foo\": \"bar\"}");
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
-
-    HttpRequestInfo request = HttpRequestInfo.buildPostRequest(
-        "https://firebase.google.com",
-        new JsonHttpContent(DEFAULT_JSON_FACTORY, ImmutableMap.of("key", "value")));
     TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response)
+        .setInterceptor(interceptor);
+
+    HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(
+        "https://firebase.google.com", ImmutableMap.of("key", "value"));
+
     request.addHeader("h1", "v1")
-        .addAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3"))
-        .setResponseInterceptor(interceptor);
+        .addAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3"));
     GenericData body = client.sendAndParse(request, GenericData.class);
 
     assertEquals(1, body.size());
@@ -132,6 +134,55 @@ public class ErrorHandlingHttpClientTest {
     assertEquals("{\"key\":\"value\"}", out.toString());
   }
 
+  @Test
+  public void testSuccessfulRequestWithNonJsonBody() throws FirebaseException, IOException {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("{\"foo\": \"bar\"}");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response)
+        .setInterceptor(interceptor);
+    HttpContent content = new ByteArrayContent("text/plain", "Test".getBytes());
+
+    HttpRequestInfo request = HttpRequestInfo.buildRequest(
+        HttpMethods.POST, "https://firebase.google.com", content);
+
+    GenericData body = client.sendAndParse(request, GenericData.class);
+
+    assertEquals(1, body.size());
+    assertEquals("bar", body.get("foo"));
+    HttpRequest last = interceptor.getLastRequest();
+    assertEquals(HttpMethods.POST, last.getRequestMethod());
+    assertEquals("text/plain", last.getContent().getType());
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    last.getContent().writeTo(out);
+    assertEquals("Test", out.toString());
+  }
+
+  @Test
+  public void testUnsupportedMethod() throws FirebaseException, IOException {
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("{}"))
+        .setSupportedMethods(ImmutableSet.of(HttpMethods.GET, HttpMethods.POST))
+        .build();
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    ErrorHandlingHttpClient<FirebaseException> client = new ErrorHandlingHttpClient<>(
+        transport.createRequestFactory(), DEFAULT_JSON_FACTORY, new TestHttpErrorHandler());
+    client.setInterceptor(interceptor);
+    HttpRequestInfo patchRequest = HttpRequestInfo.buildJsonRequest(
+        HttpMethods.PATCH, "https://firebase.google.com", ImmutableMap.of("key", "value"));
+
+    client.sendAndParse(patchRequest, GenericData.class);
+
+    HttpRequest last = interceptor.getLastRequest();
+    assertEquals(HttpMethods.POST, last.getRequestMethod());
+    assertEquals(HttpMethods.PATCH, last.getHeaders().get("X-HTTP-Method-Override"));
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    last.getContent().writeTo(out);
+    assertEquals("{\"key\":\"value\"}", out.toString());
+  }
+
   @Test
   public void testNetworkError() {
     final IOException exception = new IOException("Test");
diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
index bac2a51..70bf9a2 100644
--- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
+++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
@@ -378,7 +378,6 @@ public class InstanceIdClientImplTest {
     try {
       InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app);
 
-      assertSame(options.getJsonFactory(), client.getJsonFactory());
       client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2"));
       fail("No error thrown for error response");
     } catch (FirebaseMessagingException e) {
diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
index 0c4cfca..8d227ef 100644
--- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
+++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
@@ -18,8 +18,6 @@ package com.google.firebase.projectmanagement;
 
 import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.FIREBASE_PROJECT_MANAGEMENT_URL;
 import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.MAXIMUM_LIST_APPS_PAGE_SIZE;
-import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_KEY;
-import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_VALUE;
 import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_1;
 import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_256;
 import static org.junit.Assert.assertEquals;
@@ -530,11 +528,10 @@ public class FirebaseProjectManagementServiceImplTest {
         "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name",
         FIREBASE_PROJECT_MANAGEMENT_URL,
         IOS_APP_ID);
-    checkRequestHeader(expectedUrl, HttpMethod.POST);
+    checkRequestHeader(expectedUrl, HttpMethod.PATCH);
     ImmutableMap<String, String> payload =
         ImmutableMap.<String, String>builder().put("display_name", DISPLAY_NAME).build();
     checkRequestPayload(payload);
-    checkPatchRequest();
   }
 
   @Test
@@ -548,11 +545,10 @@ public class FirebaseProjectManagementServiceImplTest {
         "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name",
         FIREBASE_PROJECT_MANAGEMENT_URL,
         IOS_APP_ID);
-    checkRequestHeader(expectedUrl, HttpMethod.POST);
+    checkRequestHeader(expectedUrl, HttpMethod.PATCH);
     ImmutableMap<String, String> payload =
         ImmutableMap.<String, String>builder().put("display_name", DISPLAY_NAME).build();
     checkRequestPayload(payload);
-    checkPatchRequest();
   }
 
   @Test
@@ -872,11 +868,10 @@ public class FirebaseProjectManagementServiceImplTest {
         "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name",
         FIREBASE_PROJECT_MANAGEMENT_URL,
         ANDROID_APP_ID);
-    checkRequestHeader(expectedUrl, HttpMethod.POST);
+    checkRequestHeader(expectedUrl, HttpMethod.PATCH);
     ImmutableMap<String, String> payload =
         ImmutableMap.<String, String>builder().put("display_name", DISPLAY_NAME).build();
     checkRequestPayload(payload);
-    checkPatchRequest();
   }
 
   @Test
@@ -890,11 +885,10 @@ public class FirebaseProjectManagementServiceImplTest {
         "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name",
         FIREBASE_PROJECT_MANAGEMENT_URL,
         ANDROID_APP_ID);
-    checkRequestHeader(expectedUrl, HttpMethod.POST);
+    checkRequestHeader(expectedUrl, HttpMethod.PATCH);
     ImmutableMap<String, String> payload =
         ImmutableMap.<String, String>builder().put("display_name", DISPLAY_NAME).build();
     checkRequestPayload(payload);
-    checkPatchRequest();
   }
 
   @Test
@@ -1147,21 +1141,11 @@ public class FirebaseProjectManagementServiceImplTest {
     assertEquals(expected, parsed);
   }
 
-  private void checkPatchRequest() {
-    assertEquals(
-        "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses());
-    checkPatchRequest(0);
-  }
-
-  private void checkPatchRequest(int index) {
-    assertTrue(interceptor.getResponse(index).getRequest().getHeaders()
-        .getHeaderStringValues(PATCH_OVERRIDE_KEY).contains(PATCH_OVERRIDE_VALUE));
-  }
-
   private enum HttpMethod {
     GET,
     POST,
-    DELETE
+    DELETE,
+    PATCH,
   }
 
   /**