Repository: firebase/firebase-admin-java
Pull request #360 (2020-02-06 21:15:10)

fix(auth): Migrated user management APIs to the new error handling scheme
1. Added `AuthErrorCode` enum type with just the error codes necessary to update user management APIs.
2. Added a new constructor to `FirebaseAuthException` and deprecated the old ones. These will be removed completely by the end of this refactor.
3. Deprecated the string error code currently included in `FirebaseAuthException`. This will also be removed by the end of the refactor.
4. Updated the `FirebaseUserManager` class to the new error handling scheme.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
new file mode 100644
index 0000000..83287d7
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+/**
+ * Error codes that can be raised by the Firebase Auth APIs.
+ */
+public enum AuthErrorCode {
+
+  /**
+   * A user already exists with the provided email.
+   */
+  EMAIL_ALREADY_EXISTS,
+
+  /**
+   * The provided dynamic link domain is not configured or authorized for the current project.
+   */
+  INVALID_DYNAMIC_LINK_DOMAIN,
+
+  /**
+   * A user already exists with the provided phone number.
+   */
+  PHONE_NUMBER_ALREADY_EXISTS,
+
+  /**
+   * A user already exists with the provided UID.
+   */
+  UID_ALREADY_EXISTS,
+
+  /**
+   * The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase
+   * console.
+   */
+  UNAUTHORIZED_CONTINUE_URL,
+
+  /**
+   * No user record found for the given identifier.
+   */
+  USER_NOT_FOUND,
+}
diff --git a/src/main/java/com/google/firebase/auth/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/AuthErrorHandler.java
new file mode 100644
index 0000000..a6cea39
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/AuthErrorHandler.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+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.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.ErrorCode;
+import com.google.firebase.FirebaseException;
+import com.google.firebase.internal.AbstractHttpErrorHandler;
+import com.google.firebase.internal.Nullable;
+import java.io.IOException;
+import java.util.Map;
+
+final class AuthErrorHandler extends AbstractHttpErrorHandler<FirebaseAuthException> {
+
+  private static final Map<String, AuthError> ERROR_CODES =
+      ImmutableMap.<String, AuthError>builder()
+          .put(
+              "DUPLICATE_EMAIL",
+              new AuthError(
+                  ErrorCode.ALREADY_EXISTS,
+                  "The user with the provided email already exists",
+                  AuthErrorCode.EMAIL_ALREADY_EXISTS))
+          .put(
+              "DUPLICATE_LOCAL_ID",
+              new AuthError(
+                  ErrorCode.ALREADY_EXISTS,
+                  "The user with the provided uid already exists",
+                  AuthErrorCode.UID_ALREADY_EXISTS))
+          .put(
+              "EMAIL_EXISTS",
+              new AuthError(
+                  ErrorCode.ALREADY_EXISTS,
+                  "The user with the provided email already exists",
+                  AuthErrorCode.EMAIL_ALREADY_EXISTS))
+          .put(
+              "INVALID_DYNAMIC_LINK_DOMAIN",
+              new AuthError(
+                  ErrorCode.INVALID_ARGUMENT,
+                  "The provided dynamic link domain is not "
+                      + "configured or authorized for the current project",
+                  AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN))
+          .put(
+              "PHONE_NUMBER_EXISTS",
+              new AuthError(
+                  ErrorCode.ALREADY_EXISTS,
+                  "The user with the provided phone number already exists",
+                  AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS))
+          .put(
+              "UNAUTHORIZED_DOMAIN",
+              new AuthError(
+                  ErrorCode.INVALID_ARGUMENT,
+                  "The domain of the continue URL is not whitelisted",
+                  AuthErrorCode.UNAUTHORIZED_CONTINUE_URL))
+          .put(
+              "USER_NOT_FOUND",
+              new AuthError(
+                  ErrorCode.NOT_FOUND,
+                  "No user record found for the given identifier",
+                  AuthErrorCode.USER_NOT_FOUND))
+          .build();
+
+  private final JsonFactory jsonFactory;
+
+  AuthErrorHandler(JsonFactory jsonFactory) {
+    this.jsonFactory = checkNotNull(jsonFactory);
+  }
+
+  @Override
+  protected FirebaseAuthException createException(FirebaseException base) {
+    String response = getResponse(base);
+    AuthServiceErrorResponse parsed = safeParse(response);
+    AuthError errorInfo = ERROR_CODES.get(parsed.getCode());
+    if (errorInfo != null) {
+      return new FirebaseAuthException(
+          errorInfo.getErrorCode(),
+          errorInfo.buildMessage(parsed),
+          base.getCause(),
+          base.getHttpResponse(),
+          errorInfo.getAuthErrorCode());
+    }
+
+    return new FirebaseAuthException(
+        base.getErrorCodeNew(),
+        base.getMessage(),
+        base.getCause(),
+        base.getHttpResponse(),
+        null);
+  }
+
+  private String getResponse(FirebaseException base) {
+    if (base.getHttpResponse() == null) {
+      return null;
+    }
+
+    return base.getHttpResponse().getContent();
+  }
+
+  private AuthServiceErrorResponse safeParse(String response) {
+    AuthServiceErrorResponse parsed = new AuthServiceErrorResponse();
+    if (!Strings.isNullOrEmpty(response)) {
+      try {
+        jsonFactory.createJsonParser(response).parse(parsed);
+      } catch (IOException ignore) {
+        // Ignore any error that may occur while parsing the error response. The server
+        // may have responded with a non-json payload.
+      }
+    }
+
+    return parsed;
+  }
+
+  private static class AuthError {
+
+    private final ErrorCode errorCode;
+    private final String message;
+    private final AuthErrorCode authErrorCode;
+
+    AuthError(ErrorCode errorCode, String message, AuthErrorCode authErrorCode) {
+      this.errorCode = errorCode;
+      this.message = message;
+      this.authErrorCode = authErrorCode;
+    }
+
+    ErrorCode getErrorCode() {
+      return errorCode;
+    }
+
+    AuthErrorCode getAuthErrorCode() {
+      return authErrorCode;
+    }
+
+    String buildMessage(AuthServiceErrorResponse response) {
+      StringBuilder builder = new StringBuilder(this.message)
+          .append(" (").append(response.getCode()).append(")");
+      String detail = response.getDetail();
+      if (!Strings.isNullOrEmpty(detail)) {
+        builder.append(": ").append(detail);
+      } else {
+        builder.append(".");
+      }
+
+      return builder.toString();
+    }
+  }
+
+  /**
+   * JSON data binding for JSON error messages sent by Google identity toolkit service. These
+   * error messages take the form `{"error": {"message": "CODE: OPTIONAL DETAILS"}}`.
+   */
+  private static class AuthServiceErrorResponse {
+
+    @Key("error")
+    private GenericJson error;
+
+    @Nullable
+    public String getCode() {
+      String message = getMessage();
+      if (Strings.isNullOrEmpty(message)) {
+        return null;
+      }
+
+      int separator = message.indexOf(':');
+      if (separator != -1) {
+        return message.substring(0, separator);
+      }
+
+      return message;
+    }
+
+    @Nullable
+    public String getDetail() {
+      String message = getMessage();
+      if (Strings.isNullOrEmpty(message)) {
+        return null;
+      }
+
+      int separator = message.indexOf(':');
+      if (separator != -1) {
+        return message.substring(separator + 1).trim();
+      }
+
+      return null;
+    }
+
+    private String getMessage() {
+      if (error == null) {
+        return null;
+      }
+
+      return (String) error.get("message");
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
index 2314a69..bd7f60e 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
@@ -16,17 +16,14 @@
 
 package com.google.firebase.auth;
 
-// TODO: Move it out from firebase-common. Temporary host it their for
-// database's integration.http://b/27624510.
-
-// TODO: Decide if changing this not enforcing an error code. Need to align
-// with the decision in http://b/27677218. Also, need to turn this into abstract later.
-
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.base.Strings;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseException;
+import com.google.firebase.IncomingHttpResponse;
 import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
 
 /**
  * Generic exception related to Firebase Authentication. Check the error code and message for more
@@ -34,22 +31,42 @@ import com.google.firebase.internal.NonNull;
  */
 public class FirebaseAuthException extends FirebaseException {
 
-  private final String errorCode;
+  private final AuthErrorCode errorCode;
+  private final String deprecatedErrorCode;
+
+  FirebaseAuthException(
+      @NonNull ErrorCode errorCode,
+      @NonNull String message,
+      Throwable cause,
+      IncomingHttpResponse response,
+      AuthErrorCode authErrorCode) {
+    super(errorCode, message, cause, response);
+    this.errorCode = authErrorCode;
+    this.deprecatedErrorCode = null;
+  }
 
+  @Deprecated
   public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
     this(errorCode, detailMessage, null);
   }
 
-  public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage,
-                               Throwable throwable) {
+  @Deprecated
+  public FirebaseAuthException(
+      @NonNull String errorCode, @NonNull String detailMessage, Throwable throwable) {
     super(detailMessage, throwable);
     checkArgument(!Strings.isNullOrEmpty(errorCode));
-    this.errorCode = errorCode;
+    this.errorCode = null;
+    this.deprecatedErrorCode = errorCode;
   }
 
-  /** Returns an error code that may provide more information about the error. */
-  @NonNull
-  public String getErrorCode() {
+  @Nullable
+  public AuthErrorCode getAuthErrorCode() {
     return errorCode;
   }
+
+  /** Returns an error code that may provide more information about the error. */
+  @Deprecated
+  public String getDeprecatedErrorCode() {
+    return deprecatedErrorCode;
+  }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 03c2813..650f9d0 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -21,35 +21,33 @@ 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.HttpRequest;
+import com.google.api.client.http.HttpMethods;
 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.json.JsonHttpContent;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.JsonObjectParser;
 import com.google.api.client.util.Key;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 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.ImplFirebaseTrampolines;
+import com.google.firebase.IncomingHttpResponse;
 import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.auth.internal.DownloadAccountResponse;
 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.ApiClientUtils;
+import com.google.firebase.internal.ErrorHandlingHttpClient;
+import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 import com.google.firebase.internal.SdkUtils;
 
-import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
@@ -62,30 +60,6 @@ import java.util.Map;
  */
 class FirebaseUserManager {
 
-  static final String USER_NOT_FOUND_ERROR = "user-not-found";
-  static final String INTERNAL_ERROR = "internal-error";
-
-  // Map of server-side error codes to SDK error codes.
-  // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
-  private static final Map<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
-      .put("CLAIMS_TOO_LARGE", "claims-too-large")
-      .put("CONFIGURATION_NOT_FOUND", "project-not-found")
-      .put("INSUFFICIENT_PERMISSION", "insufficient-permission")
-      .put("DUPLICATE_EMAIL", "email-already-exists")
-      .put("DUPLICATE_LOCAL_ID", "uid-already-exists")
-      .put("EMAIL_EXISTS", "email-already-exists")
-      .put("INVALID_CLAIMS", "invalid-claims")
-      .put("INVALID_EMAIL", "invalid-email")
-      .put("INVALID_PAGE_SELECTION", "invalid-page-token")
-      .put("INVALID_PHONE_NUMBER", "invalid-phone-number")
-      .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
-      .put("PROJECT_NOT_FOUND", "project-not-found")
-      .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
-      .put("WEAK_PASSWORD", "invalid-password")
-      .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri")
-      .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
-      .build();
-
   static final int MAX_LIST_USERS_RESULTS = 1000;
   static final int MAX_IMPORT_USERS = 1000;
 
@@ -96,11 +70,11 @@ class FirebaseUserManager {
   private static final String ID_TOOLKIT_URL =
       "https://identitytoolkit.googleapis.com/v1/projects/%s";
   private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
+  private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion();
 
   private final String baseUrl;
   private final JsonFactory jsonFactory;
-  private final HttpRequestFactory requestFactory;
-  private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
+  private final ErrorHandlingHttpClient<FirebaseAuthException> httpClient;
 
   private HttpResponseInterceptor interceptor;
 
@@ -122,12 +96,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();
-
     if (requestFactory == null) {
       requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
     }
 
-    this.requestFactory = requestFactory;
+    AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory);
+    this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler);
   }
 
   @VisibleForTesting
@@ -138,66 +112,34 @@ class FirebaseUserManager {
   UserRecord getUserById(String uid) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
         "localId", ImmutableList.of(uid));
-    GetAccountInfoResponse response = post(
-        "/accounts:lookup", payload, GetAccountInfoResponse.class);
-    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
-          "No user record found for the provided user ID: " + uid);
-    }
-    return new UserRecord(response.getUsers().get(0), jsonFactory);
+    return lookupUserAccount(payload, "user ID: " + uid);
   }
 
   UserRecord getUserByEmail(String email) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
         "email", ImmutableList.of(email));
