Reintentando solicitudes HTTP fallidas en Angular

La organización del acceso a los datos del servidor es la base de casi cualquier aplicación de una página. Todo el contenido dinámico en dichas aplicaciones se descarga desde el backend.

En la mayoría de los casos, las solicitudes HTTP al servidor funcionan de manera confiable y devuelven el resultado deseado. Sin embargo, en algunas situaciones, las solicitudes pueden fallar.

Imagine cómo alguien trabaja con su sitio web a través de un punto de acceso en un tren que viaja por todo el país a una velocidad de 200 kilómetros por hora. La conexión de red en este escenario puede ser lenta, pero las solicitudes del servidor, a pesar de esto, hacen su trabajo.

Pero, ¿y si el tren entra al túnel? Existe una alta probabilidad de que la conexión a Internet se interrumpa y la aplicación web no pueda "comunicarse" con el servidor. En este caso, el usuario tendrá que volver a cargar la página de la aplicación después de que el tren salga del túnel y se restablezca la conexión a Internet.

Recargar la página puede afectar el estado actual de la aplicación. Esto significa que el usuario puede, por ejemplo, perder los datos que ingresó en el formulario.

En lugar de simplemente conciliar con el hecho de que una determinada solicitud no tuvo éxito, sería mejor repetirla varias veces y mostrar al usuario una notificación correspondiente. Con este enfoque, cuando el usuario se da cuenta de que la aplicación está tratando de hacer frente al problema, lo más probable es que no vuelva a cargar la página.



El material, cuya traducción publicamos hoy, está dedicado al análisis de varias formas de repetir solicitudes fallidas en aplicaciones angulares.

Repetir solicitudes fallidas


Reproduzcamos una situación que puede encontrar un usuario que trabaja en Internet desde un tren. Crearemos un backend que procese la solicitud de forma incorrecta durante los primeros tres intentos de acceso, devolviendo datos solo del cuarto intento.
Usualmente, usando Angular, creamos un servicio, conectamos el HttpClient y lo usamos para obtener datos del 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;      })    );  } } 

No hay nada especial aquí. HttpClient módulo Angular HttpClient y ejecutamos una simple solicitud GET. Si la solicitud devuelve un error, ejecutamos un código para procesarlo y devolvemos un Observable (objeto observable) vacío para informar sobre lo que inició la solicitud. Este código, por así decirlo, dice: "Hubo un error, pero todo está en orden, puedo manejarlo".

La mayoría de las aplicaciones realizan solicitudes HTTP de esta manera. En el código anterior, la solicitud se ejecuta solo una vez. Después de eso, devuelve los datos recibidos del servidor o no tiene éxito.

¿Cómo repetir la solicitud si el punto final /greet no /greet disponible o devuelve un error? Tal vez hay una declaración RxJS adecuada? Por supuesto que existe. RxJS tiene operadores para todo.

Lo primero que puede venir a la mente en esta situación es la retry . Veamos su definición: “Devuelve un Observable que reproduce el Observable original, excepto por error . Si el Observable original llama error , entonces este método, en lugar de propagar el error, se volverá a suscribir al Observable original.

El número máximo de nuevas suscripciones se limita al count (este es el parámetro numérico pasado al método) ".

La retry muy similar a lo que necesitamos. Así que incrustemos en nuestra cadena.

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

Hemos utilizado con éxito el operador de retry . Veamos cómo esto afectó el comportamiento de la solicitud HTTP que se ejecuta en la aplicación experimental. Aquí hay un archivo GIF grande que muestra la pantalla de esta aplicación y la pestaña Red de las herramientas de desarrollo del navegador. Encontrará varias demostraciones más aquí.

Nuestra aplicación es extremadamente simple. Simplemente realiza una solicitud HTTP cuando se hace clic en el botón PING THE SERVER .

