Gestione degli errori dell'SDK di amministrazione

Gli errori dell'SDK di amministrazione sono divisi in due categorie:

  1. Errori di programmazione: sono errori di programmazione e configurazione nell'applicazione utente. Si verificano principalmente a causa dell'utilizzo errato dell'SDK (ad esempio passando null a un metodo che non accetta valori null ) e altri errori di configurazione a livello di progetto Firebase o SDK (credenziali mancanti, stringa ID progetto errata e così via) SU).
  2. Errori API: includono vari errori reversibili che si verificano all'interno dell'implementazione dell'SDK, tutti gli errori che hanno origine nei servizi backend Firebase e altri errori temporanei (come i timeout) che possono verificarsi durante l'esecuzione di chiamate RPC.

L'Admin SDK segnala gli errori di programmazione generando un errore nativo della piattaforma in questione.

  • Java: genera istanze di IllegalArgumentException , NullPointerException o un tipo di errore di runtime integrato simile.
  • Python: genera istanze di ValueError , TypeError o altro tipo di errore integrato.
  • Vai: restituisce un errore generico.
  • .NET: genera istanze di ArgumentException , ArgumentNullException o un tipo di errore incorporato simile.

Nella maggior parte delle situazioni non dovresti gestire esplicitamente gli errori di programmazione. Dovresti invece correggere il codice e la configurazione per evitare del tutto errori di programmazione. Considera il seguente frammento Java:

String uid = getUserInput();

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

Se il metodo getUserInput() restituisce stringhe null o vuote, l'API FirebaseAuth.getUser() genera IllegalArgumentException . Invece di gestirlo esplicitamente, puoi mitigare il problema assicurandoti che il metodo getUserInput() non restituisca mai una stringa UID non valida. Se ciò non è possibile, implementa il controllo degli argomenti necessari nel tuo codice come segue:

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

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

