Penanganan kesalahan Admin SDK

Kesalahan Admin SDK dibagi menjadi dua kategori:

  1. Kesalahan pemrograman: Ini adalah kesalahan pemrograman dan konfigurasi dalam aplikasi pengguna. Kesalahan ini sebagian besar terjadi karena penggunaan SDK yang salah (seperti meneruskan null ke metode yang tidak menerima nilai null ), dan kesalahan konfigurasi lainnya di tingkat proyek Firebase atau SDK (kredensial hilang, string ID proyek salah, dan sebagainya. pada).
  2. Kesalahan API: Ini mencakup berbagai kesalahan yang dapat dipulihkan yang terjadi dalam implementasi SDK, semua kesalahan yang berasal dari layanan backend Firebase, dan kesalahan sementara lainnya (seperti waktu habis) yang mungkin terjadi saat melakukan panggilan RPC.

Admin SDK memberi sinyal kesalahan pemrograman dengan menampilkan kesalahan asli platform yang dimaksud.

  • Java: Menampilkan instance IllegalArgumentException , NullPointerException , atau jenis kesalahan runtime bawaan serupa.
  • Python: Memunculkan instance ValueError , TypeError atau tipe kesalahan bawaan lainnya.
  • Go: Mengembalikan kesalahan umum.
  • .NET: Melemparkan instance ArgumentException , ArgumentNullException atau jenis kesalahan bawaan serupa.

Dalam sebagian besar situasi, Anda tidak boleh menangani kesalahan pemrograman secara eksplisit. Sebaliknya, Anda harus memperbaiki kode dan konfigurasi Anda untuk menghindari kesalahan pemrograman sama sekali. Pertimbangkan cuplikan Java berikut:

String uid = getUserInput();

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

Jika metode getUserInput() mengembalikan string null atau kosong, FirebaseAuth.getUser() API akan memunculkan IllegalArgumentException . Daripada menanganinya secara eksplisit, Anda dapat mengurangi masalah ini dengan memastikan metode getUserInput() tidak pernah mengembalikan string UID yang tidak valid. Jika itu tidak memungkinkan, terapkan argumen yang diperlukan untuk memeriksa kode Anda sendiri sebagai berikut:

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

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

Prinsipnya, jangan pernah mencoba lagi kesalahan pemrograman. Mengizinkan semantik fail-fast pada kesalahan pemrograman sering kali merupakan tindakan terbaik karena hal ini memperlihatkan bug pemrograman dan kesalahan konfigurasi selama pengembangan, sehingga kesalahan tersebut dapat segera diperbaiki. Fail-fast dalam konteks ini bisa berarti membiarkan kesalahan menyebar ke penangan kesalahan global dalam aplikasi Anda, atau hanya mencatatnya untuk tujuan audit yang diikuti dengan penghentian alur eksekusi saat ini (aplikasi tidak harus mogok). Secara umum, ikuti praktik terbaik penanganan kesalahan bahasa pemrograman dan kerangka aplikasi Anda. Ini saja sering kali cukup untuk menangani jenis kesalahan ini dengan benar.

Biasanya, sebagian besar upaya penanganan kesalahan Anda akan berfokus pada penanganan kesalahan API . Beberapa dari kesalahan ini dapat dipulihkan, seperti kesalahan yang diakibatkan oleh layanan yang tidak tersedia untuk sementara, dan beberapa bahkan diantisipasi selama alur eksekusi program normal, seperti mendeteksi token ID yang tidak valid atau kedaluwarsa. Panduan selanjutnya menguraikan bagaimana Admin SDK merepresentasikan kesalahan API tersebut, dan berbagai opsi yang tersedia untuk menanganinya.

Struktur kesalahan API

Kesalahan API terdiri dari komponen berikut:

  1. Kode kesalahan
  2. Pesan eror
  3. Kode kesalahan layanan (Opsional)
  4. Respons HTTP (Opsional)

Setiap kesalahan API dijamin berisi kode kesalahan dan pesan kesalahan. Kesalahan API tertentu juga berisi kode kesalahan layanan yang khusus untuk API yang menghasilkan kesalahan tersebut. Misalnya, beberapa error yang dihasilkan oleh Firebase Auth API berisi kode error layanan yang khusus untuk Firebase Auth. Jika kesalahan tersebut disebabkan oleh respons kesalahan HTTP dari layanan backend, kesalahan API juga berisi respons HTTP yang sesuai. Ini dapat digunakan untuk memeriksa header dan konten yang tepat dari respons asli, yang berguna untuk melakukan debug, mencatat log, atau menerapkan logika penanganan kesalahan yang lebih canggih.

