Gestion des erreurs du SDK d'administration

Les erreurs du SDK Admin sont divisées en deux catégories :

  1. Erreurs de programmation : Il s'agit d'erreurs de programmation et de configuration dans l'application utilisateur. Ils se produisent principalement en raison d'une utilisation incorrecte du SDK (comme le passage null à une méthode qui n'accepte pas les valeurs null ) et d'autres erreurs de configuration au niveau du projet Firebase ou du SDK (informations d'identification manquantes, chaîne d'ID de projet incorrecte, etc. sur).
  2. Erreurs d'API : celles-ci incluent diverses erreurs récupérables qui se produisent dans l'implémentation du SDK, toutes les erreurs provenant des services backend Firebase et d'autres erreurs transitoires (telles que des délais d'attente) qui peuvent survenir lors des appels RPC.

Le SDK Admin signale les erreurs de programmation en lançant une erreur native de la plate-forme en question.

  • Java : génère des instances de IllegalArgumentException , NullPointerException ou un type d'erreur d'exécution intégré similaire.
  • Python : génère des instances de ValueError , TypeError ou un autre type d'erreur intégré.
  • Go : renvoie une erreur générique.
  • .NET : génère des instances de ArgumentException , ArgumentNullException ou un type d'erreur intégré similaire.

Dans la plupart des situations, vous ne devez pas gérer explicitement les erreurs de programmation. Au lieu de cela, vous devez corriger votre code et votre configuration pour éviter complètement les erreurs de programmation. Considérez l'extrait Java suivant :

String uid = getUserInput();

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

Si la méthode getUserInput() renvoie des chaînes null ou vides, l'API FirebaseAuth.getUser() renvoie une IllegalArgumentException . Au lieu de le gérer explicitement, vous pouvez atténuer le problème en vous assurant que la méthode getUserInput() ne renvoie jamais une chaîne UID non valide. Si ce n'est pas possible, implémentez la vérification des arguments nécessaire dans votre propre code comme suit :

String uid = getUserInput();
if (Strings.isNullOrEmpty(uid)) {
    log.warn("UID must not be null or empty");
    return;
}

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

