Yönetici SDK'sı hata işleme

uygulamaları

Yönetici SDK'sı hataları iki kategoriye ayrılır:

  1. Programlama hataları: Bunlar, kullanıcı uygulamasıdır. Çoğunlukla SDK'nın yanlış kullanımından kaynaklanır (null değerlerini kabul etmeyen bir yönteme null iletme) ve Firebase projesi veya SDK düzeyindeki diğer yapılandırma hataları (eksik) kimlik bilgileri, yanlış proje kimliği dizesi vb.).
  2. API hataları: Bunlar SDK uygulamasında oluşan çeşitli kurtarılabilir hataları içerir. Firebase arka uç hizmetlerinden kaynaklanan hatalar ve diğer geçici hataları (zaman aşımları gibi) oluşur.

Yönetici SDK'si, programlama hatalarını söz konusu platforma dayanıyor.

  • Java: IllegalArgumentException, NullPointerException örneklerini atar yerleşik çalışma zamanı hatası türü.
  • Python: ValueError, TypeError veya diğer yerleşik hata türlerinin örneklerini ortaya çıkarır.
  • Go: Genel bir hata döndürür.
  • .NET: ArgumentException, ArgumentNullException veya benzer bir yerleşik hata türü.

Çoğu durumda, programlama hatalarını açıkça ele almamanız gerekir. Bunun yerine programlama hatalarını tamamen önlemek için kodunuzu ve yapılandırmanızı düzeltmeniz gerekir. Şu Java snippet'ini ele alalım:

String uid = getUserInput();

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

getUserInput() yöntemi null veya boş dizeler döndürürse FirebaseAuth.getUser() API'si bir IllegalArgumentException gönderir. Şunun yerine: açık bir şekilde ele almazsanız, sorunu azaltmak için getUserInput() yöntemi hiçbir zaman geçersiz bir UID dizesi döndürmez. Değilse mümkün değilse, gerekli bağımsız değişken kontrolünü aşağıdaki şekilde kendi kodunuza uygulayın:

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

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

İlke olarak, hiçbir zaman programlama hatalarını yeniden denemeyin. Hataları hızlandırmaya izin verme anlamlandırması genellikle en iyi eylem planıdır çünkü sırasında ortaya çıkardığı programlama hatalarını ve yapılandırma hatalarını ortaya çıkarır. hemen çözülebilir. Bu bağlamda hatanın hızlı olması, hataların uygulamanızdaki genel bir hata işleyiciye aktarılır veya yalnızca günlüğe kaydedilir. gerçekleştirildikten sonra mevcut yürütme akışının sonlandırılması (uygulama kilitlenmek zorunda kalmamalıdır). Genel olarak, en iyi uygulamalarını ele almak için kullanabileceğiniz yararlı bahsedeceğim. Bu tür bir sınıfla doğru şekilde başa çıkmak için çoğu zaman tek başına hatalar.

Genellikle hata giderme çalışmalarınızın büyük kısmı API'yi hataları. Bu hatalardan bazıları kurtarılabilir niteliktedir. Örneğin, geçici olarak kullanılamamaktadır ve hatta bu süre içinde bazılarının normal program yürütme akışı (ör. geçersiz veya süresi dolmuş kimlik jetonlarını algılama) Bu kılavuzun geri kalanında, Admin SDK'nın bu tür API hatalarını nasıl temsil ettiği açıklanmaktadır. kullanıma sunulan çeşitli seçeneklere değineceğiz.

Bir API hatasının yapısı

API hatası aşağıdaki bileşenlerden oluşur:

  1. Hata kodu
  2. Hata mesajı
  3. Hizmet hata kodu (İsteğe bağlı)
  4. HTTP yanıtı (İsteğe bağlı)

Her API hatasının bir hata kodu ve hata mesajı içereceği garanti edilir. Bazı API hataları, API'ye özgü hizmet hata kodu da içerir. hataya neden olan URL'dir. Firebase Auth tarafından oluşturulan bazı hatalar, API, Firebase Auth'a özgü bir hizmet hata kodu içerir. Hata bir arka uç hizmetinden gelen HTTP hata yanıtının sonucuydu; API hatası karşılık gelen HTTP yanıtını da içerir. Bu metrik, tam başlıkları ve içeriğini görebilirsiniz. Bu bilgiler yalnızca günlük kaydı oluşturma veya daha gelişmiş hata işleme mantığı uygulama.

