Repository: firebase/firebase-admin-java
Pull request #357 (2020-01-30 19:31:57)

Error handling revamp for the FirebaseMessaging API
* Removed `ErrorParams` helper type, and used `FirebaseException` itself for passing error information around.
* Updated `AbstractHttpErrorHandler` to provide reasonable default implementations of IO error handling and parse error handling.


* Introduced `MessagingErrorCode` enum type.
* Updated FCM and IID client APIs to use the `ErrorHandlingHttpClient`.



Patch:
diff --git a/src/main/java/com/google/firebase/FirebaseException.java b/src/main/java/com/google/firebase/FirebaseException.java
index ca1526e..e9f532b 100644
--- a/src/main/java/com/google/firebase/FirebaseException.java
+++ b/src/main/java/com/google/firebase/FirebaseException.java
@@ -38,20 +38,27 @@ public class FirebaseException extends Exception {
 
   @Deprecated
   public FirebaseException(@NonNull String detailMessage, Throwable cause) {
-    this(ErrorCode.UNKNOWN, detailMessage, null, cause);
+    this(ErrorCode.UNKNOWN, detailMessage, cause, null);
   }
 
   public FirebaseException(
       @NonNull ErrorCode errorCode,
       @NonNull String message,
-      @Nullable IncomingHttpResponse httpResponse,
-      @Nullable Throwable cause) {
+      @Nullable Throwable cause,
+      @Nullable IncomingHttpResponse httpResponse) {
     super(message, cause);
     checkArgument(!Strings.isNullOrEmpty(message), "Message must not be null or empty");
     this.errorCode = checkNotNull(errorCode, "ErrorCode must not be null");
     this.httpResponse = httpResponse;
   }
 
+  public FirebaseException(
+      @NonNull ErrorCode errorCode,
+      @NonNull String message,
+      @Nullable Throwable cause) {
+    this(errorCode, message, cause, null);
+  }
+
   /**
    * Returns the platform-wide error code associated with this exception.
    *
diff --git a/src/main/java/com/google/firebase/IncomingHttpResponse.java b/src/main/java/com/google/firebase/IncomingHttpResponse.java
index ce205ad..1e84b86 100644
--- a/src/main/java/com/google/firebase/IncomingHttpResponse.java
+++ b/src/main/java/com/google/firebase/IncomingHttpResponse.java
@@ -61,7 +61,13 @@ public final class IncomingHttpResponse {
     this(e, new OutgoingHttpRequest(request));
   }
 
-  IncomingHttpResponse(HttpResponseException e, OutgoingHttpRequest request) {
+  /**
+   * Creates an IncomingHttpResponse from an HTTP error response.
+   *
+   * @param e HttpResponseException representing the HTTP error response.
+   * @param request The OutgoingHttpRequest that resulted in the error.
+   */
+  public IncomingHttpResponse(HttpResponseException e, OutgoingHttpRequest request) {
     checkNotNull(e, "exception must not be null");
     this.statusCode = e.getStatusCode();
     this.content = e.getContent();
diff --git a/src/main/java/com/google/firebase/OutgoingHttpRequest.java b/src/main/java/com/google/firebase/OutgoingHttpRequest.java
index 09a64b0..75e0d91 100644
--- a/src/main/java/com/google/firebase/OutgoingHttpRequest.java
+++ b/src/main/java/com/google/firebase/OutgoingHttpRequest.java
@@ -36,7 +36,13 @@ public final class OutgoingHttpRequest {
   private final HttpContent content;
   private final Map<String, Object> headers;
 
-  OutgoingHttpRequest(String method, String url) {
+  /**
+   * Creates an OutgoingHttpRequest from the HTTP method and URL.
+   *
+   * @param method HTTP method name.
+   * @param url Target HTTP URL of the request.
+   */
+  public OutgoingHttpRequest(String method, String url) {
     checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty");
     checkArgument(!Strings.isNullOrEmpty(url), "url must not be empty");
     this.method = method;
diff --git a/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java
index 7af15ff..4d1f6b2 100644
--- a/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java
+++ b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java
@@ -16,20 +16,24 @@
 
 package com.google.firebase.internal;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpStatusCodes;
-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.IncomingHttpResponse;
+import java.io.IOException;
+import java.net.NoRouteToHostException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * An abstract HttpErrorHandler implementation that maps HTTP status codes to Firebase error codes.
+ * Also provides reasonable default implementations to other error handler methods in the
+ * HttpErrorHandler interface.
  */
 public abstract class AbstractHttpErrorHandler<T extends FirebaseException>
     implements HttpErrorHandler<T> {
@@ -49,11 +53,33 @@ public abstract class AbstractHttpErrorHandler<T extends FirebaseException>
   @Override
   public final T handleHttpResponseException(
       HttpResponseException e, IncomingHttpResponse response) {
-    ErrorParams params = getErrorParams(e, response);
-    return this.createException(params);
+    FirebaseException base = this.httpResponseErrorToBaseException(e, response);
+    return this.createException(base);
   }
 
-  protected ErrorParams getErrorParams(HttpResponseException e, IncomingHttpResponse response) {
+  @Override
+  public final T handleIOException(IOException e) {
+    FirebaseException base = this.ioErrorToBaseException(e);
+    return this.createException(base);
+  }
+
+  @Override
+  public final T handleParseException(IOException e, IncomingHttpResponse response) {
+    FirebaseException base = this.parseErrorToBaseException(e, response);
+    return this.createException(base);
+  }
+
+  /**
+   * Creates a FirebaseException from the given HTTP response error. Error code is determined from
+   * the HTTP status code of the response. Error message includes both the status code and full
+   * response payload to aid in debugging.
+   *
+   * @param e HTTP response exception.
+   * @param response Incoming HTTP response.
+   * @return A FirebaseException instance.
+   */
+  protected FirebaseException httpResponseErrorToBaseException(
+      HttpResponseException e, IncomingHttpResponse response) {
     ErrorCode code = HTTP_ERROR_CODES.get(e.getStatusCode());
     if (code == null) {
       code = ErrorCode.UNKNOWN;
@@ -61,47 +87,68 @@ public abstract class AbstractHttpErrorHandler<T extends FirebaseException>
 
     String message = String.format("Unexpected HTTP response with status: %d\n%s",
         e.getStatusCode(), e.getContent());
-    return new ErrorParams(code, message, e, response);
+    return new FirebaseException(code, message, e, response);
   }
 
-  protected abstract T createException(ErrorParams params);
-
-  public static final class ErrorParams {
-
-    private final ErrorCode errorCode;
-    private final String message;
-    private final HttpResponseException exception;
-    private final IncomingHttpResponse response;
-
-    public ErrorParams(
-        ErrorCode errorCode,
-        String message,
-        @Nullable HttpResponseException e,
-        @Nullable IncomingHttpResponse response) {
-
-      this.errorCode = checkNotNull(errorCode, "errorCode must not be null");
-      checkArgument(!Strings.isNullOrEmpty(message), "message must not be null or empty");
-      this.message = message;
-      this.exception = e;
-      this.response = response;
+  /**
+   * Creates a FirebaseException from the given IOException. If IOException resulted from a socket
+   * timeout, sets the error code DEADLINE_EXCEEDED. If the IOException resulted from a network
+   * outage or other connectivity issue, sets the error code to UNAVAILABLE. In all other cases sets
+   * the error code to UNKNOWN.
+   *
+   * @param e IOException to create the new exception from.
+   * @return A FirebaseException instance.
+   */
+  protected FirebaseException ioErrorToBaseException(IOException e) {
+    ErrorCode code = ErrorCode.UNKNOWN;
+    String message = "Unknown error while making a remote service call" ;
+    if (isInstance(e, SocketTimeoutException.class)) {
+      code = ErrorCode.DEADLINE_EXCEEDED;
+      message = "Timed out while making an API call";
     }
 
-    public ErrorCode getErrorCode() {
-      return errorCode;
+    if (isInstance(e, UnknownHostException.class) || isInstance(e, NoRouteToHostException.class)) {
+      code = ErrorCode.UNAVAILABLE;
+      message = "Failed to establish a connection";
     }
 
-    public String getMessage() {
-      return message;
-    }
+    return new FirebaseException(code, message + ": " + e.getMessage(), e);
+  }
 
-    @Nullable
-    public HttpResponseException getException() {
-      return exception;
-    }
+  protected FirebaseException parseErrorToBaseException(
+      IOException e, IncomingHttpResponse response) {
+    return new FirebaseException(
+        ErrorCode.UNKNOWN, "Error while parsing HTTP response: " + e.getMessage(), e, response);
+  }
 
-    @Nullable
-    public IncomingHttpResponse getResponse() {
-      return response;
+  /**
+   * Converts the given base FirebaseException to a more specific exception type. The base exception
+   * is guaranteed to have an error code, a message and a cause. But the HTTP response is only set
+   * if the exception occurred after receiving a response from a remote server.
+   *
+   * @param base A FirebaseException.
+   * @return A more specific exception created from the base.
+   */
+  protected abstract T createException(FirebaseException base);
+
+  /**
+   * Checks if the given exception stack t contains an instance of type.
+   */
+  private <U> boolean isInstance(IOException t, Class<U> type) {
+    Throwable current = t;
+    Set<Throwable> chain = new HashSet<>();
+    while (current != null) {
+      if (!chain.add(current)) {
+        break;
+      }
+
+      if (type.isInstance(current)) {
+        return true;
+      }
+
+      current = current.getCause();
     }
+
+    return false;
   }
 }
diff --git a/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java
index 8fb66d7..0a0ff1f 100644
--- a/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java
+++ b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java
@@ -41,12 +41,12 @@ public abstract class AbstractPlatformErrorHandler<T extends FirebaseException>
   }
 
   @Override
-  protected final ErrorParams getErrorParams(
+  protected final FirebaseException httpResponseErrorToBaseException(
       HttpResponseException e, IncomingHttpResponse response) {
-    ErrorParams defaults = super.getErrorParams(e, response);
+    FirebaseException base = super.httpResponseErrorToBaseException(e, response);
     PlatformErrorResponse parsedError = this.parseErrorResponse(e.getContent());
 
-    ErrorCode code = defaults.getErrorCode();
+    ErrorCode code = base.getErrorCodeNew();
     String status = parsedError.getStatus();
     if (!Strings.isNullOrEmpty(status)) {
       code = Enum.valueOf(ErrorCode.class, parsedError.getStatus());
@@ -54,10 +54,10 @@ public abstract class AbstractPlatformErrorHandler<T extends FirebaseException>
 
     String message = parsedError.getMessage();
     if (Strings.isNullOrEmpty(message)) {
-      message = defaults.getMessage();
+      message = base.getMessage();
     }
 
-    return new ErrorParams(code, message, e, response);
+    return new FirebaseException(code, message, e, response);
   }
 
   private PlatformErrorResponse parseErrorResponse(String content) {
diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java
index f8ae0e1..4ccfa93 100644
--- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java
+++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java
@@ -35,6 +35,8 @@ public class ApiClientUtils {
       .setMaxIntervalMillis(60 * 1000)
       .build();
 
+  private ApiClientUtils() { }
+
   /**
    * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
    * automatic retries.
diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
index ae7c8a6..7f9f859 100644
--- a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
+++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java
@@ -26,10 +26,10 @@ import com.google.api.client.json.JsonFactory;
 import com.google.api.client.json.JsonObjectParser;
 import com.google.api.client.json.JsonParser;
 import com.google.common.io.CharStreams;
-import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.IncomingHttpResponse;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 
 /**
@@ -42,14 +42,6 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
   private final JsonFactory jsonFactory;
   private final HttpErrorHandler<T> errorHandler;
 
-  public ErrorHandlingHttpClient(
-      FirebaseApp app, HttpErrorHandler<T> errorHandler, RetryConfig retryConfig) {
-    this(
-        ApiClientUtils.newAuthorizedRequestFactory(app, retryConfig),
-        app.getOptions().getJsonFactory(),
-        errorHandler);
-  }
-
   public ErrorHandlingHttpClient(
       HttpRequestFactory requestFactory,
       JsonFactory jsonFactory,
@@ -74,7 +66,22 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
     return parse(response, responseType);
   }
 
-  private IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T {
+  /**
+   * Sends the given HTTP request to the target endpoint, and parses the response while handling
+   * any errors that may occur along the way. This method can be used when the response should
+   * be parsed into an instance of a private or protected class, which cannot be instantiated
+   * outside the call-site.
+   *
+   * @param requestInfo Outgoing request configuration.
+   * @param destination Object to parse the response into.
+   * @throws T If any error occurs while making the request.
+   */
+  public void sendAndParse(HttpRequestInfo requestInfo, Object destination) throws T {
+    IncomingHttpResponse response = send(requestInfo);
+    parse(response, destination);
+  }
+
+  public IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T {
     HttpRequest request = createHttpRequest(requestInfo);
 
     HttpResponse response = null;
@@ -82,8 +89,13 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
       response = request.execute();
       // Read and buffer the content. Otherwise if a parse error occurs later,
       // we lose the content stream.
-      String content = CharStreams.toString(
-          new InputStreamReader(response.getContent(), response.getContentCharset()));
+      String content = null;
+      InputStream stream = response.getContent();
+      if (stream != null) {
+        // Stream is null when the response body is empty (e.g. 204 No Content responses).
+        content = CharStreams.toString(new InputStreamReader(stream, response.getContentCharset()));
+      }
+
       return new IncomingHttpResponse(response, content);
     } catch (HttpResponseException e) {
       throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request));
@@ -103,6 +115,15 @@ public final class ErrorHandlingHttpClient<T extends FirebaseException> {
     }
   }
 
+  private void parse(IncomingHttpResponse response, Object destination) throws T {
+    try {
+      JsonParser parser = jsonFactory.createJsonParser(response.getContent());
+      parser.parse(destination);
+    } catch (IOException e) {
+      throw errorHandler.handleParseException(e, response);
+    }
+  }
+
   private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T {
     try {
       return requestInfo.newHttpRequest(requestFactory)
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
index ee25957..e1b4f79 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
@@ -41,10 +41,6 @@ import java.util.List;
  */
 public class FirebaseMessaging {
 
-  static final String INTERNAL_ERROR = "internal-error";
-
-  static final String UNKNOWN_ERROR = "unknown-error";
-
   private final FirebaseApp app;
   private final Supplier<? extends FirebaseMessagingClient> messagingClient;
   private final Supplier<? extends InstanceIdClient> instanceIdClient;
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
index c2659a2..7247153 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java
@@ -23,24 +23,29 @@ import com.google.api.client.googleapis.batch.BatchCallback;
 import com.google.api.client.googleapis.batch.BatchRequest;
 import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpHeaders;
+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.HttpRequestInitializer;
-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.JsonFactory;
 import com.google.api.client.json.JsonObjectParser;
-import com.google.api.client.json.JsonParser;
 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.FirebaseException;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.IncomingHttpResponse;
+import com.google.firebase.OutgoingHttpRequest;
+import com.google.firebase.internal.AbstractPlatformErrorHandler;
 import com.google.firebase.internal.ApiClientUtils;
-import com.google.firebase.internal.Nullable;
+import com.google.firebase.internal.ErrorHandlingHttpClient;
+import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.messaging.internal.MessagingServiceErrorResponse;
 import com.google.firebase.messaging.internal.MessagingServiceResponse;
@@ -61,31 +66,18 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
 
   private static final String CLIENT_VERSION_HEADER = "X-Firebase-Client";
 
-  private static final Map<String, String> FCM_ERROR_CODES =
-      ImmutableMap.<String, String>builder()
-          // FCM v1 canonical error codes
-          .put("NOT_FOUND", "registration-token-not-registered")
-          .put("PERMISSION_DENIED", "mismatched-credential")
-          .put("RESOURCE_EXHAUSTED", "message-rate-exceeded")
-          .put("UNAUTHENTICATED", "third-party-auth-error")
-
-          // FCM v1 new error codes
-          .put("APNS_AUTH_ERROR", "third-party-auth-error")
-          .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR)
-          .put("INVALID_ARGUMENT", "invalid-argument")
-          .put("QUOTA_EXCEEDED", "message-rate-exceeded")
-          .put("SENDER_ID_MISMATCH", "mismatched-credential")
-          .put("THIRD_PARTY_AUTH_ERROR", "third-party-auth-error")
-          .put("UNAVAILABLE", "server-unavailable")
-          .put("UNREGISTERED", "registration-token-not-registered")
-          .build();
+  private static final Map<String, String> COMMON_HEADERS =
+      ImmutableMap.of(
+          API_FORMAT_VERSION_HEADER, "2",
+          CLIENT_VERSION_HEADER, "fire-admin-java/" + SdkUtils.getVersion());
 
   private final String fcmSendUrl;
   private final HttpRequestFactory requestFactory;
   private final HttpRequestFactory childRequestFactory;
   private final JsonFactory jsonFactory;
   private final HttpResponseInterceptor responseInterceptor;
-  private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion();
+  private final MessagingErrorHandler errorHandler;
+  private final ErrorHandlingHttpClient<FirebaseMessagingException> httpClient;
 
   private FirebaseMessagingClientImpl(Builder builder) {
     checkArgument(!Strings.isNullOrEmpty(builder.projectId));
@@ -94,6 +86,9 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     this.childRequestFactory = checkNotNull(builder.childRequestFactory);
     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);
   }
 
   @VisibleForTesting
@@ -116,58 +111,42 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     return jsonFactory;
   }
 
-  @VisibleForTesting
-  String getClientVersion() {
-    return clientVersion;
-  }
-
   public String send(Message message, boolean dryRun) throws FirebaseMessagingException {
-    try {
-      return sendSingleRequest(message, dryRun);
-    } catch (HttpResponseException e) {
-      throw createExceptionFromResponse(e);
-    } catch (IOException e) {
-      throw new FirebaseMessagingException(
-          FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e);
-    }
+    return sendSingleRequest(message, dryRun);
   }
 
   public BatchResponse sendAll(
       List<Message> messages, boolean dryRun) throws FirebaseMessagingException {
-    try {
-      return sendBatchRequest(messages, dryRun);
-    } catch (HttpResponseException e) {
-      throw createExceptionFromResponse(e);
-    } catch (IOException e) {
-      throw new FirebaseMessagingException(
-          FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e);
-    }
+    return sendBatchRequest(messages, dryRun);
   }
 
-  private String sendSingleRequest(Message message, boolean dryRun) throws IOException {
-    HttpRequest request = requestFactory.buildPostRequest(
-        new GenericUrl(fcmSendUrl),
-        new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun)));
-    setCommonFcmHeaders(request.getHeaders());
-    request.setParser(new JsonObjectParser(jsonFactory));
-    request.setResponseInterceptor(responseInterceptor);
-    HttpResponse response = request.execute();
-    try {
-      MessagingServiceResponse parsed = new MessagingServiceResponse();
-      jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed);
-      return parsed.getMessageId();
-    } finally {
-      ApiClientUtils.disconnectQuietly(response);
-    }
+  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);
+    MessagingServiceResponse parsed = httpClient.sendAndParse(
+        request, MessagingServiceResponse.class);
+    return parsed.getMessageId();
   }
 
   private BatchResponse sendBatchRequest(
-      List<Message> messages, boolean dryRun) throws IOException {
+      List<Message> messages, boolean dryRun) throws FirebaseMessagingException {
 
     MessagingBatchCallback callback = new MessagingBatchCallback();
-    BatchRequest batch = newBatchRequest(messages, dryRun, callback);
-    batch.execute();
-    return new BatchResponse(callback.getResponses());
+    try {
+      BatchRequest batch = newBatchRequest(messages, dryRun, callback);
+      batch.execute();
+      return new BatchResponse(callback.getResponses());
+    } catch (HttpResponseException e) {
+      OutgoingHttpRequest req = new OutgoingHttpRequest(HttpMethods.POST, FCM_BATCH_URL);
+      IncomingHttpResponse resp = new IncomingHttpResponse(e, req);
+      throw errorHandler.handleHttpResponseException(e, resp);
+    } catch (IOException e) {
+      throw errorHandler.handleIOException(e);
+    }
   }
 
   private BatchRequest newBatchRequest(
@@ -186,32 +165,13 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
           sendUrl,
           new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun)));
       request.setParser(jsonParser);
-      setCommonFcmHeaders(request.getHeaders());
+      request.getHeaders().putAll(COMMON_HEADERS);
       batch.queue(
           request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback);
     }
     return batch;
   }
 
-  private void setCommonFcmHeaders(HttpHeaders headers) {
-    headers.set(API_FORMAT_VERSION_HEADER, "2");
-    headers.set(CLIENT_VERSION_HEADER, clientVersion);
-  }
-
-  private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) {
-    MessagingServiceErrorResponse response = new MessagingServiceErrorResponse();
-    if (e.getContent() != null) {
-      try {
-        JsonParser parser = jsonFactory.createJsonParser(e.getContent());
-        parser.parseAndClose(response);
-      } catch (IOException ignored) {
-        // ignored
-      }
-    }
-
-    return newException(response, e);
-  }
-
   private HttpRequestInitializer getBatchRequestInitializer() {
     return new HttpRequestInitializer() {
       @Override
@@ -283,30 +243,6 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     }
   }
 
-  private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) {
-    return newException(response, null);
-  }
-
-  private static FirebaseMessagingException newException(
-      MessagingServiceErrorResponse response, @Nullable HttpResponseException e) {
-    String code = FCM_ERROR_CODES.get(response.getErrorCode());
-    if (code == null) {
-      code = FirebaseMessaging.UNKNOWN_ERROR;
-    }
-
-    String msg = response.getErrorMessage();
-    if (Strings.isNullOrEmpty(msg)) {
-      if (e != null) {
-        msg = String.format("Unexpected HTTP response with status: %d; body: %s",
-            e.getStatusCode(), e.getContent());
-      } else {
-        msg = String.format("Unexpected HTTP response: %s", response.toString());
-      }
-    }
-
-    return new FirebaseMessagingException(code, msg, e);
-  }
-
   private static class MessagingBatchCallback
       implements BatchCallback<MessagingServiceResponse, MessagingServiceErrorResponse> {
 
@@ -319,13 +255,68 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
     }
 
     @Override
-    public void onFailure(
-        MessagingServiceErrorResponse error, HttpHeaders responseHeaders) {
-      responses.add(SendResponse.fromException(newException(error)));
+    public void onFailure(MessagingServiceErrorResponse error, HttpHeaders responseHeaders) {
+      // We only specify error codes and message for these partial failures. Recall that these
+      // exceptions are never actually thrown, but only made accessible via SendResponse.
+      FirebaseException base = createFirebaseException(error);
+      FirebaseMessagingException exception = FirebaseMessagingException.withMessagingErrorCode(
+          base, error.getMessagingErrorCode());
+      responses.add(SendResponse.fromException(exception));
     }
 
     List<SendResponse> getResponses() {
       return this.responses.build();
     }
+
+    private FirebaseException createFirebaseException(MessagingServiceErrorResponse error) {
+      String status = error.getStatus();
+      ErrorCode errorCode = Strings.isNullOrEmpty(status)
+          ? ErrorCode.UNKNOWN : Enum.valueOf(ErrorCode.class, status);
+
+      String msg = error.getErrorMessage();
+      if (Strings.isNullOrEmpty(msg)) {
+        msg = String.format("Unexpected HTTP response: %s", error.toString());
+      }
+
+      return new FirebaseException(errorCode, msg, null);
+    }
+  }
+
+  private static class MessagingErrorHandler
+      extends AbstractPlatformErrorHandler<FirebaseMessagingException> {
+
+    private MessagingErrorHandler(JsonFactory jsonFactory) {
+      super(jsonFactory);
+    }
+
+    @Override
+    protected FirebaseMessagingException createException(FirebaseException base) {
+      String response = getResponse(base);
+      MessagingServiceErrorResponse parsed = safeParse(response);
+      return FirebaseMessagingException.withMessagingErrorCode(
+          base, parsed.getMessagingErrorCode());
+    }
+
+    private String getResponse(FirebaseException base) {
+      if (base.getHttpResponse() == null) {
+        return null;
+      }
+
+      return base.getHttpResponse().getContent();
+    }
+
+    private MessagingServiceErrorResponse safeParse(String response) {
+      if (!Strings.isNullOrEmpty(response)) {
+        try {
+          return jsonFactory.createJsonParser(response)
+              .parseAndClose(MessagingServiceErrorResponse.class);
+        } 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 new MessagingServiceErrorResponse();
+    }
   }
 }
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java
index 5f57474..04f65d3 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java
@@ -16,26 +16,54 @@
 
 package com.google.firebase.messaging;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Strings;
+import com.google.common.annotations.VisibleForTesting;
+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;
+
+public final class FirebaseMessagingException extends FirebaseException {
 
-public class FirebaseMessagingException extends FirebaseException {
+  private final MessagingErrorCode errorCode;
 
-  private final String errorCode;
+  @VisibleForTesting
+  FirebaseMessagingException(@NonNull ErrorCode code, @NonNull String message) {
+    this(code, message, null, null, null);
+  }
 
-  FirebaseMessagingException(String errorCode, String message, Throwable cause) {
-    super(message, cause);
-    checkArgument(!Strings.isNullOrEmpty(errorCode));
+  private FirebaseMessagingException(
+      @NonNull ErrorCode code,
+      @NonNull String message,
+      @Nullable Throwable cause,
+      @Nullable IncomingHttpResponse response,
+      @Nullable MessagingErrorCode errorCode) {
+    super(code, message, cause, response);
     this.errorCode = errorCode;
   }
 
+  static FirebaseMessagingException withMessagingErrorCode(
+      FirebaseException base, @Nullable MessagingErrorCode errorCode) {
+    return new FirebaseMessagingException(
+        base.getErrorCodeNew(),
+        base.getMessage(),
+        base.getCause(),
+        base.getHttpResponse(),
+        errorCode);
+  }
+
+  static FirebaseMessagingException withCustomMessage(FirebaseException base, String message) {
+    return new FirebaseMessagingException(
+        base.getErrorCodeNew(),
+        message,
+        base.getCause(),
+        base.getHttpResponse(),
+        null);
+  }
 
   /** Returns an error code that may provide more information about the error. */
-  @NonNull
-  public String getErrorCode() {
+  @Nullable
+  public MessagingErrorCode getMessagingErrorCode() {
     return errorCode;
   }
 }
diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
index 15f8158..09962a4 100644
--- a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
+++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java
@@ -18,23 +18,21 @@ package com.google.firebase.messaging;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.api.client.http.GenericUrl;
-import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
-import com.google.api.client.http.HttpResponse;
-import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
 import com.google.api.client.http.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.json.JsonParser;
 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;
+import com.google.firebase.FirebaseException;
+import com.google.firebase.internal.AbstractHttpErrorHandler;
 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;
@@ -53,16 +51,7 @@ final class InstanceIdClientImpl implements InstanceIdClient {
 
   private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove";
 
-  static final Map<Integer, String> IID_ERROR_CODES =
-      ImmutableMap.<Integer, String>builder()
-          .put(400, "invalid-argument")
-          .put(401, "authentication-error")
-          .put(403, "authentication-error")
-          .put(500, FirebaseMessaging.INTERNAL_ERROR)
-          .put(503, "server-unavailable")
-          .build();
-
-  private final HttpRequestFactory requestFactory;
+  private final ErrorHandlingHttpClient<FirebaseMessagingException> requestFactory;
   private final JsonFactory jsonFactory;
   private final HttpResponseInterceptor responseInterceptor;
 
@@ -74,7 +63,8 @@ final class InstanceIdClientImpl implements InstanceIdClient {
       HttpRequestFactory requestFactory,
       JsonFactory jsonFactory,
       @Nullable HttpResponseInterceptor responseInterceptor) {
-    this.requestFactory = checkNotNull(requestFactory);
+    this.requestFactory = new ErrorHandlingHttpClient<>(
+        requestFactory, jsonFactory, new InstanceIdErrorHandler(jsonFactory));
     this.jsonFactory = checkNotNull(jsonFactory);
     this.responseInterceptor = responseInterceptor;
   }
@@ -85,11 +75,6 @@ final class InstanceIdClientImpl implements InstanceIdClient {
         app.getOptions().getJsonFactory());
   }
 
-  @VisibleForTesting
-  HttpRequestFactory getRequestFactory() {
-    return requestFactory;
-  }
-
   @VisibleForTesting
   JsonFactory getJsonFactory() {
     return jsonFactory;
@@ -97,64 +82,32 @@ final class InstanceIdClientImpl implements InstanceIdClient {
 
   public TopicManagementResponse subscribeToTopic(
       String topic, List<String> registrationTokens) throws FirebaseMessagingException {
-    try {
-      return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH);
-    } catch (HttpResponseException e) {
-      throw createExceptionFromResponse(e);
-    } catch (IOException e) {
-      throw new FirebaseMessagingException(
-          FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e);
-    }
+    return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH);
   }
 
   public TopicManagementResponse unsubscribeFromTopic(
       String topic, List<String> registrationTokens) throws FirebaseMessagingException {
-    try {
-      return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH);
-    } catch (HttpResponseException e) {
-      throw createExceptionFromResponse(e);
-    } catch (IOException e) {
-      throw new FirebaseMessagingException(
-          FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e);
-    }
+    return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH);
   }
 
   private TopicManagementResponse sendInstanceIdRequest(
-      String topic, List<String> registrationTokens, String path) throws IOException {
+      String topic,
+      List<String> registrationTokens,
+      String path) throws FirebaseMessagingException {
+
     String url = String.format("%s/%s", IID_HOST, path);
     Map<String, Object> payload = ImmutableMap.of(
         "to", getPrefixedTopic(topic),
         "registration_tokens", registrationTokens
     );
-    HttpResponse response = null;
-    try {
-      HttpRequest request = requestFactory.buildPostRequest(
-          new GenericUrl(url), new JsonHttpContent(jsonFactory, payload));
-      request.getHeaders().set("access_token_auth", "true");
-      request.setParser(new JsonObjectParser(jsonFactory));
-      request.setResponseInterceptor(responseInterceptor);
-      response = request.execute();
-
-      JsonParser parser = jsonFactory.createJsonParser(response.getContent());
-      InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse();
-      parser.parse(parsedResponse);
-      return new TopicManagementResponse(parsedResponse.results);
-    } finally {
-      ApiClientUtils.disconnectQuietly(response);
-    }
-  }
 
-  private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) {
-    InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse();
-    if (e.getContent() != null) {
-      try {
-        JsonParser parser = jsonFactory.createJsonParser(e.getContent());
-        parser.parseAndClose(response);
-      } catch (IOException ignored) {
-        // ignored
-      }
-    }
-    return newException(response, e);
+    HttpRequestInfo request =
+        HttpRequestInfo.buildPostRequest(url, new JsonHttpContent(jsonFactory, payload))
+            .addHeader("access_token_auth", "true")
+            .setResponseInterceptor(responseInterceptor);
+    InstanceIdServiceResponse response = new InstanceIdServiceResponse();
+    requestFactory.sendAndParse(request, response);
+    return new TopicManagementResponse(response.results);
   }
 
   private String getPrefixedTopic(String topic) {
@@ -165,21 +118,6 @@ final class InstanceIdClientImpl implements InstanceIdClient {
     }
   }
 
-  private static FirebaseMessagingException newException(
-      InstanceIdServiceErrorResponse response, HttpResponseException e) {
-    // Infer error code from HTTP status
-    String code = IID_ERROR_CODES.get(e.getStatusCode());
-    if (code == null) {
-      code = FirebaseMessaging.UNKNOWN_ERROR;
-    }
-    String msg = response.error;
-    if (Strings.isNullOrEmpty(msg)) {
-      msg = String.format("Unexpected HTTP response with status: %d; body: %s",
-          e.getStatusCode(), e.getContent());
-    }
-    return new FirebaseMessagingException(code, msg, e);
-  }
-
   private static class InstanceIdServiceResponse {
     @Key("results")
     private List<GenericJson> results;
@@ -189,4 +127,54 @@ final class InstanceIdClientImpl implements InstanceIdClient {
     @Key("error")
     private String error;
   }
+
+  private static class InstanceIdErrorHandler
+      extends AbstractHttpErrorHandler<FirebaseMessagingException> {
+
+    private final JsonFactory jsonFactory;
+
+    InstanceIdErrorHandler(JsonFactory jsonFactory) {
+      this.jsonFactory = jsonFactory;
+    }
+
+    @Override
+    protected FirebaseMessagingException createException(FirebaseException base) {
+      String message = getCustomMessage(base);
+      return FirebaseMessagingException.withCustomMessage(base, message);
+    }
+
+    private String getCustomMessage(FirebaseException base) {
+      String response = getResponse(base);
+      InstanceIdServiceErrorResponse parsed = safeParse(response);
+      if (!Strings.isNullOrEmpty(parsed.error)) {
+        return parsed.error;
+      }
+
+      return base.getMessage();
+    }
+
+    private String getResponse(FirebaseException base) {
+      if (base.getHttpResponse() == null) {
+        return null;
+      }
+
+      return base.getHttpResponse().getContent();
+    }
+
+    private InstanceIdServiceErrorResponse safeParse(String response) {
+      InstanceIdServiceErrorResponse parsed = new InstanceIdServiceErrorResponse();
+      if (!Strings.isNullOrEmpty(response)) {
+        // Parse the error response from the IID service.
+        // Sample response: {"error": "error message text"}
+        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;
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java
new file mode 100644
index 0000000..f76294c
--- /dev/null
+++ b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java
@@ -0,0 +1,43 @@
+package com.google.firebase.messaging;
+
+/**
+ * Error codes that can be raised by the Cloud Messaging APIs.
+ */
+public enum MessagingErrorCode {
+
+  /**
+   * APNs certificate or web push auth key was invalid or missing.
+   */
+  THIRD_PARTY_AUTH_ERROR,
+
+  /**
+   * One or more argument specified in the request was invalid.
+   */
+  INVALID_ARGUMENT,
+
+  /**
+   * Internal server error.
+   */
+  INTERNAL,
+
+  /**
+   * Sending limit exceeded for the message target.
+   */
+  QUOTA_EXCEEDED,
+
+  /**
+   * The authenticated sender ID is different from the sender ID for the registration token.
+   */
+  SENDER_ID_MISMATCH,
+
+  /**
+   * Cloud Messaging service is temporarily unavailable.
+   */
+  UNAVAILABLE,
+
+  /**
+   * App instance was unregistered from FCM. This usually means that the token used is no longer
+   * valid and a new one must be used.
+   */
+  UNREGISTERED,
+}
diff --git a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java
index d63f3af..7c21199 100644
--- a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java
+++ b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java
@@ -2,14 +2,28 @@ package com.google.firebase.messaging.internal;
 
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.util.Key;
+import com.google.common.collect.ImmutableMap;
 import com.google.firebase.internal.Nullable;
+import com.google.firebase.messaging.MessagingErrorCode;
 import java.util.List;
 import java.util.Map;
 
 /**
  * The DTO for parsing error responses from the FCM service.
  */
-public class MessagingServiceErrorResponse extends GenericJson {
+public final class MessagingServiceErrorResponse extends GenericJson {
+
+  private static final Map<String, MessagingErrorCode> MESSAGING_ERROR_CODES =
+      ImmutableMap.<String, MessagingErrorCode>builder()
+          .put("APNS_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR)
+          .put("INTERNAL", MessagingErrorCode.INTERNAL)
+          .put("INVALID_ARGUMENT", MessagingErrorCode.INVALID_ARGUMENT)
+          .put("QUOTA_EXCEEDED", MessagingErrorCode.QUOTA_EXCEEDED)
+          .put("SENDER_ID_MISMATCH", MessagingErrorCode.SENDER_ID_MISMATCH)
+          .put("THIRD_PARTY_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR)
+          .put("UNAVAILABLE", MessagingErrorCode.UNAVAILABLE)
+          .put("UNREGISTERED", MessagingErrorCode.UNREGISTERED)
+          .build();
 
   private static final String FCM_ERROR_TYPE =
       "type.googleapis.com/google.firebase.fcm.v1.FcmError";
@@ -17,23 +31,35 @@ public class MessagingServiceErrorResponse extends GenericJson {
   @Key("error")
   private Map<String, Object> error;
 
+  public String getStatus() {
+    if (error == null) {
+      return null;
+    }
+
+    return (String) error.get("status");
+  }
+
+
   @Nullable
-  public String getErrorCode() {
+  public MessagingErrorCode getMessagingErrorCode() {
     if (error == null) {
       return null;
     }
+
     Object details = error.get("details");
-    if (details != null && details instanceof List) {
+    if (details instanceof List) {
       for (Object detail : (List<?>) details) {
         if (detail instanceof Map) {
           Map<?,?> detailMap = (Map<?,?>) detail;
           if (FCM_ERROR_TYPE.equals(detailMap.get("@type"))) {
-            return (String) detailMap.get("errorCode");
+            String errorCode = (String) detailMap.get("errorCode");
+            return MESSAGING_ERROR_CODES.get(errorCode);
           }
         }
       }
     }
-    return (String) error.get("status");
+
+    return null;
   }
 
   @Nullable
@@ -41,6 +67,7 @@ public class MessagingServiceErrorResponse extends GenericJson {
     if (error != null) {
       return (String) error.get("message");
     }
+
     return null;
   }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseExceptionTest.java b/src/test/java/com/google/firebase/FirebaseExceptionTest.java
index 81e6104..aeddc85 100644
--- a/src/test/java/com/google/firebase/FirebaseExceptionTest.java
+++ b/src/test/java/com/google/firebase/FirebaseExceptionTest.java
@@ -83,8 +83,8 @@ public class FirebaseExceptionTest {
     FirebaseException exception = new FirebaseException(
         ErrorCode.INTERNAL,
         "Test error",
-        response,
-        null);
+        null,
+        response);
 
     assertEquals(ErrorCode.INTERNAL, exception.getErrorCodeNew());
     assertEquals("Test error", exception.getMessage());
@@ -99,7 +99,6 @@ public class FirebaseExceptionTest {
     FirebaseException exception = new FirebaseException(
         ErrorCode.INTERNAL,
         "Test error",
-        null,
         cause);
 
     assertEquals(ErrorCode.INTERNAL, exception.getErrorCodeNew());
diff --git a/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java
index d858a23..e7db495 100644
--- a/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java
+++ b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java
@@ -20,9 +20,11 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpStatusCodes;
 import com.google.api.client.http.LowLevelHttpRequest;
 import com.google.api.client.testing.http.MockHttpTransport;
@@ -32,6 +34,9 @@ import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.IncomingHttpResponse;
 import java.io.IOException;
+import java.net.NoRouteToHostException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
 import org.junit.Test;
 
 public class AbstractPlatformErrorHandlerTest {
@@ -45,12 +50,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCodeNew());
       assertEquals("Test error", e.getMessage());
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload);
@@ -64,12 +69,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew());
       assertEquals("Unexpected HTTP response with status: 500\nnot json", e.getMessage());
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload);
@@ -83,12 +88,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew());
       assertEquals("Test error", e.getMessage());
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload);
@@ -102,12 +107,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
       assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage());
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload);
@@ -121,12 +126,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew());
       assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage());
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload);
@@ -135,25 +140,68 @@ public class AbstractPlatformErrorHandlerTest {
   }
 
   @Test
-  public void testNetworkError() {
-    final IOException exception = new IOException("Test");
-    MockHttpTransport transport = new MockHttpTransport(){
-      @Override
-      public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
-        throw exception;
-      }
-    };
-    ErrorHandlingHttpClient<FirebaseException> client = new ErrorHandlingHttpClient<>(
-        transport.createRequestFactory(),
-        Utils.getDefaultJsonFactory(),
-        new TestPlatformErrorHandler());
+  public void testGenericIOException() {
+    IOException exception = new IOException("Test");
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(exception);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew());
-      assertEquals("IO error", e.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: Test", e.getMessage());
+      assertNull(e.getHttpResponse());
+      assertSame(exception, e.getCause());
+    }
+  }
+
+  @Test
+  public void testTimeoutError() {
+    IOException exception = new IOException("Test", new SocketTimeoutException());
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(exception);
+
+    try {
+      client.sendAndParse(TEST_REQUEST, GenericData.class);
+      fail("No exception thrown for HTTP error response");
+    } catch (MockFirebaseException e) {
+      assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCodeNew());
+      assertEquals(
+          "Timed out while making an API call: Test", e.getMessage());
+      assertNull(e.getHttpResponse());
+      assertSame(exception, e.getCause());
+    }
+  }
+
+  @Test
+  public void testNoRouteToHostError() {
+    IOException exception = new IOException("Test", new NoRouteToHostException());
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(exception);
+
+    try {
+      client.sendAndParse(TEST_REQUEST, GenericData.class);
+      fail("No exception thrown for HTTP error response");
+    } catch (MockFirebaseException e) {
+      assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCodeNew());
+      assertEquals(
+          "Failed to establish a connection: Test", e.getMessage());
+      assertNull(e.getHttpResponse());
+      assertSame(exception, e.getCause());
+    }
+  }
+
+  @Test
+  public void testUnknownHostError() {
+    IOException exception = new IOException("Test", new UnknownHostException());
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(exception);
+
+    try {
+      client.sendAndParse(TEST_REQUEST, GenericData.class);
+      fail("No exception thrown for HTTP error response");
+    } catch (MockFirebaseException e) {
+      assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCodeNew());
+      assertEquals(
+          "Failed to establish a connection: Test", e.getMessage());
       assertNull(e.getHttpResponse());
       assertSame(exception, e.getCause());
     }
@@ -164,14 +212,14 @@ public class AbstractPlatformErrorHandlerTest {
     String payload = "not json";
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew());
-      assertEquals("Parse error", e.getMessage());
+      assertTrue(e.getMessage().startsWith("Error while parsing HTTP response: "));
       assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_OK, payload);
       assertNotNull(e.getCause());
     }