-    GetAccountInfoResponse response = post(
-        "/accounts:lookup", payload, GetAccountInfoResponse.class);
-    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
-          "No user record found for the provided email: " + email);
-    }
-    return new UserRecord(response.getUsers().get(0), jsonFactory);
+    return lookupUserAccount(payload, "email: " + email);
   }
 
   UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
         "phoneNumber", ImmutableList.of(phoneNumber));
-    GetAccountInfoResponse response = post(
-        "/accounts:lookup", payload, GetAccountInfoResponse.class);
-    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
-          "No user record found for the provided phone number: " + phoneNumber);
-    }
-    return new UserRecord(response.getUsers().get(0), jsonFactory);
+    return lookupUserAccount(payload, "phone number: " + phoneNumber);
   }
 
   String createUser(CreateRequest request) throws FirebaseAuthException {
     GenericJson response = post(
         "/accounts", request.getProperties(), GenericJson.class);
-    if (response != null) {
-      String uid = (String) response.get("localId");
-      if (!Strings.isNullOrEmpty(uid)) {
-        return uid;
-      }
-    }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create new user");
+    return (String) response.get("localId");
   }
 
   void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException {
-    GenericJson response = post(
-        "/accounts:update", request.getProperties(jsonFactory), GenericJson.class);
-    if (response == null || !request.getUid().equals(response.get("localId"))) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to update user: " + request.getUid());
-    }
+    post("/accounts:update", request.getProperties(jsonFactory), GenericJson.class);
   }
 
   void deleteUser(String uid) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of("localId", uid);
