Obsługa błędów pakietu Admin SDK

Błędy pakietu Admin SDK dzielą się na dwie kategorie:

  1. Błędy programistyczne: Są to błędy programowania i konfiguracji w aplikacji użytkownika. Występują one najczęściej z powodu nieprawidłowego użycia pakietu SDK (takiego jak przekazanie wartości null do metody, która nie akceptuje wartości null ) i innych błędów konfiguracyjnych na poziomie projektu Firebase lub pakietu SDK (brakujące dane uwierzytelniające, nieprawidłowy ciąg identyfikatora projektu itp. NA).
  2. Błędy API: obejmują one różne możliwe do naprawienia błędy występujące w implementacji SDK, wszystkie błędy pochodzące z usług zaplecza Firebase i inne błędy przejściowe (takie jak przekroczenia limitu czasu), które mogą wystąpić podczas wykonywania wywołań RPC.

Pakiet Admin SDK sygnalizuje błędy programowania , zgłaszając błąd natywny dla danej platformy.

  • Java: Zgłasza wystąpienia IllegalArgumentException , NullPointerException lub podobnego wbudowanego typu błędu środowiska wykonawczego.
  • Python: wywołuje instancje ValueError , TypeError lub innego wbudowanego typu błędu.
  • Idź: Zwraca ogólny błąd.
  • .NET: Zgłasza wystąpienia ArgumentException , ArgumentNullException lub podobnego wbudowanego typu błędu.

W większości sytuacji nie należy bezpośrednio zajmować się błędami programistycznymi. Zamiast tego powinieneś naprawić swój kod i konfigurację, aby całkowicie uniknąć błędów programistycznych. Rozważ następujący fragment kodu Java:

String uid = getUserInput();

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

Jeśli metoda getUserInput() zwróci ciągi null lub puste, funkcja API FirebaseAuth.getUser() zgłosi wyjątek IllegalArgumentException . Zamiast zajmować się tym bezpośrednio, możesz złagodzić problem, upewniając się, że metoda getUserInput() nigdy nie zwraca nieprawidłowego ciągu UID. Jeśli nie jest to możliwe, zaimplementuj niezbędne sprawdzanie argumentów we własnym kodzie w następujący sposób:

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

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

Z zasady nigdy nie należy ponawiać błędów programistycznych. Zezwolenie na niezawodną semantykę błędów programistycznych jest często najlepszym sposobem działania, ponieważ ujawnia błędy programistyczne i konfiguracyjne podczas programowania, gdzie można je szybko naprawić. W tym kontekście odporność na awarie może oznaczać pozwolenie błędom na propagację do globalnego modułu obsługi błędów w aplikacji lub po prostu rejestrowanie ich w celach audytu, po czym następuje zakończenie bieżącego przepływu wykonywania (aplikacja nie powinna ulegać awarii). Ogólnie rzecz biorąc, postępuj zgodnie z najlepszymi praktykami obsługi błędów swojego języka programowania i struktury aplikacji. Samo to często wystarcza, aby poprawnie uporać się z tą klasą błędów.

Zwykle większość wysiłków związanych z obsługą błędów skupia się na obsłudze błędów interfejsu API . Niektóre z tych błędów można naprawić, na przykład błędy wynikające z tymczasowo niedostępnej usługi, a niektóre można nawet przewidzieć podczas normalnego wykonywania programu, na przykład podczas wykrycia nieprawidłowych lub wygasłych tokenów identyfikacyjnych. W pozostałej części tego przewodnika opisano, w jaki sposób pakiet Admin SDK reprezentuje takie błędy interfejsu API oraz różne dostępne opcje ich obsługi.

Struktura błędu API

Błąd API składa się z następujących elementów:

  1. Kod błędu
  2. Komunikat o błędzie
  3. Kod błędu serwisowego (opcjonalnie)
  4. Odpowiedź HTTP (opcjonalnie)

Gwarantujemy, że każdy błąd API będzie zawierał kod błędu i komunikat o błędzie. Niektóre błędy interfejsu API zawierają również kod błędu usługi specyficzny dla interfejsu API, który wygenerował błąd. Na przykład niektóre błędy generowane przez interfejs API Firebase Auth zawierają kod błędu usługi specyficzny dla Firebase Auth. Jeśli błąd był wynikiem odpowiedzi na błąd HTTP z usługi zaplecza, błąd interfejsu API zawiera również odpowiednią odpowiedź HTTP. Można to wykorzystać do sprawdzenia dokładnych nagłówków i zawartości oryginalnej odpowiedzi, co jest przydatne do debugowania, rejestrowania lub implementowania bardziej wyrafinowanej logiki obsługi błędów.