@@ -183,12 +231,12 @@ public class AbstractPlatformErrorHandlerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
         .setStatusCode(512)
         .setContent(payload);
-    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+    ErrorHandlingHttpClient<MockFirebaseException> client = createHttpClient(response);
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
       fail("No exception thrown for HTTP error response");
-    } catch (FirebaseException e) {
+    } catch (MockFirebaseException e) {
       assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew());
       assertEquals("Test error", e.getMessage());
       assertHttpResponse(e, 512, payload);
@@ -196,7 +244,7 @@ public class AbstractPlatformErrorHandlerTest {
     }
   }
 
-  private ErrorHandlingHttpClient<FirebaseException> createHttpClient(
+  private ErrorHandlingHttpClient<MockFirebaseException> createHttpClient(
       MockLowLevelHttpResponse response) {
     MockHttpTransport transport = new MockHttpTransport.Builder()
         .setLowLevelHttpResponse(response)
@@ -207,35 +255,44 @@ public class AbstractPlatformErrorHandlerTest {
         new TestPlatformErrorHandler());
   }
 
+  private ErrorHandlingHttpClient<MockFirebaseException> createHttpClient(
+      final IOException exception) {
+    MockHttpTransport transport = new MockHttpTransport(){
+      @Override
+      public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+        throw exception;
+      }
+    };
+    return new ErrorHandlingHttpClient<>(
+        transport.createRequestFactory(),
+        Utils.getDefaultJsonFactory(),
+        new TestPlatformErrorHandler());
+  }
+
   private void assertHttpResponse(FirebaseException e, int statusCode, String content) {
     IncomingHttpResponse httpResponse = e.getHttpResponse();
     assertNotNull(httpResponse);
     assertEquals(statusCode, httpResponse.getStatusCode());
     assertEquals(content, httpResponse.getContent());
-    assertEquals("GET", httpResponse.getRequest().getMethod());
+    assertEquals(HttpMethods.GET, httpResponse.getRequest().getMethod());
   }
 
   private static class TestPlatformErrorHandler extends