-    GenericJson response = post(
-        "/accounts:delete", payload, GenericJson.class);
-    if (response == null || !response.containsKey("kind")) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid);
-    }
+    post("/accounts:delete", payload, GenericJson.class);
   }
 
   DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException {
@@ -210,21 +152,14 @@ class FirebaseUserManager {
 
     GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet");
     url.putAll(builder.build());
-    DownloadAccountResponse response = sendRequest(
-            "GET", url, null, DownloadAccountResponse.class);
-    if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users.");
-    }
-    return response;
+    return sendRequest(
+        HttpMethods.GET, url.toString(), null, DownloadAccountResponse.class);
   }
 
   UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException {
     checkNotNull(request);
     UploadAccountResponse response = post(
-            "/accounts:batchCreate", request, UploadAccountResponse.class);
-    if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users.");
-    }
+        "/accounts:batchCreate", request, UploadAccountResponse.class);
     return new UserImportResult(request.getUsersCount(), response);
   }
 
@@ -233,13 +168,7 @@ class FirebaseUserManager {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
         "idToken", idToken, "validDuration", options.getExpiresInSeconds());
     GenericJson response = post(":createSessionCookie", payload, GenericJson.class);
-    if (response != null) {
-      String cookie = (String) response.get("sessionCookie");
-      if (!Strings.isNullOrEmpty(cookie)) {
-        return cookie;
-      }
-    }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie");
+    return (String) response.get("sessionCookie");
   }
 
   String getEmailActionLink(EmailLinkType type, String email,
@@ -251,71 +180,49 @@ class FirebaseUserManager {
     if (settings != null) {
       payload.putAll(settings.getProperties());
     }
+
     GenericJson response = post("/accounts:sendOobCode", payload.build(), GenericJson.class);
-    if (response != null) {
-      String link = (String) response.get("oobLink");
-      if (!Strings.isNullOrEmpty(link)) {
-        return link;
-      }
+    return (String) response.get("oobLink");
+  }
+
+  private UserRecord lookupUserAccount(
+      Map<String, Object> payload, String identifier) throws FirebaseAuthException {
+    IncomingHttpResponse response = sendRequest(
+        HttpMethods.POST, baseUrl + "/accounts:lookup", payload);
+    GetAccountInfoResponse parsed = httpClient.parse(response, GetAccountInfoResponse.class);
+    if (parsed.getUsers() == null || parsed.getUsers().isEmpty()) {
+      throw new FirebaseAuthException(ErrorCode.NOT_FOUND,
+          "No user record found for the provided " + identifier,
+          null,
+          response,
+          AuthErrorCode.USER_NOT_FOUND);
     }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link");
+
+    return new UserRecord(parsed.getUsers().get(0), jsonFactory);
   }
 
   private <T> T post(String path, Object content, Class<T> clazz) throws FirebaseAuthException {
     checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
     checkNotNull(content, "content must not be null for POST requests");
-    GenericUrl url = new GenericUrl(baseUrl + path);
-    return sendRequest("POST", url, content, clazz);
+    return sendRequest(HttpMethods.POST, baseUrl + path, content, clazz);
   }
 
   private <T> T sendRequest(
-          String method, GenericUrl url,
-          @Nullable Object content, Class<T> clazz) throws FirebaseAuthException {
-
-    checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty");
-    checkNotNull(url, "url must not be null");
-    checkNotNull(clazz, "response class must not be null");
-    HttpResponse response = null;
-    try {
-      HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
-      HttpRequest request = requestFactory.buildRequest(method, url, httpContent);
-      request.setParser(new JsonObjectParser(jsonFactory));
-      request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion);
-      request.setResponseInterceptor(interceptor);
-      response = request.execute();
-      return response.parseAs(clazz);
-    } catch (HttpResponseException e) {
-      // Server responded with an HTTP error
-      handleHttpError(e);
-      return null;
-    } catch (IOException e) {
-      // All other IO errors (Connection refused, reset, parse error etc.)
-      throw new FirebaseAuthException(
-          INTERNAL_ERROR, "Error while calling user management backend service", e);
-    } finally {
-      if (response != null) {
-        try {
-          response.disconnect();
-        } catch (IOException ignored) {
-          // Ignored
-        }
-      }
-    }
+      String method, String url, @Nullable Object content, Class<T> clazz)
+      throws FirebaseAuthException {
+
+    IncomingHttpResponse response = sendRequest(method, url, content);
+    return httpClient.parse(response, clazz);
   }
 
-  private void handleHttpError(HttpResponseException e) throws FirebaseAuthException {
-    try {
-      HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class);
-      String code = ERROR_CODES.get(response.getErrorCode());
-      if (code != null) {
-        throw new FirebaseAuthException(code, "User management service responded with an error", e);
-      }
-    } catch (IOException ignored) {
-      // Ignored
-    }
-    String msg = String.format(
-        "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent());
-    throw new FirebaseAuthException(INTERNAL_ERROR, msg, e);
+  private IncomingHttpResponse sendRequest(
+      String method, String url, @Nullable Object content) throws FirebaseAuthException {
+
+    HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
+    HttpRequestInfo requestInfo = HttpRequestInfo.buildRequest(method, url, httpContent)
+        .addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION)
+        .setResponseInterceptor(interceptor);
+    return httpClient.send(requestInfo);
   }
 
   static class UserImportRequest extends GenericJson {
diff --git a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java b/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java
deleted file mode 100644
index d4be4b4..0000000
--- a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.auth.internal;
-
-import com.google.api.client.util.Key;
-import com.google.common.base.Strings;
-
-/**
- * JSON data binding for JSON error messages sent by Google identity toolkit service.
- */
-public class HttpErrorResponse {
-
-  @Key("error")
-  private Error error;
-
-  public String getErrorCode() {
-    if (error != null) {
-      if (!Strings.isNullOrEmpty(error.getCode())) {
-        return error.getCode();
-      }
-    }
-    return "unknown";
-  }
-
-  public static class Error {
-
-    @Key("message")
-    private String code;
-
-    public String getCode() {
-      return code;
-    }
-  }
-
-}
diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
index 7f9f859..e7d9e73 100644
--- a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
+++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
@@ -106,7 +106,8 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
     }
   }
 