Par principe, ne réessayez jamais sur des erreurs de programmation. Autoriser une sémantique rapide sur les erreurs de programmation est souvent la meilleure solution car elle expose les bogues de programmation et les erreurs de configuration pendant le développement, où ils peuvent être rapidement corrigés. Dans ce contexte, l'échec rapide peut signifier laisser les erreurs se propager à un gestionnaire d'erreurs global dans votre application, ou simplement les enregistrer à des fins d'audit, suivi de la fin du flux d'exécution en cours (l'application ne devrait pas avoir à planter). En général, suivez les meilleures pratiques de gestion des erreurs de votre langage de programmation et du framework d'application. Cela suffit souvent à traiter correctement cette classe d’erreurs.

En règle générale, l'essentiel de vos efforts de gestion des erreurs se concentrera sur la gestion des erreurs d'API . Certaines de ces erreurs sont récupérables, comme les erreurs résultant d'un service temporairement indisponible, et certaines sont même anticipées pendant le flux normal d'exécution du programme, comme la détection de jetons d'identification invalides ou expirés. Le reste de ce guide décrit comment le SDK Admin représente ces erreurs d'API et les différentes options disponibles pour les gérer.

Structure d'une erreur API

Une erreur API se compose des éléments suivants :

  1. Code d'erreur
  2. Message d'erreur
  3. Code d'erreur de service (facultatif)
  4. Réponse HTTP (facultatif)

Chaque erreur API est garantie de contenir un code d'erreur et un message d'erreur. Certaines erreurs d'API contiennent également un code d'erreur de service spécifique à l'API qui a généré l'erreur. Par exemple, certaines erreurs générées par l'API Firebase Auth contiennent un code d'erreur de service spécifique à Firebase Auth. Si l'erreur était le résultat d'une réponse d'erreur HTTP provenant d'un service backend, l'erreur API contient également la réponse HTTP correspondante. Cela peut être utilisé pour inspecter les en-têtes et le contenu exacts de la réponse d'origine, ce qui est utile pour le débogage, la journalisation ou la mise en œuvre d'une logique de gestion des erreurs plus sophistiquée.

Toutes les implémentations du SDK Admin, à l'exception de Node.js, fournissent des API qui permettent d'accéder aux composants ci-dessus des erreurs d'API.

Types d'erreurs et API par langue

Java

En Java, toutes les erreurs d'API étendent la classe FirebaseException . Vous pouvez accéder au code d'erreur, au message d'erreur et à la réponse HTTP facultative à partir de cette classe de base.

public class FirebaseException extends Exception {
    @NonNull
    public ErrorCode getErrorCode() {
        // ...
    }

    @NonNull
    public String getMessage() {
        // ...
    }

    @Nullable
    public IncomingHttpResponse getHttpResponse() {
        // ...
    }
}

Les API qui exposent les codes d'erreur de service fournissent des sous-classes de FirebaseException spécifiques aux API. Par exemple, toutes les méthodes publiques de l'API FirebaseAuth sont déclarées pour lancer des instances de FirebaseAuthException . Vous pouvez accéder au code d'erreur du service à partir de cette classe dérivée.

public class FirebaseAuthException extends FirebaseException {
    @Nullable
    public AuthErrorCode getAuthErrorCode() {
        // ...
    }
}

Python

En Python, toutes les erreurs d'API étendent la classe exceptions.FirebaseError . Vous pouvez accéder au code d'erreur, au message d'erreur et à la réponse HTTP facultative à partir de cette classe de base.

class FirebaseError(Exception):
    @property
    def code(self):
          # ...

    @property
    def message(self):
          # ...

    @property
    def http_response(self):
          # ...

De plus, Python Admin SDK propose des classes dérivées distinctes pour chaque code d'erreur. Nous les appelons classes d'erreurs de plate-forme .

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Vous pouvez soit intercepter FirebaseError dans votre code et vérifier son code , soit effectuer une vérification isinstance() par rapport à une classe d'erreur de plate-forme. Vous pouvez également écrire du code pour détecter directement des types d’erreurs spécifiques à la plate-forme. Cette dernière approche est susceptible d'aboutir à un code de gestion des erreurs plus lisible.

Les API qui exposent les codes d'erreur de service fournissent des sous-classes spécifiques aux API des classes d'erreur de plateforme. Par exemple, toutes les méthodes publiques du module auth peuvent générer des types d'erreur spécifiques à l'API tels que auth.UserNotFoundError et auth.ExpiredIdTokenError .

class UserNotFoundError(exceptions.NotFoundError):
    # …

class ExpiredIdTokenError(exceptions.InvalidArgumentError):
    # ...

Aller

Le SDK Go Admin fournit un package errorutils qui contient une série de fonctions permettant de tester les codes d'erreur.

package errorutils

func IsInvalidArgument(err error) bool {
    // ...
}

func IsNotFound(err error) bool {
    // ...
}

Le message d'erreur est simplement la chaîne renvoyée par la fonction Error() d'une erreur. La réponse HTTP facultative est accessible en appelant la fonction errorutils.HTTPResponse() , qui renvoie un *http.Response .

Il est prudent de transmettre nil ou toute autre valeur d’erreur aux fonctions de vérification des erreurs du package errorutils . Ils renvoient true si l'argument d'entrée contient réellement le code d'erreur en question et renvoient false pour tout le reste. La fonction HTTPResponse() a un comportement similaire, sauf qu'elle renvoie nil au lieu de false .

Les API qui exposent les codes d'erreur de service fournissent des fonctions de vérification des erreurs spécifiques à l'API dans les packages correspondants. Par exemple, le package auth fournit les fonctions IsUserNotFound() et IsExpiredIDTokenError() .

.FILET

Dans .NET, toutes les erreurs d'API étendent la classe FirebaseException . Vous pouvez accéder au code d'erreur de la plateforme, au message d'erreur et à la réponse HTTP facultative à partir de cette classe de base.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

Les API qui exposent les codes d'erreur de service fournissent des sous-classes de FirebaseException spécifiques aux API. Par exemple, toutes les méthodes publiques de l'API FirebaseAuth sont déclarées pour lancer des instances de FirebaseAuthException . Vous pouvez accéder au code d'erreur du service à partir de cette classe dérivée.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Codes d'erreur de la plateforme

Les codes d'erreur sont communs à tous les services Firebase et Google Cloud Platform. Le tableau suivant présente tous les codes d'erreur possibles de la plate-forme. Il s’agit d’une liste stable et qui devrait rester inchangée pendant une longue période.

ARGUMENT INVALIDE Le client a spécifié un argument non valide.
FAILED_PRECONDITION La requête ne peut pas être exécutée dans l'état actuel du système, comme la suppression d'un répertoire non vide.
OUT_OF_RANGE Le client a spécifié une plage non valide.
NON AUTHENTIFIÉ Demande non authentifiée en raison d'un jeton OAuth manquant, invalide ou expiré.
PERMISSION REFUSÉE Le client ne dispose pas d'une autorisation suffisante. Cela peut se produire parce que le jeton OAuth n'a pas les bonnes étendues, que le client n'a pas l'autorisation ou que l'API n'a pas été activée pour le projet client.
PAS TROUVÉ La ressource spécifiée est introuvable ou la demande est rejetée pour des raisons non divulguées telles que la mise sur liste blanche.
CONFLIT Conflit de concurrence, tel qu'un conflit de lecture-modification-écriture. Utilisé uniquement par quelques services existants. La plupart des services utilisent ABORTED ou ALREADY_EXISTS à la place. Reportez-vous à la documentation spécifique au service pour voir lequel gérer dans votre code.
AVORTÉ Conflit de concurrence, tel qu'un conflit de lecture-modification-écriture.
EXISTE DÉJÀ La ressource qu'un client a tenté de créer existe déjà.
RESOURCE_EXHAUSTED Soit en dehors du quota de ressources, soit en atteignant la limite de débit.
ANNULÉ Demande annulée par le client.
PERTE DE DONNÉES Perte de données irrécupérable ou corruption de données. Le client doit signaler l'erreur à l'utilisateur.
INCONNU Erreur de serveur inconnue. Généralement un bug du serveur.

Ce code d'erreur est également attribué aux erreurs d'analyse de réponse locale (unmarshal) et à un large éventail d'autres erreurs d'E/S de bas niveau qui ne sont pas facilement diagnosticables.

INTERNE Erreur interne du serveur. Généralement un bug du serveur.
INDISPONIBLE Service non disponible. Généralement, le serveur est temporairement hors service.

Ce code d'erreur est également attribué aux erreurs du réseau local (connexion refusée, pas de route vers l'hôte).