-      AbstractPlatformErrorHandler<FirebaseException> {
+      AbstractPlatformErrorHandler<MockFirebaseException> {
 
     TestPlatformErrorHandler() {
       super(Utils.getDefaultJsonFactory());
     }
 
     @Override
-    protected FirebaseException createException(ErrorParams params) {
-      return new FirebaseException(params.getErrorCode(), params.getMessage(),
-          params.getResponse(), params.getException());
-    }
-
-    @Override
-    public FirebaseException handleIOException(IOException e) {
-      return new FirebaseException(ErrorCode.UNKNOWN, "IO error", null, e);
+    protected MockFirebaseException createException(FirebaseException base) {
+      return new MockFirebaseException(base);
     }
+  }
 
-    @Override
-    public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) {
-      return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", response, e);
+  private static class MockFirebaseException extends FirebaseException {
+    MockFirebaseException(FirebaseException base) {
+      super(base.getErrorCodeNew(), base.getMessage(), base.getCause(), base.getHttpResponse());
     }
   }
 }
diff --git a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
index 504ee5c..a59014d 100644
--- a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
+++ b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.fail;
 import com.google.api.client.googleapis.util.Utils;
 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;
@@ -91,6 +92,18 @@ public class ErrorHandlingHttpClientTest {
     assertEquals("bar", body.get("foo"));
   }
 
+  @Test
+  public void testSuccessfulRequestWithoutContent() throws FirebaseException {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setZeroContent();
+    ErrorHandlingHttpClient<FirebaseException> client = createHttpClient(response);
+
+    IncomingHttpResponse responseInfo = client.send(TEST_REQUEST);
+
+    assertEquals(HttpStatusCodes.STATUS_CODE_OK, responseInfo.getStatusCode());
+    assertNull(responseInfo.getContent());
+  }
+
   @Test
   public void testSuccessfulRequestWithHeadersAndBody() throws FirebaseException, IOException {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
@@ -200,8 +213,10 @@ public class ErrorHandlingHttpClientTest {
         .setRetryStatusCodes(ImmutableList.of(503))
         .setSleeper(new MockSleeper())
         .build();
+    HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(
+        app, retryConfig);
     ErrorHandlingHttpClient<FirebaseException> client = new ErrorHandlingHttpClient<>(
-        app, new TestHttpErrorHandler(), retryConfig);
+        requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler());
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
@@ -234,8 +249,9 @@ public class ErrorHandlingHttpClientTest {
         })
         .setHttpTransport(transport)
         .build());