Semua implementasi Admin SDK kecuali Node.js menyediakan API yang memungkinkan akses komponen kesalahan API di atas.

Jenis kesalahan dan API menurut bahasa

Jawa

Di Java, semua kesalahan API memperluas kelas FirebaseException . Anda dapat mengakses kode kesalahan, pesan kesalahan, dan respons HTTP opsional dari kelas dasar ini.

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

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

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

API yang mengekspos kode kesalahan layanan menyediakan subkelas FirebaseException khusus API. Misalnya, semua metode publik di FirebaseAuth API dideklarasikan untuk menampilkan instance FirebaseAuthException . Anda dapat mengakses kode kesalahan layanan dari kelas turunan ini.

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

ular piton

Di Python, semua kesalahan API memperluas kelas exceptions.FirebaseError . Anda dapat mengakses kode kesalahan, pesan kesalahan, dan respons HTTP opsional dari kelas dasar ini.

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

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

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

Selain itu, Python Admin SDK menawarkan kelas turunan terpisah untuk setiap kode kesalahan. Kami menyebutnya sebagai kelas kesalahan platform .

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Anda dapat menangkap FirebaseError dalam kode Anda dan memeriksa code , atau melakukan pemeriksaan isinstance() terhadap kelas kesalahan platform. Atau Anda dapat menulis kode untuk langsung menangkap jenis kesalahan platform tertentu. Pendekatan terakhir kemungkinan besar akan menghasilkan kode penanganan kesalahan yang lebih mudah dibaca.

API yang mengekspos kode kesalahan layanan menyediakan subkelas kelas kesalahan platform khusus API. Misalnya, semua metode publik dalam modul auth dapat menampilkan jenis kesalahan khusus API seperti auth.UserNotFoundError dan auth.ExpiredIdTokenError .

class UserNotFoundError(exceptions.NotFoundError):
    # …

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

Pergi

Go Admin SDK menyediakan paket errorutils yang berisi serangkaian fungsi yang memungkinkan pengujian kode kesalahan.

package errorutils

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

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

Pesan kesalahan hanyalah string yang dikembalikan oleh fungsi Error() dari sebuah kesalahan. Respons HTTP opsional dapat diakses dengan memanggil fungsi errorutils.HTTPResponse() , yang mengembalikan *http.Response .

Aman untuk meneruskan nil atau nilai kesalahan lainnya ke fungsi pemeriksaan kesalahan dalam paket errorutils . Mereka mengembalikan true jika argumen input benar-benar berisi kode kesalahan yang dimaksud, dan mengembalikan false untuk yang lainnya. Fungsi HTTPResponse() memiliki perilaku serupa, hanya saja ia mengembalikan nil , bukan false .

API yang mengekspos kode kesalahan layanan menyediakan fungsi pemeriksaan kesalahan khusus API dalam paket yang sesuai. Misalnya, paket auth menyediakan fungsi IsUserNotFound() dan IsExpiredIDTokenError() .

.BERSIH

Di .NET semua kesalahan API memperluas kelas FirebaseException . Anda dapat mengakses kode kesalahan platform, pesan kesalahan, dan respons HTTP opsional dari kelas dasar ini.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

API yang mengekspos kode kesalahan layanan menyediakan subkelas FirebaseException khusus API. Misalnya, semua metode publik di FirebaseAuth API dideklarasikan untuk menampilkan instance FirebaseAuthException . Anda dapat mengakses kode kesalahan layanan dari kelas turunan ini.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Kode kesalahan platform

Kode kesalahan umum terjadi di semua layanan Firebase dan Google Cloud Platform. Tabel berikut menguraikan semua kemungkinan kode kesalahan platform. Ini adalah daftar yang stabil, dan diperkirakan tidak akan berubah untuk jangka waktu yang lama.