Como ya se mencionó, el back-end devuelve un error al realizar los primeros tres intentos de ejecutar una solicitud, y cuando llega una cuarta solicitud, devuelve una respuesta normal.

En la pestaña de herramientas del desarrollador de la red, puede ver que la retry resuelve la tarea asignada y repite la ejecución de la solicitud fallida tres veces. El último intento es exitoso, la aplicación recibe una respuesta, aparece un mensaje correspondiente en la página.

Todo esto es muy bueno. Ahora la aplicación puede repetir solicitudes fallidas.

Sin embargo, este ejemplo aún se puede mejorar. Tenga en cuenta que ahora las solicitudes repetidas se ejecutan inmediatamente después de la ejecución de las solicitudes que no tienen éxito. Este comportamiento del sistema no traerá muchos beneficios en nuestra situación: cuando el tren ingresa al túnel y se pierde la conexión a Internet por un tiempo.

Retraso en el reintento de solicitudes fallidas


El tren que entró en el túnel no lo deja al instante. Él pasa algún tiempo allí. Por lo tanto, necesitamos "estirar" el período durante el cual realizamos solicitudes repetidas al servidor. Puede hacerlo aplazando los reintentos.

Para hacer esto, necesitamos controlar mejor el proceso de ejecución de solicitudes repetidas. Necesitamos poder tomar decisiones sobre cuándo exactamente repetir las solicitudes. Esto significa que las capacidades del operador de retry ya no son suficientes para nosotros. Por lo tanto, volvemos a la documentación sobre RxJS.

La documentación contiene una descripción de la retryWhen , que parece ser retryWhen para nosotros. En la documentación, se describe de la siguiente manera: “Devuelve un Observable que reproduce el Observable original, excepto por error . Si el Observable original llama error , entonces este método arrojará Throwable, que causó el error, el Observable regresó del notifier . Si este Observable llama complete o error , entonces este método llamará complete o error en la suscripción secundaria. De lo contrario, este método se volverá a suscribir al Observable original ".

Sí, la definición no es simple. Describamos lo mismo en un lenguaje más accesible.

La retryWhen acepta una devolución de llamada que devuelve un Observable. El Observable devuelto decide cómo se retryWhen se comportará el operador en función de algunas reglas. A saber, así es como el retryWhen operador se retryWhen :

  • Deja de funcionar y arroja un error si el Observable devuelto arroja un error.
  • Sale si el Observable devuelto informa que se completó.
  • En otros casos, cuando el Observable regresa con éxito, repite la ejecución del Observable original.

Solo se llama a una devolución de llamada cuando el Observable original arroja un error por primera vez.

Ahora podemos usar este conocimiento para crear un mecanismo de reintento retrasado para una solicitud fallida usando la instrucción retryWhen .

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

Si el Observable original, que es nuestra solicitud HTTP, devuelve un error, entonces se retryWhen instrucción retryWhen . En la devolución de llamada, tenemos acceso al error que causó la falla. Diferimos los errors , reducimos el número de reintentos y devolvemos un nuevo Observable que arroja un error.

Según las reglas de la retryWhen , este Observable, dado que retryWhen , retryWhen solicitud. Si la repetición no tiene éxito varias veces y el valor de la variable de retries disminuye a 0, finalizamos la tarea con un error que se produjo al ejecutar la solicitud.

Genial Aparentemente, podemos tomar el código anterior y reemplazar el operador de retry en nuestra cadena. Pero aquí disminuimos un poco la velocidad.

¿Cómo retries con los retries variables? Esta variable contiene el estado actual del sistema de reintento de solicitud fallida. ¿Dónde se anuncia ella? ¿Cuándo se restablece la condición? El estado debe gestionarse dentro de la secuencia, no fuera de ella.

▍Cree su propia declaración de Retraso retrasado


Podemos resolver el problema de la gestión del estado y mejorar la legibilidad del código escribiendo el código anterior como un operador RxJS separado.