+    HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
     ErrorHandlingHttpClient<FirebaseException> client = new ErrorHandlingHttpClient<>(
-        app, new TestHttpErrorHandler(), null);
+        requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler());
 
     try {
       client.sendAndParse(TEST_REQUEST, GenericData.class);
@@ -273,19 +289,19 @@ public class ErrorHandlingHttpClientTest {
     @Override
     public FirebaseException handleIOException(IOException e) {
       return new FirebaseException(
-          ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), null, e);
+          ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), e);
     }
 
     @Override
     public FirebaseException handleHttpResponseException(
         HttpResponseException e, IncomingHttpResponse response) {
       return new FirebaseException(
-          ErrorCode.INTERNAL, "Example error message: " + e.getContent(), response, e);
+          ErrorCode.INTERNAL, "Example error message: " + e.getContent(), e, response);
     }
 
     @Override
     public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) {
-      return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", response, e);
+      return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", e, response);
     }
   }
 }
diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java
index cea0010..0dd2a61 100644
--- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java
+++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
+import com.google.firebase.ErrorCode;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
@@ -43,8 +44,8 @@ public class BatchResponseTest {
     ImmutableList<SendResponse> responses = ImmutableList.of(
         SendResponse.fromMessageId("message1"),
         SendResponse.fromMessageId("message2"),
-        SendResponse.fromException(new FirebaseMessagingException("error-code",
-            "error-message", null))
+        SendResponse.fromException(
+            new FirebaseMessagingException(ErrorCode.INTERNAL, "error-message"))
     );
 
     BatchResponse batchResponse = new BatchResponse(responses);
diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java
index 0b84e00..bb69a46 100644
--- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java
+++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java
@@ -27,6 +27,7 @@ import static org.junit.Assert.fail;
 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.HttpMethods;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestInitializer;
 import com.google.api.client.http.HttpResponseException;
@@ -37,8 +38,10 @@ import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
+import com.google.firebase.OutgoingHttpRequest;
 import com.google.firebase.auth.MockGoogleCredentials;
 import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.messaging.WebpushNotification.Action;
@@ -62,6 +65,11 @@ public class FirebaseMessagingClientImplTest {
 
   private static final List<Integer> HTTP_ERRORS = ImmutableList.of(401, 404, 500);
 
+  private static final Map<Integer, ErrorCode> HTTP_2_ERROR = ImmutableMap.of(
+      401, ErrorCode.UNAUTHENTICATED,
+      404, ErrorCode.NOT_FOUND,
+      500, ErrorCode.INTERNAL);
+
   private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}";
 
   private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource(
@@ -128,8 +136,8 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "unknown-error",
-            "Unexpected HTTP response with status: " + code + "; body: {}");
+        checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null,
+            "Unexpected HTTP response with status: " + code + "\n{}");
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -143,9 +151,12 @@ public class FirebaseMessagingClientImplTest {
       client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
-      assertEquals("internal-error", error.getErrorCode());
-      assertEquals("Error while calling FCM backend service", error.getMessage());
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertEquals("Unknown error while making a remote service call: transport error",
+          error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
+      assertNull(error.getHttpResponse());
+      assertNull(error.getMessagingErrorCode());
     }
   }
 