Wszystkie implementacje Admin SDK z wyjątkiem Node.js udostępniają interfejsy API umożliwiające dostęp do powyższych składników błędów interfejsu API.

Typy błędów i interfejsy API według języka

Jawa

W Javie wszystkie błędy API rozszerzają klasę FirebaseException . Z tej klasy bazowej można uzyskać dostęp do kodu błędu, komunikatu o błędzie i opcjonalnej odpowiedzi HTTP.

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

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

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

Interfejsy API, które ujawniają kody błędów usług, udostępniają specyficzne dla interfejsu API podklasy FirebaseException . Na przykład wszystkie metody publiczne w interfejsie API FirebaseAuth są deklarowane jako zgłaszające instancje FirebaseAuthException . Dostęp do kodu błędu usługi można uzyskać z tej klasy pochodnej.

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

Pyton

W Pythonie wszystkie błędy API rozszerzają klasę exceptions.FirebaseError . Z tej klasy bazowej można uzyskać dostęp do kodu błędu, komunikatu o błędzie i opcjonalnej odpowiedzi HTTP.

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

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

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

Co więcej, pakiet Python Admin SDK oferuje osobne klasy pochodne dla każdego kodu błędu. Nazywamy je klasami błędów platformy .

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Możesz albo złapać FirebaseError w swoim kodzie i sprawdzić jego code , albo wykonać kontrolę isinstance() pod kątem klasy błędów platformy. Możesz też napisać kod, aby bezpośrednio wychwytywać określone typy błędów platformy. To drugie podejście prawdopodobnie spowoduje bardziej czytelny kod obsługi błędów.

Interfejsy API ujawniające kody błędów usług udostępniają specyficzne dla interfejsu API podklasy klas błędów platformy. Na przykład wszystkie metody publiczne w module auth mogą zgłaszać typy błędów specyficzne dla interfejsu API, takie jak auth.UserNotFoundError i auth.ExpiredIdTokenError .

class UserNotFoundError(exceptions.NotFoundError):
    # …

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

Iść

Pakiet Go Admin SDK udostępnia pakiet errorutils , który zawiera szereg funkcji umożliwiających testowanie kodów błędów.

package errorutils

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

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

Komunikat o błędzie to po prostu ciąg znaków zwrócony przez funkcję Error() błędu. Dostęp do opcjonalnej odpowiedzi HTTP można uzyskać, wywołując funkcję errorutils.HTTPResponse() , która zwraca wartość *http.Response .

Bezpieczne jest przekazanie nil lub dowolnej innej wartości błędu do funkcji sprawdzania błędów w pakiecie errorutils . Zwracają true , jeśli argument wejściowy faktycznie zawiera kod błędu, o którym mowa, i zwracają false w przypadku wszystkich pozostałych parametrów. Funkcja HTTPResponse() zachowuje się podobnie, z tą różnicą, że zwraca nil zamiast false .

Interfejsy API ujawniające kody błędów usług udostępniają w odpowiednich pakietach funkcje sprawdzania błędów specyficzne dla interfejsu API. Na przykład pakiet auth udostępnia funkcje IsUserNotFound() i IsExpiredIDTokenError() .

.INTERNET

W .NET wszystkie błędy API rozszerzają klasę FirebaseException . Z tej klasy bazowej można uzyskać dostęp do kodu błędu platformy, komunikatu o błędzie i opcjonalnej odpowiedzi HTTP.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

Interfejsy API, które ujawniają kody błędów usług, udostępniają specyficzne dla interfejsu API podklasy FirebaseException . Na przykład wszystkie metody publiczne w interfejsie API FirebaseAuth są zadeklarowane w celu zgłaszania wystąpień FirebaseAuthException . Dostęp do kodu błędu usługi można uzyskać z tej klasy pochodnej.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Kody błędów platformy

Kody błędów są wspólne dla wszystkich usług Firebase i Google Cloud Platform. W poniższej tabeli przedstawiono wszystkie możliwe kody błędów platformy. Jest to lista stabilna i oczekuje się, że pozostanie niezmieniona przez długi okres.