-  private <V> V parse(IncomingHttpResponse response, Class<V> responseType) throws T {
+  public <V> V parse(IncomingHttpResponse response, Class<V> responseType) throws T {
+    checkNotNull(responseType, "responseType must not be null");
     try {
       JsonParser parser = jsonFactory.createJsonParser(response.getContent());
       return parser.parseAndClose(responseType);
diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
index 8a29a8c..683ddcf 100644
--- a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
+++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java
@@ -16,12 +16,15 @@
 
 package com.google.firebase.internal;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.api.client.http.GenericUrl;
 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.common.base.Strings;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
@@ -39,6 +42,8 @@ public final class HttpRequestInfo {
   private HttpResponseInterceptor interceptor;
 
   private HttpRequestInfo(String method, String url, HttpContent content) {
+    checkArgument(!Strings.isNullOrEmpty(method), "method must not be null");
+    checkArgument(!Strings.isNullOrEmpty(url), "url must not be null");
     this.method = method;
     this.url = new GenericUrl(url);
     this.content = content;
@@ -60,7 +65,7 @@ public final class HttpRequestInfo {
   }
 
   public static HttpRequestInfo buildGetRequest(String url) {
-    return new HttpRequestInfo(HttpMethods.GET, url, null);
+    return buildRequest(HttpMethods.GET, url, null);
   }
 
   public static HttpRequestInfo buildDeleteRequest(String url) {
@@ -68,7 +73,11 @@ public final class HttpRequestInfo {
   }
 
   public static HttpRequestInfo buildPostRequest(String url, HttpContent content) {
-    return new HttpRequestInfo(HttpMethods.POST, url, content);
+    return buildRequest(HttpMethods.POST, url, content);
+  }
+
+  public static HttpRequestInfo buildRequest(String method, String url, HttpContent content) {
+    return new HttpRequestInfo(method, url, content);
   }
 
   HttpRequest newHttpRequest(HttpRequestFactory factory) throws IOException {


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index 6a8361d..804739e 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.BaseEncoding;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
@@ -96,8 +97,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(
+          "No user record found for the provided user ID: non.existing",
+          authException.getMessage());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -108,8 +115,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing email");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(
+          "No user record found for the provided email: non.existing@definitely.non.existing",
+          authException.getMessage());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -120,8 +133,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(
+          "No user record found for the given identifier (USER_NOT_FOUND).",
+          authException.getMessage());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertNotNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -132,8 +151,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(
+          "No user record found for the given identifier (USER_NOT_FOUND).",
+          authException.getMessage());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertNotNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -243,8 +268,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for deleted user");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(
+          "No user record found for the provided user ID: " + userRecord.getUid(),
+          authException.getMessage());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -409,7 +440,7 @@ public class FirebaseAuthIT {
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR,
-                   ((FirebaseAuthException) e.getCause()).getErrorCode());
+                   ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode());
     }
     idToken = signInWithCustomToken(customToken);
     decoded = auth.verifyIdTokenAsync(idToken, true).get();
@@ -443,7 +474,7 @@ public class FirebaseAuthIT {
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
+          ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode());
     }
 
     idToken = signInWithCustomToken(customToken);
@@ -692,7 +723,14 @@ public class FirebaseAuthIT {
       fail("No error thrown for creating user with existing ID");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode());
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(ErrorCode.ALREADY_EXISTS, authException.getErrorCodeNew());
+      assertEquals(
+          "The user with the provided uid already exists (DUPLICATE_LOCAL_ID).",
+          authException.getMessage());
+      assertNotNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.UID_ALREADY_EXISTS, authException.getAuthErrorCode());
     }
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index 1bc0517..3bc8ab2 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -32,7 +32,6 @@ import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
-import com.google.firebase.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
 import java.lang.reflect.InvocationTargetException;