ARGUMEN YANG TIDAK SAH Klien menentukan argumen yang tidak valid.
FAILED_PRECONDITION Permintaan tidak dapat dijalankan dalam keadaan sistem saat ini, seperti menghapus direktori yang tidak kosong.
DILUAR JANGKAUAN Klien menentukan rentang yang tidak valid.
TIDAK DIAutentikasi Permintaan tidak diautentikasi karena token OAuth hilang, tidak valid, atau kedaluwarsa.
IZIN DITOLAK Klien tidak memiliki izin yang memadai. Hal ini dapat terjadi karena token OAuth tidak memiliki cakupan yang tepat, klien tidak memiliki izin, atau API belum diaktifkan untuk proyek klien.
TIDAK DITEMUKAN Sumber daya tertentu tidak ditemukan, atau permintaan ditolak karena alasan yang tidak diungkapkan seperti masuk daftar putih.
KONFLIK Konflik konkurensi, seperti konflik baca-modifikasi-tulis. Hanya digunakan oleh beberapa layanan lama. Kebanyakan layanan menggunakan ABORTED atau ALREADY_EXISTS sebagai ganti ini. Lihat dokumentasi khusus layanan untuk melihat mana yang harus ditangani dalam kode Anda.
DIBATALKAN Konflik konkurensi, seperti konflik baca-modifikasi-tulis.
SUDAH ADA Sumber daya yang coba dibuat oleh klien sudah ada.
RESOURCE_EXHAUSTED Entah kehabisan kuota sumber daya atau mencapai batas kecepatan.
DIBATALKAN Permintaan dibatalkan oleh klien.
DATA HILANG Kehilangan data yang tidak dapat dipulihkan atau kerusakan data. Klien harus melaporkan kesalahan tersebut kepada pengguna.
TIDAK DIKENAL Kesalahan server tidak diketahui. Biasanya bug server.

Kode kesalahan ini juga ditetapkan untuk kesalahan penguraian respons lokal (unmarshal), dan berbagai kesalahan I/O tingkat rendah lainnya yang tidak mudah didiagnosis.

INTERN Kesalahan server dari dalam. Biasanya bug server.
TIDAK TERSEDIA Layanan tidak tersedia. Biasanya server sedang down untuk sementara.

Kode kesalahan ini juga ditetapkan untuk kesalahan jaringan lokal (koneksi ditolak, tidak ada rute ke host).

DEADLINE_EXCEEDED Batas waktu permintaan terlampaui. Hal ini akan terjadi hanya jika pemanggil menetapkan tenggat waktu yang lebih pendek dari tenggat waktu default API target (yaitu tenggat waktu yang diminta tidak cukup bagi server untuk memproses permintaan), dan permintaan tidak selesai dalam tenggat waktu tersebut.

Kode kesalahan ini juga ditetapkan untuk koneksi lokal dan batas waktu baca.

Kebanyakan API hanya dapat menghasilkan sebagian dari kode kesalahan di atas. Bagaimanapun, Anda tidak diharapkan untuk secara eksplisit menangani semua kode kesalahan ini saat menerapkan penangan kesalahan Anda. Sebagian besar aplikasi hanya tertarik pada 1-2 kode kesalahan tertentu dan menganggap yang lainnya sebagai kegagalan umum yang tidak dapat dipulihkan.

Kode kesalahan khusus layanan

Otentikasi Firebase

CERTIFICATE_FETCH_FAILED Gagal mengambil sertifikat kunci publik yang diperlukan untuk memverifikasi JWT (token ID atau cookie sesi).
EMAIL SUDAH ADA Seorang pengguna sudah ada dengan email yang diberikan.
EXPIRED_ID_TOKEN Token ID yang ditentukan ke verifyIdToken() sudah habis masa berlakunya.
EXPIRED_SESSION_COOKIE Cookie sesi yang ditentukan untuk verifySessionCookie() telah kedaluwarsa.
INVALID_DYNAMIC_LINK_DOMAIN Domain tautan dinamis yang disediakan tidak dikonfigurasi atau diotorisasi untuk proyek saat ini. Terkait dengan API tautan tindakan email.
INVALID_ID_TOKEN Token ID yang ditentukan ke verifyIdToken() tidak valid.
INVALID_SESSION_COOKIE Cookie sesi yang ditentukan ke verifySessionCookie() tidak valid.
PHONE_NUMBER_ALREADY_EXISTS Seorang pengguna sudah ada dengan nomor telepon yang diberikan.
DITAWARKAN_ID_TOKEN Token ID yang ditentukan ke verifyIdToken() dicabut.
REVOKED_SESSION_COOKIE Cookie sesi yang ditentukan untuk verifySessionCookie() telah kedaluwarsa.
UNAUTHORIZED_CONTINUE_URL Domain URL lanjutkan tidak masuk daftar putih. Terkait dengan API tautan tindakan email.
PENGGUNA TIDAK DITEMUKAN Tidak ada catatan pengguna yang ditemukan untuk pengidentifikasi yang diberikan.