DEADLINE_EXCEEDED Délai de demande dépassé. Cela ne se produira que si l'appelant définit un délai plus court que le délai par défaut de l'API cible (c'est-à-dire que le délai demandé n'est pas suffisant pour que le serveur traite la demande) et que la demande ne s'est pas terminée dans le délai imparti.

Ce code d'erreur est également attribué à la connexion locale et aux délais de lecture.

La plupart des API ne peuvent générer qu'un sous-ensemble des codes d'erreur ci-dessus. Dans tous les cas, vous n'êtes pas censé gérer explicitement tous ces codes d'erreur lors de l'implémentation de vos gestionnaires d'erreurs. La plupart des applications ne s'intéresseraient qu'à 1 à 2 codes d'erreur spécifiques et traiteraient tout le reste comme une panne générique irrécupérable.

Codes d'erreur spécifiques au service

Authentification Firebase

CERTIFICAT_FETCH_FAILED Échec de la récupération des certificats de clé publique requis pour vérifier un JWT (jeton d'identification ou cookie de session).
L'EMAIL EXISTE DÉJÀ Un utilisateur existe déjà avec l'e-mail fourni.
EXPIRED_ID_TOKEN Le jeton d'ID spécifié pour verifyIdToken() a expiré.
EXPIRED_SESSION_COOKIE Le cookie de session spécifié pour verifySessionCookie() a expiré.
INVALID_DYNAMIC_LINK_DOMAIN Le domaine de lien dynamique fourni n'est pas configuré ou autorisé pour le projet en cours. Lié aux API de liens d’action par courrier électronique.
INVALID_ID_TOKEN Le jeton d'ID spécifié pour verifyIdToken() n'est pas valide.
INVALID_SESSION_COOKIE Le cookie de session spécifié pour verifySessionCookie() n'est pas valide.
PHONE_NUMBER_ALREADY_EXISTS Un utilisateur existe déjà avec le numéro de téléphone fourni.
REVOKED_ID_TOKEN Le jeton d'ID spécifié pour verifyIdToken() est révoqué.
REVOKED_SESSION_COOKIE Le cookie de session spécifié pour verifySessionCookie() a expiré.
UNAUTHORIZED_CONTINUE_URL Le domaine de l'URL de suite n'est pas sur liste blanche. Lié aux API de liens d’action par courrier électronique.
UTILISATEUR NON TROUVÉ Aucun enregistrement d'utilisateur trouvé pour l'identifiant donné.

Messagerie cloud Firebase

THIRD_PARTY_AUTH_ERROR Le certificat APN ou la clé API d'authentification Web Push était invalide ou manquant.
ARGUMENT INVALIDE Un ou plusieurs arguments spécifiés dans la demande n'étaient pas valides.
INTERNE Erreur interne du serveur.
QUOTA DÉPASSÉ Limite d'envoi dépassée pour la cible du message.
SENDER_ID_MISMATCH L'ID de l'expéditeur authentifié est différent de l'ID de l'expéditeur du jeton d'enregistrement. Cela signifie généralement que l'expéditeur et le jeton d'enregistrement cible ne se trouvent pas dans le même projet Firebase.
INDISPONIBLE Le service Cloud Messaging est temporairement indisponible.
NON ENREGISTRÉ L'instance d'application n'a pas été enregistrée auprès de FCM. Cela signifie généralement que le jeton d'enregistrement de l'appareil utilisé n'est plus valide et qu'un nouveau doit être utilisé.

Nouvelles tentatives automatiques

Le SDK Admin réessaye automatiquement certaines erreurs avant de les exposer aux utilisateurs. En général, les types d’erreurs suivants font l’objet de nouvelles tentatives transparentes :

  • Toutes les erreurs API résultant de réponses HTTP 503 (Service non disponible).
  • Certaines erreurs API résultant de réponses HTTP 500 (Internal Server Error).
  • La plupart des erreurs d'E/S de bas niveau (connexion refusée, réinitialisation de la connexion, etc.).

Le SDK réessayera chacune des erreurs ci-dessus jusqu'à 5 fois (la tentative d'origine + 4 tentatives) avec un intervalle exponentiel. Vous pouvez implémenter vos propres mécanismes de nouvelle tentative au niveau de l'application si vous le souhaitez, mais cela n'est généralement pas obligatoire.