Hay diferentes formas de crear sus propios operadores RxJS. El método a utilizar depende de cómo esté estructurado el operador en particular.

Nuestro operador se basa en los operadores existentes de RxJS. Como resultado, podemos usar la forma más sencilla de crear nuestros propios operadores. En nuestro caso, el operador RxJs es solo una función con la siguiente firma:

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

Esta declaración toma el Observable original y devuelve otro Observable.

Dado que nuestro operador permite al usuario especificar con qué frecuencia se deben ejecutar las solicitudes repetidas y cuántas veces deben ejecutarse, debemos ajustar la declaración de función anterior en una función de fábrica, que requiere delayMs (retraso entre maxRetry ) y maxRetry ( Número máximo de repeticiones).

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

Si desea crear un operador que no se base en operadores existentes, debe prestar atención al manejo de errores y suscripciones. Además, deberá ampliar la clase Observable e implementar la función de lift .

Si está interesado, eche un vistazo aquí .

Entonces, en base a los fragmentos de código anteriores, escribamos nuestro propio operador RxJs.

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

Genial Ahora podemos importar este operador al código del cliente. Lo usaremos al ejecutar una solicitud HTTP.

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

Pusimos el operador delayedRetry en la cadena y pasamos los números 1000 y 3. Como parámetros. El primer parámetro establece el retraso en milisegundos entre los intentos de realizar solicitudes repetidas. El segundo parámetro determina el número máximo de solicitudes repetidas.

Reinicie la aplicación y observe cómo funciona el nuevo operador.

Después de analizar el comportamiento del programa utilizando las herramientas del desarrollador del navegador, podemos ver que la ejecución de intentos repetidos para ejecutar la solicitud se retrasa por un segundo. Después de recibir la respuesta correcta a la solicitud, aparecerá un mensaje correspondiente en la ventana de la aplicación.

Solicitud exponencial posponer


Desarrollemos la idea del reintento retrasado de solicitudes fallidas. Anteriormente, siempre retrasábamos la ejecución de cada una de las solicitudes repetidas al mismo tiempo.

Aquí hablamos sobre cómo aumentar el retraso después de cada intento. El primer intento de reintentar la solicitud se realiza después de un segundo, el segundo después de dos segundos, el tercero después de tres.

Cree una nueva instrucción, retryWithBackoff , que implementa este comportamiento.

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

Si utiliza este operador en la aplicación y lo prueba, puede ver cómo aumenta el retraso en la ejecución de la solicitud repetida después de cada nuevo intento.

Después de cada intento, esperamos un cierto tiempo, repetimos la solicitud y aumentamos el tiempo de espera. Aquí, como de costumbre, después de que el servidor devuelve la respuesta correcta a la solicitud, mostramos un mensaje en la ventana de la aplicación.

Resumen


La repetición de solicitudes HTTP fallidas hace que las aplicaciones sean más estables. Esto es especialmente significativo cuando se realizan consultas muy importantes, sin los datos obtenidos a través de los cuales, la aplicación no puede funcionar normalmente. Por ejemplo, pueden ser datos de configuración que contienen las direcciones de los servidores con los que la aplicación necesita interactuar.

En la mayoría de los escenarios, la retry reintento de RxJs no retry suficiente para proporcionar un sistema de reintento confiable para solicitudes fallidas. La retryWhen le da al desarrollador un mayor nivel de control sobre las solicitudes repetidas. Le permite configurar el intervalo para solicitudes repetidas. Debido a las capacidades de este operador, es posible implementar un esquema de repetición retrasada o repeticiones exponencialmente retrasadas.

Al implementar patrones de comportamiento adecuados para su reutilización en cadenas RxJS, se recomienda que se formatee como nuevos operadores. Aquí está el repositorio desde el que se utilizó el código en este artículo.

Estimados lectores! ¿Cómo resuelve el problema de volver a intentar las solicitudes HTTP fallidas?

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


All Articles