BŁĘDNY ARGUMENT Klient podał nieprawidłowy argument.
NIEUDANY_WARUNEK WSTĘPNY Żądania nie można wykonać w bieżącym stanie systemu, na przykład w celu usunięcia niepustego katalogu.
POZA ZAKRESEM Klient podał nieprawidłowy zakres.
NIEUWIERZYTELNIONE Żądanie nie zostało uwierzytelnione z powodu brakującego, nieprawidłowego lub wygasłego tokena OAuth.
PERMISSION_DENIED Klient nie ma wystarczających uprawnień. Może się tak zdarzyć, ponieważ token OAuth nie ma odpowiednich zakresów, klient nie ma uprawnień lub nie włączono interfejsu API dla projektu klienta.
NIE ZNALEZIONO Nie znaleziono określonego zasobu lub żądanie zostało odrzucone z nieujawnionych powodów, takich jak umieszczenie na białej liście.
KONFLIKT Konflikt współbieżności, taki jak konflikt odczytu, modyfikacji i zapisu. Używany tylko przez kilka starszych usług. Większość usług używa zamiast tego ABORTED lub ALREADY_EXISTS. Zapoznaj się z dokumentacją specyficzną dla usługi, aby zobaczyć, który z nich ma być obsługiwany w kodzie.
NIEDONOSZONY Konflikt współbieżności, taki jak konflikt odczytu, modyfikacji i zapisu.
JUŻ ISTNIEJE Zasób, który klient próbował utworzyć, już istnieje.
ZASOBY_WYKOŃCZONE Albo wyczerpał się limit zasobów, albo osiągnięto limit szybkości.
ODWOŁANY Zapytanie anulowane przez klienta.
UTRATA DANYCH Nieodwracalna utrata lub uszkodzenie danych. Klient powinien zgłosić błąd użytkownikowi.
NIEZNANY Nieznany błąd serwera. Typowy błąd serwera.

Ten kod błędu jest również przypisany do błędów analizy lokalnej odpowiedzi (unmarshal) i szerokiej gamy innych błędów we/wy niskiego poziomu, które nie są łatwe do zdiagnozowania.

WEWNĘTRZNY Wewnętrzny błąd serwera. Typowy błąd serwera.
NIEDOSTĘPNE Serwis niedostępny. Zazwyczaj serwer jest chwilowo wyłączony.

Ten kod błędu jest również przypisany do błędów sieci lokalnej (odmowa połączenia, brak trasy do hosta).

DEADLINE_EXCEEDED Przekroczono termin składania wniosków. Stanie się tak tylko wtedy, gdy wywołujący ustawi termin krótszy niż domyślny termin docelowego API (tj. żądany termin nie jest wystarczający, aby serwer mógł przetworzyć żądanie), a żądanie nie zostało zakończone w wyznaczonym terminie.

Ten kod błędu jest również przypisany do limitów czasu połączenia lokalnego i odczytu.

Większość interfejsów API może skutkować jedynie wyświetleniem podzbioru powyższych kodów błędów. W każdym razie nie oczekuje się, że będziesz jawnie obsługiwał wszystkie te kody błędów podczas implementowania programów obsługi błędów. Większość aplikacji byłaby zainteresowana tylko 1-2 konkretnymi kodami błędów, a wszystko inne traktowałaby jako ogólną, nieodwracalną awarię.

Kody błędów specyficzne dla usługi

Uwierzytelnianie Firebase

CERTIFICATE_FETCH_FAILED Nie udało się pobrać certyfikatów klucza publicznego wymaganych do zweryfikowania tokenu identyfikacyjnego lub pliku cookie sesji.
EMAIL JUŻ ISTNIEJE Użytkownik z podanym adresem e-mail już istnieje.
EXPIRED_ID_TOKEN Token identyfikatora określony w funkcji verifyIdToken() wygasł.
EXPIRED_SESSION_COOKIE Sesyjny plik cookie określony w verifySessionCookie() ii wygasł.
INVALID_DYNAMIC_LINK_DOMAIN Podana domena łącza dynamicznego nie jest skonfigurowana lub autoryzowana dla bieżącego projektu. Powiązane z interfejsami API linków do akcji e-mail.
INVALID_ID_TOKEN Token identyfikatora określony w funkcji verifyIdToken() jest nieprawidłowy.
INVALID_SESSION_COOKIE Plik cookie sesji określony w funkcji verifySessionCookie() jest nieprawidłowy.
PHONE_NUMBER_ALREADY_EXISTS Użytkownik o podanym numerze telefonu już istnieje.
REVOKED_ID_TOKEN Token identyfikacyjny określony w funkcji verifyIdToken() został unieważniony.
REVOKED_SESSION_COOKIE Plik cookie sesji określony w funkcji verifySessionCookie() wygasł.
UNAUTHORIZED_CONTINUE_URL Domena adresu URL kontynuacji nie znajduje się na białej liście. Powiązane z interfejsami API linków do akcji e-mail.
UŻYTKOWNIK NIE ZNALEZIONY Nie znaleziono rekordu użytkownika dla podanego identyfikatora.

Wiadomości w chmurze Firebase