Pesan Cloud Firebase

THIRD_PARTY_AUTH_ERROR Sertifikat APN atau kunci API autentikasi web push tidak valid atau hilang.
ARGUMEN YANG TIDAK SAH Satu atau lebih argumen yang ditentukan dalam permintaan tidak valid.
INTERN Kesalahan server dari dalam.
KUOTA TERLAMPAUI Batas pengiriman melebihi target pesan.
SENDER_ID_MISMATCH ID pengirim yang diautentikasi berbeda dengan ID pengirim untuk token pendaftaran. Hal ini biasanya berarti pengirim dan token pendaftaran target tidak berada dalam proyek Firebase yang sama.
TIDAK TERSEDIA Layanan Cloud Messaging untuk sementara tidak tersedia.
TIDAK TERDAFTAR Instance aplikasi tidak terdaftar dari FCM. Biasanya ini berarti token registrasi perangkat yang digunakan sudah tidak valid dan harus digunakan yang baru.

Percobaan ulang otomatis

Admin SDK secara otomatis mencoba ulang kesalahan tertentu sebelum memperlihatkan kesalahan tersebut kepada pengguna. Secara umum, jenis kesalahan berikut dicoba ulang secara transparan:

  • Semua kesalahan API disebabkan oleh respons HTTP 503 (Layanan Tidak Tersedia).
  • Beberapa kesalahan API disebabkan oleh respons HTTP 500 (Kesalahan Server Internal).
  • Sebagian besar kesalahan I/O tingkat rendah (koneksi ditolak, koneksi disetel ulang, dll).

SDK akan mencoba ulang setiap kesalahan di atas hingga 5 kali (percobaan awal + 4 percobaan ulang) dengan backoff eksponensial. Anda dapat menerapkan mekanisme percobaan ulang Anda sendiri di tingkat aplikasi jika Anda mau, namun hal ini biasanya tidak diperlukan.

Dukungan Coba Lagi-Setelah

Implementasi Admin SDK Go dan .NET dilengkapi dengan dukungan untuk menangani header HTTP Retry-After . Artinya, jika respons kesalahan yang dikirim oleh server backend berisi header Retry-After standar, SDK akan mematuhinya saat mencoba ulang selama durasi tunggu yang ditentukan tidak terlalu lama. Jika header Retry-After menunjukkan durasi tunggu yang sangat lama, SDK akan mengabaikan percobaan ulang dan memunculkan kesalahan API yang sesuai.

SDK Admin Python saat ini tidak mendukung header Retry-After , dan hanya mendukung backoff eksponensial sederhana.

Contoh penanganan kesalahan API

Menerapkan penangan kesalahan umum

Dalam kebanyakan kasus, yang Anda inginkan adalah penangan kesalahan umum yang menangkap berbagai macam kesalahan untuk mencegah penghentian aliran program yang tidak terduga karena kesalahan API. Penangan kesalahan seperti itu biasanya hanya mencatat kesalahan untuk tujuan audit, atau menjalankan beberapa rutinitas penanganan kesalahan default lainnya untuk semua kesalahan API yang ditemui. Mereka belum tentu tertarik pada kode kesalahan yang berbeda atau alasan yang mungkin menyebabkan kesalahan tersebut.

Jawa

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

ular piton

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

Pergi

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

performPrivilegedOperation(token)

.Bersih

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

Memeriksa kode kesalahan

Dalam beberapa kasus, Anda ingin memeriksa kode kesalahan yang sebenarnya, dan menjalankan rutinitas penanganan kesalahan yang sadar konteks. Dalam contoh berikut, kami memiliki penangan kesalahan yang mencatat pesan kesalahan yang lebih spesifik berdasarkan kode kesalahan layanan.

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

ular piton

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

Pergi

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)

.Bersih

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

Berikut contoh lain di mana kami memeriksa kode kesalahan tingkat atas dan layanan:

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

ular piton

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

Pergi

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

.Bersih

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

Mengakses respons HTTP

Dalam beberapa kasus yang jarang terjadi, Anda mungkin ingin memeriksa respons kesalahan HTTP yang dihasilkan oleh layanan backend dan melakukan beberapa tindakan penanganan kesalahan terhadapnya. Admin SDK memperlihatkan header dan konten respons kesalahan ini. Konten respons biasanya dikembalikan sebagai string atau urutan byte mentah, dan dapat diuraikan ke dalam format target apa pun yang diperlukan.

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

ular piton

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

Pergi

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

.Bersih

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