Mencoba ulang permintaan HTTP yang gagal di Angular

Organisasi akses ke data server adalah dasar dari hampir semua aplikasi satu halaman. Semua konten dinamis dalam aplikasi tersebut diunduh dari backend.

Dalam kebanyakan kasus, permintaan HTTP ke server berfungsi dengan andal dan mengembalikan hasil yang diinginkan. Namun, dalam beberapa situasi, permintaan mungkin gagal.

Bayangkan bagaimana seseorang bekerja dengan situs web Anda melalui titik akses di kereta api yang bepergian di seluruh negeri dengan kecepatan 200 kilometer per jam. Koneksi jaringan dalam skenario ini bisa lambat, tetapi server meminta, meskipun demikian, melakukan pekerjaan mereka.

Tetapi bagaimana jika kereta itu masuk ke dalam terowongan? Ada kemungkinan besar bahwa koneksi ke Internet akan terputus dan aplikasi web tidak akan dapat "menjangkau" ke server. Dalam hal ini, pengguna harus memuat ulang halaman aplikasi setelah kereta meninggalkan terowongan dan koneksi Internet dipulihkan.

Muat ulang halaman dapat memengaruhi kondisi aplikasi saat ini. Ini berarti bahwa pengguna dapat, misalnya, kehilangan data yang ia masukkan ke dalam formulir.

Daripada hanya berdamai dengan fakta bahwa permintaan tertentu tidak berhasil, akan lebih baik untuk mengulanginya beberapa kali dan menunjukkan kepada pengguna pemberitahuan yang sesuai. Dengan pendekatan ini, ketika pengguna menyadari bahwa aplikasi sedang mencoba untuk mengatasi masalah, ia kemungkinan besar tidak akan memuat ulang halaman.



Bahan, terjemahan yang kami terbitkan hari ini, dikhususkan untuk analisis beberapa cara mengulangi permintaan yang gagal dalam aplikasi Angular.

Ulangi permintaan yang gagal