THIRD_PARTY_AUTH_ERROR Certyfikat APNs lub klucz API uwierzytelniania web push jest nieprawidłowy lub go brak.
BŁĘDNY ARGUMENT Co najmniej jeden argument określony w żądaniu jest nieprawidłowy.
WEWNĘTRZNY Wewnętrzny błąd serwera.
MOZLIWA ILOŚĆ PRZEKROCZONA Przekroczono limit wysyłania dla celu wiadomości.
SENDER_ID_MISMATCH Uwierzytelniony identyfikator nadawcy różni się od identyfikatora nadawcy w tokenie rejestracyjnym. Zwykle oznacza to, że nadawca i docelowy token rejestracji nie znajdują się w tym samym projekcie Firebase.
NIEDOSTĘPNE Usługa Cloud Messaging jest tymczasowo niedostępna.
NIEZAREJESTROWANY Instancja aplikacji została wyrejestrowana z FCM. Zwykle oznacza to, że użyty token rejestracyjny urządzenia nie jest już ważny i należy użyć nowego.

Automatyczne ponowne próby

Pakiet Admin SDK automatycznie ponawia próbę wystąpienia określonych błędów, zanim udostępni je użytkownikom. Ogólnie rzecz biorąc, w przejrzysty sposób ponawiane są następujące typy błędów:

  • Wszystkie błędy API wynikające z odpowiedzi HTTP 503 (Usługa niedostępna).
  • Niektóre błędy API wynikające z odpowiedzi HTTP 500 (wewnętrzny błąd serwera).
  • Większość błędów we/wy niskiego poziomu (odmowa połączenia, reset połączenia itp.).

Zestaw SDK ponawia próbę każdego z powyższych błędów maksymalnie 5 razy (pierwotna próba + 4 próby) z wykładniczym wycofywaniem. Jeśli chcesz, możesz zaimplementować własne mechanizmy ponawiania prób na poziomie aplikacji, ale zazwyczaj nie jest to wymagane.

Spróbuj ponownie – po wsparciu

Implementacje Go i .NET pakietu Admin SDK obsługują obsługę nagłówka HTTP Retry-After . Oznacza to, że jeśli odpowiedź na błąd wysłana przez serwery zaplecza zawiera standardowy nagłówek Retry-After , zestaw SDK uwzględni go przy ponawianiu próby, o ile określony czas oczekiwania nie będzie zbyt długi. Jeśli nagłówek Retry-After wskazuje bardzo długi czas oczekiwania, zestaw SDK pominie ponowne próby i zgłosi odpowiedni błąd interfejsu API.

Pakiet SDK administratora języka Python nie obsługuje obecnie nagłówka Retry-After i obsługuje jedynie proste wycofywanie wykładnicze.

Przykłady obsługi błędów API

Implementacja ogólnej procedury obsługi błędów

W większości przypadków potrzebna jest ogólna obsługa błędów, która wychwytuje szeroki zakres błędów, aby zapobiec nieoczekiwanemu zakończeniu działania programu z powodu błędu API. Takie programy obsługi błędów zwykle po prostu rejestrują błędy w celach kontrolnych lub wywołują inną domyślną procedurę obsługi błędów dla wszystkich napotkanych błędów interfejsu API. Niekoniecznie interesują ich różne kody błędów lub przyczyny, które mogły spowodować błąd.

Jawa

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

Pyton

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

Iść

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

performPrivilegedOperation(token)

.Internet

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

Sprawdzanie kodów błędów

W niektórych przypadkach warto sprawdzić dokładne kody błędów i wywołać różne kontekstowe procedury obsługi błędów. W poniższym przykładzie mamy procedurę obsługi błędów, która rejestruje bardziej szczegółowe komunikaty o błędach w oparciu o kod błędu usługi.

Jawa

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

Pyton

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

Iść

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)

.Internet

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

Oto kolejny przykład, w którym sprawdzamy kody błędów najwyższego poziomu i kody usług:

Jawa

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

Pyton

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

Iść

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

.Internet

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

Dostęp do odpowiedzi HTTP

W niektórych rzadkich przypadkach możesz chcieć sprawdzić odpowiedź na błąd HTTP zwróconą przez usługę zaplecza i wykonać na niej pewne działania związane z obsługą błędów. Pakiet Admin SDK udostępnia zarówno nagłówki, jak i treść tych odpowiedzi na błędy. Treść odpowiedzi jest zwykle zwracana jako ciąg znaków lub surowa sekwencja bajtów i może zostać przeanalizowana do dowolnego niezbędnego formatu docelowego.

Jawa

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

Pyton

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

Iść

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

.Internet

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