FCM Topics in the Firebase Admin SDK for Java: Which Error, What to Do?

subscribeToTopic and unsubscribeFromTopic in firebase-admin 9.11.0 for Java report errors as a reason string per token. This post maps each response to the reaction your backend should take.

Google's docs are of little help here: they mention getReason() but list neither the possible values nor what to do with them.

The findings come from tests on an Android device and from the SDK source code.

Up front

  • Server errors don't throw an exception. They come back as a list with one entry per token.
  • getIndex() points to the token's position in the list you passed in.
  • Subscribe and unsubscribe are idempotent, so retrying is always safe.
TopicManagementResponse response = messaging.subscribeToTopic(tokens, "news");
for (TopicManagementResponse.Error error : response.getErrors()) {
    String token = tokens.get(error.getIndex());
    String reason = error.getReason();
    // react depending on reason, see below
}

Success, nothing to do

  • No entry in getErrors(), getSuccessCount() counts the token.
  • Subscribing to a topic the token already has: the server answers 409 ALREADY_EXISTS, the SDK counts that as success.
  • Unsubscribing from a topic the token never had: success.

Success does not mean the app is still installed. Right after an uninstall, and as long as the device is offline, the calls keep succeeding.

The call fails permanently

The token is of no further use. You don't need it anymore.

  • registration-token-not-registered: the app was uninstalled, deleteToken() was called, or the app data was cleared. For unsubscribe, the subscription is gone, so treat the call as done.
  • invalid-argument: the string is not a valid token.

If every token in a call fails with the same reason, the tokens are probably not the problem. Check the configuration first.

Retry later

The token itself is still valid. A retry at a later time may succeed.

  • internal-error
  • unavailable: if Google answers with 503, the SDK retries four times on its own, five attempts in total.
  • deadline-exceeded
  • resource-exhausted
  • quota-exceeded
  • aborted
  • cancelled
  • failed-precondition
  • unknown-error: network errors, an unknown HTTP status, or a response the SDK can't read
  • FirebaseMessagingException with getErrorCode() == ErrorCode.CANCELLED: the waiting thread was interrupted. The SDK may already have processed some of the tokens.
  • IllegalStateException "FirebaseApp … was deleted"

Any other server status ends up in reason in lowercase with hyphens, for example out-of-range.

Fix the configuration

  • permission-denied: the IAM permissions are missing, check the settings.
  • unauthenticated: the configured Firebase credentials are invalid.
  • IllegalArgumentException "Project ID is required …"

Fix your own code

The SDK checks these cases before sending a request and throws IllegalArgumentException:

  • An empty or null token in the list. A single one fails the whole call.
  • An empty list or more than 1000 tokens.
  • A topic name that doesn't match ^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$.

Setup

  • firebase-admin 9.11.0, Java 21
  • Android device running Android 16 with Google Play services
  • Capacitor app with @capacitor-firebase/messaging 8.4.0

For comments or feedback, write to me at info (at) leonardw (dot) de