Mari kita mereproduksi situasi yang mungkin ditemui pengguna yang bekerja di Internet dari kereta. Kami akan membuat backend yang memproses permintaan secara tidak benar selama tiga upaya pertama untuk mengaksesnya, mengembalikan data hanya dari upaya keempat.
Biasanya, menggunakan Angular, kami membuat layanan, menghubungkan HttpClient dan menggunakannya untuk mendapatkan data dari backend.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      catchError(() => {        //           return EMPTY;      })    );  } } 

Tidak ada yang istimewa di sini. Kami menyambungkan modul HttpClient Angular dan menjalankan permintaan GET sederhana. Jika permintaan mengembalikan kesalahan, kami menjalankan beberapa kode untuk memprosesnya dan mengembalikan yang Observable diamati (objek yang dapat diobservasi) untuk menginformasikan tentang apa yang memulai permintaan. Kode ini, seolah-olah, mengatakan: "Ada kesalahan, tetapi semuanya beres, saya bisa mengatasinya."

Sebagian besar aplikasi melakukan permintaan HTTP dengan cara ini. Dalam kode di atas, permintaan dieksekusi hanya sekali. Setelah itu, ia mengembalikan data yang diterima dari server, atau tidak berhasil.

Bagaimana cara mengulang permintaan jika titik akhir /greet tidak tersedia atau mengembalikan kesalahan? Mungkin ada pernyataan RxJS yang cocok? Tentu saja ada. RxJS memiliki operator untuk semuanya.

Hal pertama yang mungkin terpikirkan dalam situasi ini adalah retry . Mari kita lihat definisinya: “Mengembalikan Observable yang memainkan Observable original kecuali untuk error . Jika error Observable original dapat diobservasi, maka metode ini, alih-alih menyebarkan kesalahan, akan berlangganan kembali ke Observable original.

Jumlah maksimum berlangganan ulang terbatas untuk count (ini adalah parameter numerik yang diteruskan ke metode). "

retry sangat mirip dengan yang kita butuhkan. Jadi mari kita tanamkan dalam rantai kita.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      retry(3),      catchError(() => {        //           return EMPTY;      }),      shareReplay()    );  } } 

Kami telah berhasil menggunakan operator retry . Mari kita lihat bagaimana ini mempengaruhi perilaku permintaan HTTP yang dieksekusi di aplikasi eksperimental. Berikut adalah file GIF besar yang menunjukkan layar aplikasi ini dan tab Jaringan alat pengembang browser. Anda akan menemukan beberapa demonstrasi lagi di sini.

Aplikasi kami sangat sederhana. Itu hanya membuat permintaan HTTP ketika tombol PING THE SERVER diklik.

Seperti yang telah disebutkan, backend mengembalikan kesalahan saat melakukan tiga upaya pertama untuk mengeksekusi permintaan, dan ketika permintaan keempat datang, itu mengembalikan respons normal.

Pada tab Alat pengembang jaringan, Anda dapat melihat bahwa retry memecahkan tugas yang ditugaskan untuk itu dan mengulangi pelaksanaan permintaan gagal tiga kali. Upaya terakhir berhasil, aplikasi menerima respons, pesan yang sesuai muncul di halaman.

Semua ini sangat bagus. Sekarang aplikasi dapat mengulangi permintaan yang gagal.

Namun, contoh ini masih bisa diperbaiki. Harap perhatikan bahwa sekarang permintaan yang berulang akan dieksekusi segera setelah eksekusi permintaan yang tidak berhasil. Perilaku sistem ini tidak akan membawa banyak manfaat dalam situasi kita - ketika kereta memasuki terowongan dan koneksi internet terputus untuk sementara waktu.

Coba lagi permintaan yang gagal


Kereta yang masuk ke terowongan tidak langsung meninggalkannya. Dia menghabiskan beberapa waktu di sana. Oleh karena itu, kita perlu "meregangkan" periode di mana kita melakukan permintaan berulang ke server. Anda dapat melakukan ini dengan menunda coba lagi.

Untuk melakukan ini, kita perlu lebih mengontrol proses mengeksekusi permintaan berulang. Kita harus dapat membuat keputusan tentang kapan tepatnya mengulangi permintaan. Ini berarti bahwa kemampuan operator retry lagi tidak lagi cukup bagi kami. Karena itu, kami kembali membuka dokumentasi tentang RxJS.

Dokumentasi berisi deskripsi retryWhen , yang tampaknya cocok untuk kita. Dalam dokumentasi, dijelaskan sebagai berikut: “Mengembalikan Observable yang memainkan Observable asli dengan pengecualian error . Jika error dapat diobservasi asli, maka metode ini akan melempar Throwable, yang menyebabkan kesalahan, Observable kembali dari notifier . Jika panggilan yang diamati ini complete atau error , maka metode ini akan memanggil complete atau error pada langganan anak. Jika tidak, metode ini akan berlangganan kembali ke Observable original. "

Ya, definisi itu tidak sederhana. Mari kita gambarkan hal yang sama dalam bahasa yang lebih mudah diakses.

retryWhen menerima panggilan balik yang mengembalikan Observable. Observable yang dikembalikan memutuskan bagaimana retryWhen operator akan berperilaku berdasarkan beberapa aturan. Yaitu, ini adalah bagaimana retryWhen operator retryWhen :

  • Itu berhenti bekerja dan melempar kesalahan jika yang diamati kembali melemparkan kesalahan.
  • Itu keluar jika laporan yang diamati diamati selesai.
  • Dalam kasus lain, ketika Observable kembali dengan sukses, ia mengulangi eksekusi Observable yang asli

Panggilan balik hanya dipanggil saat Observable yang asli melontarkan kesalahan untuk pertama kalinya.

Sekarang kita bisa menggunakan pengetahuan ini untuk membuat mekanisme coba lagi yang ditunda untuk permintaan gagal menggunakan coba lagi retryWhen .

 retryWhen((errors: Observable<any>) => errors.pipe(    delay(delayMs),    mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry))    )) ) 

Jika Observable asli, yang merupakan permintaan HTTP kami, mengembalikan kesalahan, maka retryWhen pernyataan retryWhen . Dalam panggilan balik, kami memiliki akses ke kesalahan yang menyebabkan kegagalan. Kami menangguhkan errors , mengurangi jumlah percobaan ulang, dan mengembalikan Observable baru yang melempar kesalahan.

Berdasarkan aturan retryWhen , diamati ini, karena retryWhen , retryWhen permintaan. Jika pengulangan tidak berhasil beberapa kali dan nilai variabel retries menurun menjadi 0, maka kami mengakhiri tugas dengan kesalahan yang terjadi saat menjalankan permintaan.

Hebat! Tampaknya, kita dapat mengambil kode di atas dan mengganti operator retry di rantai kami dengannya. Tapi di sini kita sedikit melambat.

Bagaimana cara retries variabel? Variabel ini berisi kondisi saat ini dari sistem coba lagi permintaan yang gagal. Di mana dia diumumkan? Kapan kondisinya diatur ulang? Negara perlu dikelola di dalam aliran, bukan di luarnya.

▍Membuat pernyataanRedri Anda sendiri yang tertunda


Kita dapat memecahkan masalah manajemen negara dan meningkatkan keterbacaan kode dengan menulis kode di atas sebagai operator RxJS terpisah.

Ada berbagai cara untuk membuat operator RxJS Anda sendiri. Metode mana yang digunakan tergantung pada bagaimana operator terstruktur.

Operator kami didasarkan pada operator RxJS yang ada. Akibatnya, kita dapat menggunakan cara paling sederhana untuk membuat operator kita sendiri. Dalam kasus kami, operator RxJ hanya fungsi dengan tanda tangan berikut:

 const customOperator = (src: Observable<A>) => Observable<B> 

Pernyataan ini mengambil Observable asli dan mengembalikan Observable lainnya.

Karena operator kami memungkinkan pengguna untuk menentukan seberapa sering permintaan berulang harus dieksekusi, dan berapa kali mereka perlu dieksekusi, kita perlu membungkus deklarasi fungsi di atas dalam fungsi pabrik, yang membutuhkan delayMs (keterlambatan antara maxRetry ) dan maxRetry ( jumlah pengulangan maksimum).

 const customOperator = (delayMs: number, maxRetry: number) => {   return (src: Observable<A>) => Observable<B> } 

Jika Anda ingin membuat operator yang tidak didasarkan pada operator yang ada, Anda perlu memperhatikan penanganan kesalahan dan langganan. Selain itu, Anda perlu memperluas kelas yang Dapat Observable dan mengimplementasikan fungsi lift .

Jika Anda tertarik, silakan lihat di sini .

Jadi, berdasarkan cuplikan kode di atas, mari kita tulis operator RxJ kita sendiri.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        delay(delayMs),        mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry))        ))      )    ); } 