Node.js dışındaki tüm Yönetici SDK'sı uygulamaları, erişimi etkinleştiren API'ler sağlar yukarıdaki API hatalarının bileşenlerine göz atın.

Dile göre hata türleri ve API'ler

Java

Java'da tüm API hataları FirebaseException sınıfını genişletir. Buradan: hata kodu, hata mesajı ve bu temel sınıftan gelen isteğe bağlı HTTP yanıtı.

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

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

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

Hizmet hata kodlarını gösteren API'ler, API'ye özgü alt sınıfları FirebaseException Örneğin, FirebaseAuth API'deki herkese açık tüm yöntemler FirebaseAuthException örnekleri attığı açıklandı. Buradan: hizmet hatası kodu girin.

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

Python

Python'da tüm API hataları exceptions.FirebaseError uzantısını genişletin sınıfını kullanır. Hata koduna, hata mesajına ve isteğe bağlı HTTP yanıtı ekleyin.

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

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

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

Ayrıca, Python Admin SDK'sı her hata kodu için ayrı türetilmiş sınıflar sunar. Bunlara platform hata sınıfları diyoruz.

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Kodunuzda FirebaseError öğesini yakalayıp code değerini kontrol edebilirsiniz veya platform hatası sınıfına göre isinstance() kontrolü gerçekleştirin. İsterseniz belirli platform hatası türlerini doğrudan yakalamak için kullanılabilecek kodlardan oluşur. İkinci yaklaşım ise daha okunabilir hata işleme koduna neden olabilir.

Hizmet hata kodlarını gösteren API'ler, platformun API'ye özel alt sınıflarını sağlar hata sınıflarından bahsedeceğiz. Örneğin, auth modülündeki tüm herkese açık yöntemler auth.UserNotFoundError ve auth.ExpiredIdTokenError.

class UserNotFoundError(exceptions.NotFoundError):
    # …

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

Go

Go Yönetici SDK'sı,errorutils özellikleri test etmenize olanak tanır.

package errorutils

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

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

Hata mesajı, basit bir ifadenin Error() işlevi tarafından döndürülen hatası. İsteğe bağlı HTTP yanıtına errorutils.HTTPResponse() işlevi, *http.Response döndürür.

Hata kontrolüne nil veya başka bir hata değeri güvenle iletilebilir errorutils paketindeki işlevlerin kullanımı. Giriş bağımsız değişkeni true aslında söz konusu hata kodunu içerir ve her şey için false değerini döndürür else. HTTPResponse() işlevi benzer davranışa sahiptir ancak false yerine nil.

Hizmet hata kodlarını gösteren API'ler, API'ye özel hata kontrolü sağlar işlevlerine karşılık gelir. Örneğin, auth paketi IsUserNotFound() ve IsExpiredIDTokenError() işlevlerini sağlar.

.NET

.NET'te tüm API hataları FirebaseException uzantısını genişletir sınıfını kullanır. Buradan: platform hata kodu, hata mesajı ve bu tabandaki isteğe bağlı HTTP yanıtı sınıfını kullanır.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

Hizmet hata kodlarını gösteren API'ler, API'ye özgü alt sınıfları FirebaseException Örneğin, FirebaseAuth API'deki herkese açık tüm yöntemler FirebaseAuthException örneklerini bildirdiği açıklandı. Buradan: hizmet hatası kodu girin.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Platform hata kodları

Hata kodları tüm Firebase ve Google Cloud Platform hizmetlerinde yaygındır. Aşağıdaki tabloda, olası tüm platform hata kodları özetlenmektedir. Bu, uzun süre değişmeden kalması beklenmektedir.