Prise en charge de la nouvelle tentative après

Les implémentations Go et .NET du SDK Admin prennent en charge la gestion de l'en-tête HTTP Retry-After . Autrement dit, si la réponse d'erreur envoyée par les serveurs backend contient l'en-tête Retry-After standard, le SDK le respectera lors d'une nouvelle tentative tant que la durée d'attente spécifiée n'est pas très longue. Si l’en-tête Retry-After indique une durée d’attente très longue, le SDK ignorera les tentatives et générera l’erreur API appropriée.

Le SDK Python Admin ne prend actuellement pas en charge l’en-tête Retry-After et ne prend en charge qu’un simple intervalle exponentiel.

Exemples de gestion des erreurs API

Implémentation d'un gestionnaire d'erreurs générique

Dans la plupart des cas, vous souhaitez un gestionnaire d'erreurs générique qui détecte un large éventail d'erreurs afin d'éviter une interruption inattendue du flux du programme en raison d'une erreur d'API. Ces gestionnaires d'erreurs se contentent généralement d'enregistrer les erreurs à des fins d'audit ou d'appeler une autre routine de gestion des erreurs par défaut pour toutes les erreurs d'API rencontrées. Ils ne sont pas forcément intéressés par les différents codes d’erreur ni par les raisons qui ont pu provoquer l’erreur.

Java

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  System.err.println("Failed to verify ID token: " + ex.getMessage());
}

Python

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_pperation(token.uid)
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Aller

token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Filet

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex) 
{
  Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
}

Vérification des codes d'erreur

Dans certains cas, vous souhaiterez inspecter les codes d'erreur exacts et invoquer différentes routines de gestion des erreurs contextuelles. Dans l'exemple suivant, nous avons un gestionnaire d'erreurs qui enregistre des messages d'erreur plus spécifiques en fonction du code d'erreur du service.

Java

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_EXPIRED) {
    System.err.println("ID token has expired");
  } else if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_INVALID) {
    System.err.println("ID token is malformed or invalid");
  } else {
    System.err.println("Failed to verify ID token: " + ex.getMessage());
  }
}

Python

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_operation(token.uid)
except auth.ExpiredIdTokenError:
  print('ID token has expired')
except auth.InvalidIdTokenError:
  print('ID token is malformed or invalid')
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Aller