@@ -235,7 +234,7 @@ public class FirebaseAuthTest {
       auth.verifyIdToken("idtoken");
       fail("No error thrown for invalid token");
     } catch (FirebaseAuthException authException) {
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
@@ -264,7 +263,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
@@ -281,7 +280,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
@@ -366,7 +365,7 @@ public class FirebaseAuthTest {
       auth.verifySessionCookie("idtoken");
       fail("No error thrown for invalid token");
     } catch (FirebaseAuthException authException) {
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
@@ -395,7 +394,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
@@ -412,7 +411,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
       assertEquals("Test error message", authException.getMessage());
       assertEquals("idtoken", tokenVerifier.getLastTokenString());
     }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index de0b7fa..29f1f17 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -37,6 +37,7 @@ import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
@@ -54,7 +55,6 @@ import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import org.junit.After;
@@ -117,7 +117,12 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertEquals(
+          "No user record found for the provided user ID: testuser", authException.getMessage());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -140,7 +145,13 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertEquals(
+          "No user record found for the provided email: testuser@example.com",
+          authException.getMessage());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -163,7 +174,13 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+      assertEquals(
+          "No user record found for the provided phone number: +1234567890",
+          authException.getMessage());
+      assertNull(authException.getCause());
+      assertNotNull(authException.getHttpResponse());
+      assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
     }
   }
 