@@ -160,8 +171,11 @@ public class FirebaseMessagingClientImplTest {
         client.send(entry.getKey(), DRY_RUN_DISABLED);
         fail("No error thrown for malformed response");
       } catch (FirebaseMessagingException error) {
-        assertEquals("internal-error", error.getErrorCode());
-        assertEquals("Error while calling FCM backend service", error.getMessage());
+        assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+        assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: "));
+        assertNotNull(error.getCause());
+        assertNotNull(error.getHttpResponse());
+        assertNull(error.getMessagingErrorCode());
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -176,8 +190,8 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "unknown-error",
-            "Unexpected HTTP response with status: " + code + "; body: null");
+        checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null,
+            "Unexpected HTTP response with status: " + code + "\nnull");
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -192,8 +206,8 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "unknown-error",
-            "Unexpected HTTP response with status: " + code + "; body: not json");
+        checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null,
+            "Unexpected HTTP response with status: " + code + "\nnot json");
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -209,7 +223,7 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "invalid-argument");
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null);
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -225,7 +239,7 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "registration-token-not-registered");
+        checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null);
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -243,7 +257,8 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "registration-token-not-registered");
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
+            MessagingErrorCode.UNREGISTERED);
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -261,7 +276,44 @@ public class FirebaseMessagingClientImplTest {
         client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "third-party-auth-error");
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
+            MessagingErrorCode.THIRD_PARTY_AUTH_ERROR);
+      }
+      checkRequestHeader(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testSendErrorWithUnknownFcmErrorCode() {
+    for (int code : HTTP_ERRORS) {
+      response.setStatusCode(code).setContent(
+          "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", "
+              + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm"
+              + ".v1.FcmError\", \"errorCode\": \"UNKNOWN_FCM_ERROR\"}]}}");
+
+      try {
+        client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseMessagingException error) {
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null);
+      }
+      checkRequestHeader(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testSendErrorWithDetailsAndNoCode() {
+    for (int code : HTTP_ERRORS) {
+      response.setStatusCode(code).setContent(
+          "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", "
+              + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm"
+              + ".v1.FcmError\"}]}}");
+
+      try {
+        client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED);
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseMessagingException error) {
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null);
       }
       checkRequestHeader(interceptor.getLastRequest());
     }
@@ -338,8 +390,8 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "unknown-error",
-            "Unexpected HTTP response with status: " + code + "; body: {}");
+        checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null,
+            "Unexpected HTTP response with status: " + code + "\n{}");
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -353,9 +405,12 @@ public class FirebaseMessagingClientImplTest {
       client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
-      assertEquals("internal-error", error.getErrorCode());
-      assertEquals("Error while calling FCM backend service", error.getMessage());
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
+      assertNull(error.getHttpResponse());
+      assertNull(error.getMessagingErrorCode());
     }
   }
 