GEÇERSİZ_BAĞ_DEĞİŞKEN İstemci, geçersiz bağımsız değişken belirtti.
FAILED_PRECONDITION İstek, geçerli sistem durumunda yürütülemez (örneğin, boş olmayan bir dizinin silinmesi).
ARALIK DIŞI İstemci, geçersiz aralık belirtti.
KİMLİK DOĞRULAMADI Eksik, geçersiz veya süresi dolmuş OAuth jetonundan dolayı isteğin kimliği doğrulanamadı.
PERMISSION_DENIED İstemci, gerekli izne sahip değil. Bu durum, OAuth jetonunun doğru kapsamlara sahip olmaması, istemcinin izne sahip olmaması veya API'nin istemci projesi için etkinleştirilmemesi nedeniyle ortaya çıkabilir.
NOT_FOUND Belirtilen kaynak bulunamadı veya istek, izin verilenler listesine ekleme gibi açıklanmayan nedenlerden dolayı reddedildi.
ÇAKIŞMA NOKTASI Eşzamanlılık uyuşmazlığı (ör. okuma-değiştirme-yazma uyuşmazlığı). Yalnızca birkaç eski hizmet tarafından kullanılır. Çoğu hizmet bunun yerine ABORTED veya ALREADY_EXISTS kullanır. Kodunuzda hangisini kullanacağınızı görmek için hizmete özgü belgelere bakın.
İPTAL EDİLDİ Eşzamanlılık uyuşmazlığı (ör. okuma-değiştirme-yazma uyuşmazlığı).
ALREADY_EXISTS Bir istemcinin oluşturmaya çalıştığı kaynak zaten mevcut.
RESOURCE_EXHAUSTED Kaynak kotası kalmadı veya hız sınırlamasına yaklaşılıyor.
İPTAL EDİLDİ İstek, istemci tarafından iptal edildi.
VERİ_KAYBI Kurtarılamaz veri kaybı veya veri bozulması. İstemci hatayı kullanıcıya bildirmelidir.
BİLİNMİYOR Bilinmeyen sunucu hatası. Bu genellikle bir sunucu hatasıdır.

Bu hata kodu, yerel yanıt ayrıştırma (marshal) hatalarına ve kolayca teşhis edilemeyen çok sayıda düşük düzeyli G/Ç hatasına da atanır.

DAHİLİ Dahili sunucu hatası. Bu genellikle bir sunucu hatasıdır.
KULLANILAMIYOR Hizmet kullanılamıyor. Genellikle sunucu geçici olarak kapalıdır.

Bu hata kodu, yerel ağ hatalarına da atanır (bağlantı reddedildi, ana makineye yol yok).

DEADLINE_EXCEEDED İstek bitiş tarihi aşıldı. Bu durum yalnızca çağrıyı yapan kişi, hedef API'nin varsayılan son tarihinden daha kısa bir son tarih belirlerse (ör. istenen son tarih, sunucunun isteği işlemesi için yeterli değil) ve istek son tarih içinde tamamlanmamıştır.

Bu hata kodu, yerel bağlantı ve okuma zaman aşımlarına da atanır.

Çoğu API, yukarıdaki hata kodlarının yalnızca bir alt kümesine neden olabilir. Her koşulda, tüm bu hata kodlarını açıkça işlemesi beklenmez. yardımcı olur. Çoğu uygulama yalnızca 1-2 belirli hata kodu kullanın ve diğer her şeyi genel, düzeltilemeyen olarak ele alın. başarısız olur.

Hizmete özgü hata kodları

Firebase Auth

CERTIFICATE_FETCH_BAŞARISIZ JWT'yi (kimlik jetonu veya oturum çerezi) doğrulamak için gereken ortak anahtar sertifikaları getirilemedi.
EMAIL_ALREADY_EXISTS Sağlanan e-postaya sahip bir kullanıcı zaten var.
SÜRESİ_DOLMUŞ_ID_TOKEN verifyIdToken() için belirtilen kimlik jetonunun süresi doldu.
SÜRESİ_DOLMUŞ_SESSION_ÇABA verifySessionCookie() için belirtilen oturum çerezinin süresi doldu.
GEÇERSİZ_DYNAMIC_LINK_ALAN Sağlanan dinamik bağlantı alan adı, geçerli proje için yapılandırılmadı veya yetkilendirilmedi. E-posta işlemi bağlantısı API'leriyle ilgili.
GEÇERSİZ_KİMLİĞİ_TOKEN verifyIdToken() için belirtilen kimlik jetonu geçersiz.
GEÇERSİZ_SESSION_COOKIE verifySessionCookie() için belirtilen oturum çerezi geçersiz.
PHONE_NUMBER_ALREADY_EXISTS Sağlanan telefon numarasına sahip bir kullanıcı zaten var.
REVOKED_ID_TOKEN verifyIdToken() için belirtilen kimlik jetonu iptal edilmiş.
REVOKED_SESSION_COOKIE: REVOKED_SESSION_COOKIE verifySessionCookie() için belirtilen oturum çerezinin süresi doldu.
YETKİLİ_D_DEVAMI_URL Devam URL'sinin alanı izin verilenler listesine eklenmemiş. E-posta işlemi bağlantısı API'leriyle ilgili.
KULLANICI_NOT_FOUND Belirtilen tanımlayıcı için kullanıcı kaydı bulunamadı.

Firebase Cloud Messaging