@@ -182,7 +199,7 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
 
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals(999, url.getFirst("maxResults"));
+    assertEquals("999", url.getFirst("maxResults"));
     assertNull(url.getFirst("nextPageToken"));
   }
 
@@ -201,7 +218,7 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
 
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals(999, url.getFirst("maxResults"));
+    assertEquals("999", url.getFirst("maxResults"));
     assertEquals("token", url.getFirst("nextPageToken"));
   }
 
@@ -552,9 +569,15 @@ public class FirebaseUserManagerTest {
 
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
     FirebaseAuth auth = getRetryDisabledAuth(response);
+    Map<Integer, ErrorCode> codes = ImmutableMap.of(
+        302, ErrorCode.UNKNOWN,
+        400, ErrorCode.INVALID_ARGUMENT,
+        401, ErrorCode.UNAUTHENTICATED,
+        404, ErrorCode.NOT_FOUND,
+        500, ErrorCode.INTERNAL);
 
     // Test for common HTTP error codes
-    for (int code : ImmutableList.of(302, 400, 401, 404, 500)) {
+    for (int code : codes.keySet()) {
       for (UserManagerOp operation : operations) {
         // Need to reset these every iteration
         response.setContent("{}");
@@ -565,15 +588,17 @@ public class FirebaseUserManagerTest {
         } catch (ExecutionException e) {
           assertTrue(e.getCause() instanceof FirebaseAuthException);
           FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-          String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code);
+          assertEquals(codes.get(code), authException.getErrorCodeNew());
+          String msg = String.format("Unexpected HTTP response with status: %d\n{}", code);
           assertEquals(msg, authException.getMessage());
           assertTrue(authException.getCause() instanceof HttpResponseException);
-          assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+          assertNotNull(authException.getHttpResponse());
+          assertNull(authException.getAuthErrorCode());
         }
       }
     }
 
-    // Test error payload parsing
+    // Test error payload with code
     for (UserManagerOp operation : operations) {
       response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}");
       response.setStatusCode(500);
@@ -583,9 +608,33 @@ public class FirebaseUserManagerTest {
       }  catch (ExecutionException e) {
         assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException);
         FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-        assertEquals("User management service responded with an error", authException.getMessage());
+        assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+        assertEquals(
+            "No user record found for the given identifier (USER_NOT_FOUND).",
+            authException.getMessage());
+        assertTrue(authException.getCause() instanceof HttpResponseException);
+        assertNotNull(authException.getHttpResponse());
+        assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
+      }
+    }
+
+    // Test error payload with code and details
+    for (UserManagerOp operation : operations) {
+      response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND: Extra details\"}}");
+      response.setStatusCode(500);
+      try {
+        operation.call(auth);
+        fail("No error thrown for HTTP error");
+      }  catch (ExecutionException e) {
+        assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException);
+        FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+        assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCodeNew());
+        assertEquals(
+            "No user record found for the given identifier (USER_NOT_FOUND): Extra details",
+            authException.getMessage());
         assertTrue(authException.getCause() instanceof HttpResponseException);
