Repository: firebase/firebase-admin-java
Pull request #148 (2018-02-27 23:46:56)

Handling FCM canonical error codes
The error codes defined in https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode are embedded in the details section of the error response. The top-level error code (`status` field in the response) may take additional values such as `NOT_FOUND` and `UNAUTHENTICATED` (the so called canonical error codes):

```
{
  "error":{
    "code":404,
    "message":"Requested entity was not found.",
    "status":"NOT_FOUND",
    "details: [
      {
         "@type":"type.googleapis.com/google.firebase.fcm.v1.FcmError",
         "errorCode":"UNREGISTERED"
      }
    ]
  }
}
```

This PR updates the FCM implementation to handle these additional error codes and map them to correct client-side errors.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b00a33..6898334 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,10 @@
 # Unreleased
 
--
+- [fixed] Improved error handling in FCM by mapping more server-side
+  errors to client-side error codes.
 
 # v5.9.0
 
-
 ### Cloud Messaging
 
 - [feature] Added the `FirebaseCloudMessaging` API for sending
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
index 00b68c6..c068004 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
@@ -63,13 +63,20 @@ public class FirebaseMessaging {
   private static final String UNKNOWN_ERROR = "unknown-error";
   private static final Map<String, String> FCM_ERROR_CODES =
       ImmutableMap.<String, String>builder()
-        .put("APNS_AUTH_ERROR", "authentication-error")
+        // 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", "invalid-apns-credentials")
+
+        // FCM v1 new error codes
+        .put("APNS_AUTH_ERROR", "invalid-apns-credentials")
         .put("INTERNAL", INTERNAL_ERROR)
         .put("INVALID_ARGUMENT", "invalid-argument")
-        .put("UNREGISTERED", "registration-token-not-registered")
         .put("QUOTA_EXCEEDED", "message-rate-exceeded")
-        .put("SENDER_ID_MISMATCH", "authentication-error")
+        .put("SENDER_ID_MISMATCH", "mismatched-credential")
         .put("UNAVAILABLE", "server-unavailable")
+        .put("UNREGISTERED", "registration-token-not-registered")
         .build();
   static final Map<Integer, String> IID_ERROR_CODES =
       ImmutableMap.<Integer, String>builder()


Test patch:
diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
index ac55255..3eb1545 100644
--- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
+++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
@@ -195,6 +195,29 @@ public class FirebaseMessagingTest {
     }
   }
 
+  @Test
+  public void testSendErrorWithCanonicalCode() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    FirebaseMessaging messaging = initMessaging(response);
+    for (int code : HTTP_ERRORS) {
+      response.setStatusCode(code).setContent(
+          "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}");
+      TestResponseInterceptor interceptor = new TestResponseInterceptor();
+      messaging.setInterceptor(interceptor);
+      try {
+        messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get();
+        fail("No error thrown for HTTP error");
+      } catch (ExecutionException e) {
+        assertTrue(e.getCause() instanceof FirebaseMessagingException);
+        FirebaseMessagingException error = (FirebaseMessagingException) e.getCause();
+        assertEquals("registration-token-not-registered", error.getErrorCode());
+        assertEquals("test error", error.getMessage());
+        assertTrue(error.getCause() instanceof HttpResponseException);
+      }
+      checkRequestHeader(interceptor);
+    }
+  }
+
   @Test
   public void testInvalidSubscribe() {
     FirebaseMessaging messaging = initDefaultMessaging();