ÜÇÜNCÜ_TARAF_AUTH_HATASI APNs sertifikası veya web push kimlik doğrulaması API anahtarı geçersiz ya da eksik.
GEÇERSİZ_BAĞ_DEĞİŞKEN İstekte belirtilen bir veya daha fazla bağımsız değişken geçersiz.
DAHİLİ Dahili sunucu hatası.
QUOTA_EXCEEDED İleti hedefi için gönderme sınırı aşıldı.
SENDER_ID_MISMATCH Kimliği doğrulanmış gönderen kimliği, kayıt jetonunun gönderen kimliğinden farklıdır. Bu, genellikle gönderen ve hedef kayıt jetonunun aynı Firebase projesinde olmadığı anlamına gelir.
KULLANILAMIYOR Cloud Messaging hizmeti geçici olarak kullanılamıyor.
KAYDEDİLMEMİŞ Uygulama örneğinin FCM'deki kaydı iptal edildi. Bu genellikle, kullanılan cihaz kayıt jetonunun artık geçerli olmadığı ve yeni bir jeton kullanılması gerektiği anlamına gelir.

Otomatik yeniden denemeler

Yönetici SDK'si, belirli hataları ortaya çıkarmadan önce otomatik olarak yeniden dener değer katmış olacaksınız. Genel olarak, aşağıdaki hata türleri şeffaf bir şekilde yeniden denenir:

  • HTTP 503 (Hizmet Kullanılamıyor) yanıtlarından kaynaklanan tüm API hataları.
  • HTTP 500 (Dahili Sunucu Hatası) yanıtlarından kaynaklanan bazı API hataları.
  • Çoğu alt düzey G/Ç hatası (bağlantı reddedildi, bağlantı sıfırlandı vb.).

SDK, yukarıdaki hataların her birini en fazla 5 kez yeniden dener (orijinal deneme + 4 yeniden deneme) eklendi. Kendi yeniden deneme işleminizi uygulayabilirsiniz mekanizmaları da ekleyebilirsiniz, ancak bu pek çok gereklidir.

Yeniden dene - Destek sonrası

Admin SDK'nın Go ve .NET uygulamaları, HTTP Retry-After üstbilgisi işleniyor. Yani, Arkadaş Bitkiler tarafından gönderilen hata yanıtı arka uç sunucuları standart Retry-After üstbilgisini içeriyorsa SDK, belirtilen bekleme süresi çok uzun olmadığı sürece yeniden denemede buna dikkat edin. gerekir. Retry-After başlığı çok uzun bir bekleme süresi belirtiyorsa SDK yeniden denemeleri atlar ve uygun API hatasını verir.

Python Admin SDK'sı şu anda Retry-After üst bilgisini desteklememektedir ve yalnızca basit bir üstel geri yüklemeyi destekler.

API hatası işleme örnekleri

Genel bir hata işleyici uygulama

Çoğu durumda, istediğiniz, geniş bir alanı yakalayan genel bir hata işleyici program akışının beklenmedik bir şekilde sonlandırılmasını önlemek için, API hatası. Bu tür hata işleyiciler genellikle hataları denetleme amacıyla günlüğe kaydeder. veya karşılaşılan tüm API için başka bir varsayılan hata işleme rutinini çağırın hatalar. Her zaman farklı hata kodlarıyla veya farklı hata kodlarıyla hataya yol açmış olabilir.

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

Go

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

performPrivilegedOperation(token)

.Net

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

Hata kodlarını kontrol etme

Bazı durumlarda tam olarak hata kodlarını inceleyerek bağlama duyarlı farklı hata işleme rutinleri oluşturabilirsiniz. Aşağıdaki örnekte, cihaza göre daha spesifik hata mesajlarını günlüğe kaydeden bir hata işleyici hizmet hata kodu.

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

Go

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)

.Net

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

Aşağıda, hem üst düzey hem de hizmet hata kodlarını kontrol ettiğimiz başka bir örnek verilmiştir:

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

Go

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

.Net

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

HTTP yanıtına erişme

Nadiren de olsa, bir sağlayıcı tarafından döndürülen HTTP hatası yanıtını bir arka uç hizmetini ve bu hizmet üzerinde bazı hata işleme işlemleri gerçekleştirmenizi öneririz. Yönetici SDK'sı hem üstbilgilerini hem de içeriğini gösterir. Yanıt içerik genellikle bir dize veya ham bayt dizisi olarak döndürülür ve hedef biçimine ayrıştırılır.

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

Go

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

.Net

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