token, err := client.VerifyIDToken(ctx, idToken)
if auth.IsIDTokenExpired(err) {
  log.Print("ID token has expired")
  return
}
if auth.IsIDTokenInvalid(err) {
  log.Print("ID token is malformed or invalid")
  return
}
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Filet

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex)
{
  if (ex.AuthErrorCode == AuthErrorCode.ExpiredIdToken)
  {
    Console.WriteLine("ID token has expired");
  }
  else if (ex.AuthErrorCode == AuthErrorCode.InvalidIdToken)
  {
    Console.WriteLine("ID token is malformed or invalid");
  }
  else
  {
    Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
  }
}

Voici un autre exemple dans lequel nous vérifions les codes d'erreur de niveau supérieur et de service :

Java

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  if (ex.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) {
    System.err.println("App instance has been unregistered");
    removeTokenFromDatabase();
  } else if (ex.getErrorCode() == ErrorCode.Unavailable) {
    System.err.println("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  } else {
    System.err.println("Failed to send notification: " + ex.getMessage());
  }
}

Python

try:
  messaging.send(create_my_message())
except messaging.UnregisteredError:
  print('App instance has been unregistered')
  remove_token_from_database()
except exceptions.UnavailableError:
  print('FCM service is temporarily unavailable')
  schedule_for_retry_in_an_hour()
except exceptions.FirebaseError as ex:
  print(f'Failed to send notification: {ex}')

Aller

_, err := client.Send(ctx, createMyMessage())
if messaging.IsUnregistered(err) {
  log.Print("App instance has been unregistered")
  removeTokenFromDatabase()
  return
}
if errorutils.IsUnavailable(err) {
  log.Print("FCM service is temporarily unavailable")
  scheduleForRetryInAnHour()
  return
}
if err != nil {
  log.Printf("Failed to send notification: %v", err)
  return
}

.Filet

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  if (ex.MessagingErrorCode == MessagingErrorCode.UNREGISTERED)
  {
    Console.WriteLine("App instance has been unregistered");
    removeTokenFromDatabase();
  }
  else if (ex.ErrorCode == ErrorCode.Unavailable)
  {
    Console.WriteLine("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  }
  else
  {
    Console.WriteLine($"Failed to send notification: {ex.Message}");
  }
}

Accéder à la réponse HTTP

Dans de rares cas, vous souhaiterez peut-être inspecter la réponse d'erreur HTTP renvoyée par un service backend et effectuer une action de gestion des erreurs sur celle-ci. Le SDK Admin expose à la fois les en-têtes et le contenu de ces réponses d’erreur. Le contenu de la réponse est généralement renvoyé sous forme de chaîne ou de séquence d'octets brute et peut être analysé dans n'importe quel format cible nécessaire.

Java

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  IncomingHttpResponse response = ex.getHttpResponse();
  if (response != null) {
    System.err.println("FCM service responded with HTTP " + response.getStatusCode());

    Map<String, Object> headers = response.getHeaders();
    for (Map.Entry<String, Object> entry : headers.entrySet()) {
      System.err.println(">>> " + entry.getKey() + ": " + entry.getValue());
    }

    System.err.println(">>>");
    System.err.println(">>> " + response.getContent());
  }
}

Python

try:
  messaging.send(create_my_message())
except exceptions.FirebaseError as ex:
  response = ex.http_response
  if response is not None:
    print(f'FCM service responded with HTTP {response.status_code}')

    for key, value in response.headers.items():
      print(f'>>> {key}: {value}')

    print('>>>')
    print(f'>>> {response.content}')

Aller

_, err := client.Send(ctx, createMyMessage())
if resp := errorutils.HTTPResponse(err); resp != nil {
  log.Printf("FCM service responded with HTTP %d", resp.StatusCode)

  for key, value := range resp.Header {
      log.Printf(">>> %s: %v", key, value)
  }

  defer resp.Body.Close()
  b, _ := ioutil.ReadAll(resp.Body)
  log.Print(">>>")
  log.Printf(">>> %s", string(b))

  return
}

.Filet

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  var response = ex.HttpResponse
  if response != null
  {
    Console.WriteLine($"FCM service responded with HTTP { response.StatusCode}");

    var headers = response.Headers;
    for (var entry in response.Headers)
    {
      Console.WriteLine($">>> {entry.Key}: {entry.Value}");
    }

    var body = await response.Content.ReadAsString();
    Console.WriteLine(">>>");
    Console.WriteLine($">>> {body}");
  }
}