@@ -368,8 +423,8 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "unknown-error",
-            "Unexpected HTTP response with status: " + code + "; body: null");
+        checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null,
+            "Unexpected HTTP response with status: " + code + "\nnull");
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -385,7 +440,7 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "invalid-argument");
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null);
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -401,7 +456,7 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "registration-token-not-registered");
+        checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null);
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -419,7 +474,8 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "registration-token-not-registered");
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
+            MessagingErrorCode.UNREGISTERED);
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -437,8 +493,9 @@ public class FirebaseMessagingClientImplTest {
         client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED);
         fail("No error thrown for HTTP error");
       } catch (FirebaseMessagingException error) {
-        checkExceptionFromHttpResponse(error, "registration-token-not-registered",
-            "Unexpected HTTP response with status: " + code + "; body: " + responseBody);
+        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
+            MessagingErrorCode.UNREGISTERED,
+            "Unexpected HTTP response with status: " + code + "\n" + responseBody);
       }
       checkBatchRequestHeader(interceptor.getLastRequest());
     }
@@ -476,7 +533,6 @@ public class FirebaseMessagingClientImplTest {
       FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.fromApp(app);
 
       assertEquals(TEST_FCM_URL, client.getFcmSendUrl());
-      assertEquals("fire-admin-java/" + SdkUtils.getVersion(), client.getClientVersion());
       assertSame(options.getJsonFactory(), client.getJsonFactory());
 
       HttpRequest request = client.getRequestFactory().buildGetRequest(
@@ -569,7 +625,10 @@ public class FirebaseMessagingClientImplTest {
 
       FirebaseMessagingException exception = sendResponse.getException();
       assertNotNull(exception);
-      assertEquals("invalid-argument", exception.getErrorCode());
+      assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCodeNew());
+      assertNull(exception.getCause());
+      assertNull(exception.getHttpResponse());
+      assertEquals(MessagingErrorCode.INVALID_ARGUMENT, exception.getMessagingErrorCode());
     }
 
     checkBatchRequestHeader(interceptor.getLastRequest());
@@ -610,15 +669,26 @@ public class FirebaseMessagingClientImplTest {
   }
 
   private void checkExceptionFromHttpResponse(
-      FirebaseMessagingException error, String expectedCode) {
-    checkExceptionFromHttpResponse(error, expectedCode, "test error");
+      FirebaseMessagingException error,
+      ErrorCode expectedCode,
+      MessagingErrorCode expectedMessagingCode) {
+    checkExceptionFromHttpResponse(error, expectedCode, expectedMessagingCode, "test error");
   }
 
   private void checkExceptionFromHttpResponse(
-      FirebaseMessagingException error, String expectedCode, String expectedMessage) {
-    assertEquals(expectedCode, error.getErrorCode());
+      FirebaseMessagingException error,
+      ErrorCode expectedCode,
+      MessagingErrorCode expectedMessagingCode,
+      String expectedMessage) {
+    assertEquals(expectedCode, error.getErrorCodeNew());
     assertEquals(expectedMessage, error.getMessage());
     assertTrue(error.getCause() instanceof HttpResponseException);
+    assertEquals(expectedMessagingCode, error.getMessagingErrorCode());
+
+    assertNotNull(error.getHttpResponse());
+    OutgoingHttpRequest request = error.getHttpResponse().getRequest();
+    assertEquals(HttpMethods.POST, request.getMethod());
+    assertTrue(request.getUrl().startsWith("https://fcm.googleapis.com"));
   }
 
   private static Map<Message, Map<String, Object>> buildTestMessages() {
@@ -633,7 +703,10 @@ public class FirebaseMessagingClientImplTest {
     // Notification message
     builder.put(
         Message.builder()
-            .setNotification(new Notification("test title", "test body"))
+            .setNotification(Notification.builder()
+                .setTitle("test title")
+                .setBody("test body")
+                .build())
             .setTopic("test-topic")
             .build(),
         ImmutableMap.<String, Object>of(
diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java
index 4d9af55..d414c4b 100644
--- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java
+++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java
@@ -22,10 +22,13 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import com.google.api.client.http.HttpResponseException;
 import com.google.common.collect.ImmutableList;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -72,6 +75,27 @@ public class FirebaseMessagingIT {
     assertTrue(id != null && id.matches("^projects/.*/messages/.*$"));
   }
 
+  @Test
+  public void testSendError() throws InterruptedException {
+    FirebaseMessaging messaging = FirebaseMessaging.getInstance();
+    Message message = Message.builder()
+        .setNotification(Notification.builder()
+            .setTitle("Title")
+            .setBody("Body")
+            .build())
+        .setToken("not-a-token")
+        .build();
+    try {
+      messaging.sendAsync(message, true).get();
+    } catch (ExecutionException e) {
+      FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause();
+      assertEquals(ErrorCode.INVALID_ARGUMENT, cause.getErrorCodeNew());
+      assertEquals(MessagingErrorCode.INVALID_ARGUMENT, cause.getMessagingErrorCode());
+      assertNotNull(cause.getHttpResponse());
+      assertTrue(cause.getCause() instanceof HttpResponseException);
+    }
+  }
+
   @Test
   public void testSendAll() throws Exception {
     List<Message> messages = new ArrayList<>();
@@ -110,7 +134,7 @@ public class FirebaseMessagingIT {
     assertNull(responses.get(2).getMessageId());
     FirebaseMessagingException exception = responses.get(2).getException();
     assertNotNull(exception);
-    assertEquals("invalid-argument", exception.getErrorCode());
+    assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCodeNew());
   }
 
   @Test
diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
index bebba08..3e76285 100644
--- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
+++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
@@ -27,6 +27,7 @@ import com.google.api.client.json.GenericJson;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
@@ -50,7 +51,7 @@ public class FirebaseMessagingTest {
       .addToken("test-fcm-token2")
       .build();
   private static final FirebaseMessagingException TEST_EXCEPTION =
-      new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception());
+      new FirebaseMessagingException(ErrorCode.INTERNAL, "Test error message");
 
   private static final ImmutableList.Builder<String> TOO_MANY_IDS = ImmutableList.builder();
 
diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
index f939c83..46cdcc4 100644
--- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
+++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
@@ -17,12 +17,14 @@
 package com.google.firebase.messaging;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.util.Utils;
-import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpMethods;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
@@ -31,8 +33,12 @@ import com.google.api.client.json.JsonParser;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
+import com.google.firebase.IncomingHttpResponse;
+import com.google.firebase.OutgoingHttpRequest;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.MockGoogleCredentials;
 import com.google.firebase.testing.TestResponseInterceptor;
@@ -55,6 +61,11 @@ public class InstanceIdClientImplTest {
 
   private static final List<Integer> HTTP_ERRORS = ImmutableList.of(401, 404, 500);
 
+  private static final Map<Integer, ErrorCode> HTTP_2_ERROR = ImmutableMap.of(
+      401, ErrorCode.UNAUTHENTICATED,
+      404, ErrorCode.NOT_FOUND,
+      500, ErrorCode.INTERNAL);
+
   @After
   public void tearDown() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
@@ -124,7 +135,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: {}");
+          "Unexpected HTTP response with status: 500\n{}");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL);
@@ -142,7 +153,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: not json");
+          "Unexpected HTTP response with status: 500\nnot json");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL);
@@ -160,7 +171,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: null");
+          "Unexpected HTTP response with status: 500\nnull");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL);
@@ -174,8 +185,26 @@ public class InstanceIdClientImplTest {
       client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2"));
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
-      assertEquals("internal-error", error.getErrorCode());
-      assertEquals("Error while calling IID backend service", error.getMessage());
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
+      assertTrue(error.getCause() instanceof IOException);
+    }
+  }
+
+  @Test
+  public void testSubscribeParseError() {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("not json");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    InstanceIdClient client = initInstanceIdClient(response, interceptor);
+
+    try {
+      client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2"));
+      fail("No error thrown for HTTP error");
+    } catch (FirebaseMessagingException error) {
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: "));
       assertTrue(error.getCause() instanceof IOException);
     }
   }