In linea di principio, non riprovare mai con errori di programmazione. Consentire una semantica fail-fast sugli errori di programmazione è spesso la migliore linea d'azione perché espone bug di programmazione ed errori di configurazione durante lo sviluppo, dove possono essere prontamente corretti. Fail-fast in questo contesto può significare lasciare che gli errori si propaghino a un gestore di errori globale nell'applicazione o semplicemente registrarli a fini di controllo seguiti dalla terminazione del flusso di esecuzione corrente (l'applicazione non dovrebbe bloccarsi). In generale, segui le migliori pratiche di gestione degli errori del tuo linguaggio di programmazione e del framework dell'applicazione. Questo da solo è spesso sufficiente per affrontare correttamente questa classe di errori.

In genere, la maggior parte delle attività di gestione degli errori si concentrerà sulla gestione degli errori API . Alcuni di questi errori sono recuperabili, come gli errori derivanti da un servizio temporaneamente non disponibile, e alcuni vengono addirittura anticipati durante il normale flusso di esecuzione del programma, come il rilevamento di token ID non validi o scaduti. Il resto di questa guida descrive il modo in cui Admin SDK rappresenta tali errori API e le varie opzioni disponibili per gestirli.

Struttura di un errore API

Un errore API è costituito dai seguenti componenti:

  1. Codice di errore
  2. Messaggio di errore
  3. Codice errore servizio (facoltativo)
  4. Risposta HTTP (facoltativo)

È garantito che ogni errore API contenga un codice di errore e un messaggio di errore. Alcuni errori API contengono anche un codice di errore del servizio specifico dell'API che ha generato l'errore. Ad esempio, alcuni errori generati dall'API Firebase Auth contengono un codice di errore del servizio specifico per Firebase Auth. Se l'errore è il risultato di una risposta di errore HTTP da un servizio backend, l'errore API contiene anche la risposta HTTP corrispondente. Questo può essere utilizzato per controllare le intestazioni e i contenuti esatti della risposta originale, utile per il debug, la registrazione o l'implementazione di una logica di gestione degli errori più sofisticata.

Tutte le implementazioni di Admin SDK, ad eccezione di Node.js, forniscono API che consentono l'accesso ai componenti sopra indicati degli errori API.

Tipi di errore e API per lingua

Giava

In Java tutti gli errori API estendono la classe FirebaseException . È possibile accedere al codice di errore, al messaggio di errore e alla risposta HTTP facoltativa da questa classe base.

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

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

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

Le API che espongono codici di errore del servizio forniscono sottoclassi specifiche dell'API di FirebaseException . Ad esempio, tutti i metodi pubblici nell'API FirebaseAuth vengono dichiarati per generare istanze di FirebaseAuthException . È possibile accedere al codice di errore del servizio da questa classe derivata.

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

Pitone

In Python tutti gli errori API estendono la exceptions.FirebaseError . È possibile accedere al codice di errore, al messaggio di errore e alla risposta HTTP facoltativa da questa classe base.

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

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

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

Inoltre, Python Admin SDK offre classi derivate separate per ciascun codice di errore. Li chiamiamo classi di errore della piattaforma .

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Puoi rilevare FirebaseError nel tuo codice e controllarne il code oppure eseguire un controllo isinstance() rispetto a una classe di errore della piattaforma. Oppure puoi scrivere codice per individuare direttamente tipi di errori specifici della piattaforma. È probabile che quest'ultimo approccio dia luogo a un codice di gestione degli errori più leggibile.

Le API che espongono i codici di errore del servizio forniscono sottoclassi specifiche dell'API delle classi di errore della piattaforma. Ad esempio, tutti i metodi pubblici nel modulo auth possono generare tipi di errore specifici dell'API come auth.UserNotFoundError e auth.ExpiredIdTokenError .

class UserNotFoundError(exceptions.NotFoundError):
    # …

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

Andare

L'SDK Go Admin fornisce un pacchetto errorutils che contiene una serie di funzioni che consentono di verificare i codici di errore.

package errorutils

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

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

Il messaggio di errore è semplicemente la stringa restituita dalla funzione Error() di un errore. È possibile accedere alla risposta HTTP facoltativa chiamando la funzione errorutils.HTTPResponse() , che restituisce un *http.Response .

È sicuro passare nil o qualsiasi altro valore di errore alle funzioni di controllo degli errori nel pacchetto errorutils . Restituiscono true se l'argomento di input contiene effettivamente il codice di errore in questione e restituiscono false per tutto il resto. La funzione HTTPResponse() ha un comportamento simile, tranne che restituisce nil invece di false .

Le API che espongono codici di errore del servizio forniscono funzioni di controllo degli errori specifiche dell'API nei pacchetti corrispondenti. Ad esempio, il pacchetto auth fornisce le funzioni IsUserNotFound() e IsExpiredIDTokenError() .

.NETTO

In .NET tutti gli errori API estendono la classe FirebaseException . È possibile accedere al codice di errore della piattaforma, al messaggio di errore e alla risposta HTTP facoltativa da questa classe base.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

Le API che espongono codici di errore del servizio forniscono sottoclassi specifiche dell'API di FirebaseException . Ad esempio, tutti i metodi pubblici nell'API FirebaseAuth vengono dichiarati per generare istanze di FirebaseAuthException . È possibile accedere al codice di errore del servizio da questa classe derivata.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Codici di errore della piattaforma

I codici di errore sono comuni a tutti i servizi Firebase e Google Cloud Platform. La tabella seguente illustra tutti i possibili codici di errore della piattaforma. Si tratta di un elenco stabile e si prevede che rimanga invariato per un lungo periodo.

ARGOMENTO_INVALID Il client ha specificato un argomento non valido.
FAILED_PRECONDITION La richiesta non può essere eseguita nello stato corrente del sistema, ad esempio eliminando una directory non vuota.
FUORI DAL LIMITE Il client ha specificato un intervallo non valido.
NON AUTENTICATO Richiesta non autenticata a causa di token OAuth mancante, non valido o scaduto.
PERMESSO NEGATO Il client non dispone di autorizzazioni sufficienti. Ciò può verificarsi perché il token OAuth non ha gli ambiti corretti, il client non dispone dell'autorizzazione o l'API non è stata abilitata per il progetto client.
NON TROVATO Risorsa specificata non trovata oppure la richiesta è stata respinta per motivi non dichiarati come l'inserimento nella whitelist.
CONFLITTO Conflitto di concorrenza, ad esempio conflitto di lettura-modifica-scrittura. Utilizzato solo da alcuni servizi legacy. La maggior parte dei servizi utilizza ABORTED o ALREADY_EXISTS invece di questo. Fare riferimento alla documentazione specifica del servizio per vedere quale gestire nel codice.
ABORTITO Conflitto di concorrenza, ad esempio conflitto di lettura-modifica-scrittura.
ESISTE GIÀ La risorsa che un client ha tentato di creare esiste già.
RISORSA_ESAURITO Esaurimento della quota di risorse o raggiungimento del limite di velocità.
ANNULLATO Richiesta annullata dal cliente.
PERDITA DI DATI Perdita irrecuperabile o danneggiamento dei dati. Il client dovrebbe segnalare l'errore all'utente.
SCONOSCIUTO Errore del server sconosciuto. Solitamente è un bug del server.

Questo codice di errore viene assegnato anche agli errori di analisi della risposta locale (unmarshal) e a un'ampia gamma di altri errori I/O di basso livello che non sono facilmente diagnosticabili.

INTERNO Errore interno del server. Solitamente è un bug del server.
NON DISPONIBILE Servizio non disponibile. In genere il server è temporaneamente inattivo.

Questo codice di errore viene assegnato anche agli errori della rete locale (connessione rifiutata, nessun percorso verso l'host).

SCADENZA_SCADATA Scadenza richiesta superata. Ciò accadrà solo se il chiamante imposta una scadenza più breve della scadenza predefinita dell'API di destinazione (ovvero la scadenza richiesta non è sufficiente affinché il server elabori la richiesta) e la richiesta non è stata completata entro la scadenza.

Questo codice di errore viene assegnato anche alla connessione locale e ai timeout di lettura.

La maggior parte delle API può generare solo un sottoinsieme dei codici di errore sopra indicati. In ogni caso, non è previsto che tu gestisca esplicitamente tutti questi codici di errore durante l'implementazione dei gestori di errori. La maggior parte delle applicazioni sarebbe interessata solo a 1-2 codici di errore specifici e tratterebbe tutto il resto come un errore generico e irreversibile.

Codici di errore specifici del servizio

Aut. Firebase

CERTIFICATO_FETCH_FAILED Impossibile recuperare i certificati a chiave pubblica richiesti per verificare un JWT (token ID o cookie di sessione).
EMAIL GIÀ ESISTENTE Esiste già un utente con l'e-mail fornita.
EXPIRED_ID_TOKEN Il token ID specificato per verifyIdToken() è scaduto.
EXPIRED_SESSION_COOKIE Il cookie di sessione specificato per verifySessionCookie() è scaduto.
INVALID_DYNAMIC_LINK_DOMAIN Il dominio di collegamento dinamico fornito non è configurato o autorizzato per il progetto corrente. Relativo alle API di collegamento delle azioni e-mail.
TOKEN_ID_VALIDO Il token ID specificato per verifyIdToken() non è valido.
INVALID_SESSION_COOKIE Il cookie di sessione specificato per verifySessionCookie() non è valido.
TELEFONO_NUMBER_ALREADY_EXISTS Esiste già un utente con il numero di telefono fornito.
REVOKED_ID_TOKEN Il token ID specificato per verifyIdToken() viene revocato.
REVOKED_SESSION_COOKIE Il cookie di sessione specificato per verifySessionCookie() è scaduto.
UNAUTHORIZED_CONTINUE_URL Il dominio dell'URL di continuazione non è nella whitelist. Relativo alle API di collegamento delle azioni e-mail.
UTENTE NON TROVATO Nessun record utente trovato per l'identificatore specificato.

Messaggistica cloud Firebase

TERZA_PARTY_AUTH_ERROR Il certificato APN o la chiave API di autenticazione push Web non erano validi o mancavano.
ARGOMENTO_INVALID Uno o più argomenti specificati nella richiesta non erano validi.
INTERNO Errore interno del server.
QUOTA SUPERATA Limite di invio superato per la destinazione del messaggio.
SENDER_ID_MISMATCH L'ID mittente autenticato è diverso dall'ID mittente per il token di registrazione. Ciò di solito significa che il mittente e il token di registrazione di destinazione non si trovano nello stesso progetto Firebase.
NON DISPONIBILE Il servizio Cloud Messaging è temporaneamente non disponibile.
NON REGISTRATO La registrazione dell'istanza dell'app da FCM è stata annullata. Ciò di solito significa che il token di registrazione del dispositivo utilizzato non è più valido ed è necessario utilizzarne uno nuovo.

Nuovi tentativi automatici

L'SDK Admin riprova automaticamente determinati errori prima di esporli agli utenti. In generale, vengono ritentati in modo trasparente i seguenti tipi di errori:

  • Tutti gli errori API risultanti dalle risposte HTTP 503 (servizio non disponibile).
  • Alcuni errori API risultanti dalle risposte HTTP 500 (errore interno del server).
  • La maggior parte degli errori I/O di basso livello (connessione rifiutata, connessione ripristinata, ecc.).

L'SDK ritenterà ciascuno degli errori precedenti fino a 5 volte (il tentativo originale + 4 tentativi) con backoff esponenziale. Se lo desideri, puoi implementare i tuoi meccanismi di ripetizione a livello di applicazione, ma in genere non è necessario.

Supporto Riprova dopo

Le implementazioni Go e .NET di Admin SDK includono il supporto per la gestione dell'intestazione HTTP Retry-After . Cioè, se la risposta di errore inviata dai server back-end contiene l'intestazione Retry-After standard, l'SDK la rispetterà quando si riprova, purché la durata di attesa specificata non sia molto lunga. Se l'intestazione Retry-After indica una durata di attesa molto lunga, l'SDK ignorerà i nuovi tentativi e genererà l'errore API appropriato.

Python Admin SDK attualmente non supporta l'intestazione Retry-After e supporta solo il semplice backoff esponenziale.

Esempi di gestione degli errori API

Implementazione di un gestore di errori generico

Nella maggior parte dei casi ciò che serve è un gestore di errori generico che rilevi un'ampia gamma di errori per impedire l'interruzione imprevista del flusso del programma a causa di un errore API. Tali gestori di errori di solito registrano semplicemente gli errori a scopo di controllo o richiamano qualche altra routine di gestione degli errori predefinita per tutti gli errori API riscontrati. Non sono necessariamente interessati ai diversi codici di errore o ai motivi che potrebbero aver causato l'errore.

Giava

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

Pitone

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}')

Andare

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

performPrivilegedOperation(token)

.Netto

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

Controllo dei codici di errore

In alcuni casi, potresti voler controllare i codici di errore esatti e richiamare diverse routine di gestione degli errori sensibili al contesto. Nell'esempio seguente abbiamo un gestore errori che registra messaggi di errore più specifici in base al codice di errore del servizio.

Giava

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());
  }
}

Pitone

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}')

Andare

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)

.Netto

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}");
  }
}

Ecco un altro esempio in cui controlliamo sia i codici di errore di livello superiore che quelli di servizio:

Giava

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());
  }
}

Pitone

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}')

Andare

_, 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
}

.Netto

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}");
  }
}

Accesso alla risposta HTTP

In alcuni rari casi potresti voler controllare la risposta all'errore HTTP restituita da un servizio backend ed eseguire su di essa alcune azioni di gestione degli errori. L'Admin SDK espone sia le intestazioni che il contenuto di queste risposte agli errori. Il contenuto della risposta viene solitamente restituito come una stringa o una sequenza di byte grezzi e può essere analizzato in qualsiasi formato di destinazione necessario.

Giava

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());
  }
}

Pitone

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}')

Andare

_, 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
}

.Netto

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}");
  }
}