-        assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+        assertNotNull(authException.getHttpResponse());
+        assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode());
       }
     }
   }
@@ -599,8 +648,12 @@ public class FirebaseUserManagerTest {
     }  catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(ErrorCode.UNKNOWN, authException.getErrorCodeNew());
+      assertTrue(
+          authException.getMessage().startsWith("Error while parsing HTTP response: "));
       assertTrue(authException.getCause() instanceof IOException);
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+      assertNotNull(authException.getHttpResponse());
+      assertNull(authException.getAuthErrorCode());
     }
   }
 
@@ -616,10 +669,12 @@ public class FirebaseUserManagerTest {
     }  catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertTrue(authException.getCause() instanceof HttpResponseException);
-      assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}",
+      assertEquals(ErrorCode.INTERNAL, authException.getErrorCodeNew());
+      assertEquals("Unexpected HTTP response with status: 500\n{\"not\" json}",
           authException.getMessage());
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+      assertTrue(authException.getCause() instanceof HttpResponseException);
+      assertNotNull(authException.getHttpResponse());
+      assertNull(authException.getAuthErrorCode());
     }
   }
 
@@ -1169,8 +1224,33 @@ public class FirebaseUserManagerTest {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
       fail("No exception thrown for HTTP error");
     } catch (FirebaseAuthException e) {
-      assertEquals("unauthorized-continue-uri", e.getErrorCode());
+      assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
+      assertEquals(
+          "The domain of the continue URL is not whitelisted (UNAUTHORIZED_DOMAIN).",
+          e.getMessage());
+      assertEquals(AuthErrorCode.UNAUTHORIZED_CONTINUE_URL, e.getAuthErrorCode());
+      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+    }
+  }
+
+  @Test
+  public void testHttpErrorWithUnknownCode() {
+    String content = "{\"error\": {\"message\": \"SOMETHING_NEW\"}}";
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent(content)
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
+    FirebaseUserManager userManager = auth.getUserManager();
+    try {
+      userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
+      fail("No exception thrown for HTTP error");
+    } catch (FirebaseAuthException e) {
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew());
+      assertEquals("Unexpected HTTP response with status: 500\n" + content, e.getMessage());
+      assertNull(e.getAuthErrorCode());
       assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
     }
   }
 
@@ -1185,8 +1265,11 @@ public class FirebaseUserManagerTest {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
       fail("No exception thrown for HTTP error");
     } catch (FirebaseAuthException e) {
-      assertEquals("internal-error", e.getErrorCode());
+      assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew());
+      assertEquals("Unexpected HTTP response with status: 500\n{}", e.getMessage());
       assertTrue(e.getCause() instanceof HttpResponseException);
+      assertNotNull(e.getHttpResponse());
+      assertNull(e.getAuthErrorCode());
     }
   }
 
diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
index fec3f5d..24b90ad 100644
--- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
+++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
@@ -254,7 +254,7 @@ public class FirebaseAuthSnippets {
       // Token is valid and not revoked.
       String uid = decodedToken.getUid();
     } catch (FirebaseAuthException e) {
-      if (e.getErrorCode().equals("id-token-revoked")) {
+      if (e.getDeprecatedErrorCode().equals("id-token-revoked")) {
         // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
       } else {
         // Token is invalid.