@@ -244,7 +273,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: {}");
+          "Unexpected HTTP response with status: 500\n{}");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL);
@@ -262,7 +291,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: not json");
+          "Unexpected HTTP response with status: 500\nnot json");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL);
@@ -280,7 +309,7 @@ public class InstanceIdClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
       checkExceptionFromHttpResponse(error, 500,
-          "Unexpected HTTP response with status: 500; body: null");
+          "Unexpected HTTP response with status: 500\nnull");
     }
 
     checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL);
@@ -294,8 +323,26 @@ public class InstanceIdClientImplTest {
       client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2"));
       fail("No error thrown for HTTP error");
     } catch (FirebaseMessagingException error) {
-      assertEquals("internal-error", error.getErrorCode());
-      assertEquals("Error while calling IID backend service", error.getMessage());
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
+      assertTrue(error.getCause() instanceof IOException);
+    }
+  }
+
+  @Test
+  public void testUnsubscribeParseError() {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("not json");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    InstanceIdClient client = initInstanceIdClient(response, interceptor);
+
+    try {
+      client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2"));
+      fail("No error thrown for HTTP error");
+    } catch (FirebaseMessagingException error) {
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew());
+      assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: "));
       assertTrue(error.getCause() instanceof IOException);
     }
   }