Bagus Sekarang kita dapat mengimpor operator ini ke dalam kode klien. Kami akan menggunakannya saat menjalankan permintaan HTTP.

 return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(        delayedRetry(1000, 3),        catchError(error => {            console.log(error);            //               return EMPTY;        }),        shareReplay()    ); 

Kami menempatkan operator delayedRetry di rantai dan meneruskan angka 1000 dan 3. sebagai parameter. Parameter pertama menetapkan penundaan dalam milidetik di antara upaya membuat permintaan berulang. Parameter kedua menentukan jumlah maksimum permintaan berulang.

Mulai ulang aplikasi dan lihat bagaimana operator baru bekerja.

Setelah menganalisis perilaku program menggunakan alat pengembang peramban, kita dapat melihat bahwa pelaksanaan upaya berulang untuk mengeksekusi permintaan ditunda selama sedetik. Setelah menerima jawaban yang benar untuk permintaan tersebut, pesan yang sesuai akan muncul di jendela aplikasi.

Permintaan eksponensial tertunda


Mari kita kembangkan gagasan penundaan coba lagi permintaan yang gagal. Sebelumnya, kami selalu menunda eksekusi setiap permintaan yang berulang pada saat yang bersamaan.

Di sini kita berbicara tentang cara meningkatkan penundaan setelah setiap upaya. Upaya pertama untuk mencoba kembali permintaan dilakukan setelah satu detik, yang kedua setelah dua detik, yang ketiga setelah tiga.

Buat pernyataan baru, retryWithBackoff , yang mengimplementasikan perilaku ini.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        mergeMap(error => {            if (retries-- > 0) {              const backoffTime = delayMs + (maxRetry - retries) * backoffMs;              return of(error).pipe(delay(backoffTime));            }            return throwError(getErrorMessage(maxRetry));          }        )))); } 

Jika Anda menggunakan operator ini dalam aplikasi dan mengujinya, Anda dapat melihat bagaimana penundaan dalam menjalankan permintaan yang diulang meningkat setelah setiap upaya baru.

Setelah setiap upaya, kami menunggu waktu tertentu, ulangi permintaan dan menambah waktu tunggu. Di sini, seperti biasa, setelah server mengembalikan jawaban yang benar ke permintaan, kami menampilkan pesan di jendela aplikasi.

Ringkasan


Mengulangi permintaan HTTP yang gagal membuat aplikasi lebih stabil. Ini sangat penting ketika melakukan permintaan yang sangat penting, tanpa data yang diperoleh, aplikasi tidak dapat bekerja secara normal. Misalnya, itu bisa berupa data konfigurasi yang berisi alamat server yang dengannya aplikasi perlu berinteraksi.

Dalam sebagian besar skenario, retry coba lagi RxJ tidak cukup untuk menyediakan sistem coba lagi yang dapat diandalkan untuk permintaan gagal. retryWhen memberi pengembang tingkat kontrol yang lebih tinggi atas permintaan berulang. Ini memungkinkan Anda untuk mengkonfigurasi interval untuk permintaan yang berulang. Karena kemampuan operator ini, dimungkinkan untuk menerapkan skema pengulangan tertunda atau pengulangan tertunda secara eksponensial.

Ketika menerapkan pola perilaku yang cocok untuk digunakan kembali dalam rantai RxJS, disarankan agar mereka diformat sebagai operator baru. Berikut adalah repositori tempat kode digunakan dalam artikel ini.

Pembaca yang budiman! Bagaimana Anda memecahkan masalah mencoba kembali permintaan HTTP yang gagal?

Source: https://habr.com/ru/post/id459302/


All Articles