@@ -311,9 +358,15 @@ public class InstanceIdClientImplTest {
   }
 
   @Test
-  public void testFromApp() throws IOException {
+  public void testFromApp() {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setStatusCode(400).setZeroContent();
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
     FirebaseOptions options = new FirebaseOptions.Builder()
         .setCredentials(new MockGoogleCredentials("test-token"))
+        .setHttpTransport(transport)
         .setProjectId("test-project")
         .build();
     FirebaseApp app = FirebaseApp.initializeApp(options);
@@ -322,9 +375,14 @@ public class InstanceIdClientImplTest {
       InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app);
 
       assertSame(options.getJsonFactory(), client.getJsonFactory());
-      HttpRequest request = client.getRequestFactory().buildGetRequest(
-          new GenericUrl("https://example.com"));
-      assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
+      client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2"));
+      fail("No error thrown for error response");
+    } catch (FirebaseMessagingException e) {
+      assertNotNull(e.getHttpResponse());
+
+      List<String> auth = ImmutableList.of("Bearer test-token");
+      OutgoingHttpRequest request = e.getHttpResponse().getRequest();
+      assertEquals(auth, request.getHeaders().get("authorization"));
     } finally {
       app.delete();
     }
@@ -377,11 +435,20 @@ public class InstanceIdClientImplTest {
     assertEquals(expectedUrl, request.getUrl().toString());
   }
 
-  private void checkExceptionFromHttpResponse(FirebaseMessagingException error,
-      int expectedCode, String expectedMessage) {
-    assertEquals(getTopicManagementErrorCode(expectedCode), error.getErrorCode());
+  private void checkExceptionFromHttpResponse(
+      FirebaseMessagingException error, int statusCode, String expectedMessage) {
+    assertEquals(HTTP_2_ERROR.get(statusCode), error.getErrorCodeNew());
     assertEquals(expectedMessage, error.getMessage());
     assertTrue(error.getCause() instanceof HttpResponseException);
+    assertNull(error.getMessagingErrorCode());
+
+    IncomingHttpResponse httpResponse = error.getHttpResponse();
+    assertNotNull(httpResponse);
+    assertEquals(statusCode, httpResponse.getStatusCode());
+
+    OutgoingHttpRequest request = httpResponse.getRequest();
+    assertEquals(HttpMethods.POST, request.getMethod());
+    assertTrue(request.getUrl().startsWith("https://iid.googleapis.com"));
   }
 
   private InstanceIdClient initClientWithFaultyTransport() {
@@ -389,12 +456,4 @@ public class InstanceIdClientImplTest {
         TestUtils.createFaultyHttpTransport().createRequestFactory(),
         Utils.getDefaultJsonFactory());
   }
-
-  private String getTopicManagementErrorCode(int statusCode) {
-    String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode);
-    if (code == null) {
-      code = "unknown-error";
-    }
-    return code;
-  }
 }
diff --git a/src/test/java/com/google/firebase/messaging/SendResponseTest.java b/src/test/java/com/google/firebase/messaging/SendResponseTest.java
index e9667cc..d90f1e6 100644
--- a/src/test/java/com/google/firebase/messaging/SendResponseTest.java
+++ b/src/test/java/com/google/firebase/messaging/SendResponseTest.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import com.google.firebase.ErrorCode;
 import org.junit.Test;
 
 public class SendResponseTest {
@@ -37,8 +38,8 @@ public class SendResponseTest {
 
   @Test
   public void testFailureResponse() {
-    FirebaseMessagingException exception = new FirebaseMessagingException("error-code",
-        "error-message", null);
+    FirebaseMessagingException exception = new FirebaseMessagingException(
+        ErrorCode.INTERNAL, "error-message");
     SendResponse response = SendResponse.fromException(exception);
 
     assertNull(response.getMessageId());