Cómo agregar soporte de reintento automático para recuperar en Node.js

Cómo escribir una función contenedora para Fetch API en Node.js que agrega funcionalidad de reintento con un retraso opcional y un número máximo de intentos.

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado de cd en su nuevo proyecto y ejecute joystick start . Antes de ejecutar eso, necesitamos instalar una dependencia más, node-fetch :

Terminal

cd app && npm i node-fetch

Esto nos dará acceso a una implementación compatible con Node.js de la API Fetch. Una vez instalado, puede continuar e iniciar su aplicación.

Terminal

joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Escribiendo una función contenedora para Fetch

Para comenzar, primero escribiremos nuestra función contenedora, así como otra función para ayudarnos a crear un retraso entre los intentos de reintento. Debido a que consideraríamos un código como este "misceláneo" o parte de la "biblioteca estándar" de nuestra aplicación, vamos a crear un archivo dentro del /lib (abreviatura de "biblioteca") carpeta en la raíz del proyecto que creamos anteriormente.

Debido a que escribiremos un código que solo está destinado a un entorno Node.js, crearemos otra carpeta dentro de /lib llamado /node lo que le indicará a Joystick que nuestro archivo solo debe construirse para un entorno disponible para Node.

/lib/node/retryFetch.js

import fetch from 'node-fetch';

const retryFetch = (url = '', options = {}) => {
  const { retry = null, retryDelay = 0, retries = 5, ...requestOptions } = options;
  return fetch(url, requestOptions);
};

export default retryFetch;

Arriba, comenzamos nuestro archivo importando el fetch dependencia que instalamos anteriormente a través de node-fetch paquete. Aquí, fetch es la función Fetch real a la que llamaremos para realizar nuestra solicitud. Justo debajo de esto, hemos definido una función retryFetch que toma dos argumentos:

  1. url cuál es la URL que vamos a "obtener".
  2. options cuál es el objeto de opciones que se entregará a fetch() .

Justo dentro de nuestro retryFetch cuerpo de la función, estamos haciendo algo especial. Aquí, estamos usando la desestructuración de JavaScript para "separar" lo pasado en options objeto. Queremos hacer esto porque vamos a "aprovechar" este objeto para incluir nuestra configuración relacionada con el reintento (Fetch no admite esto, por lo que no queremos pasarlo a Fetch accidentalmente).

Para evitar eso, aquí "eliminamos" tres propiedades del options objeto que estamos anticipando:

  1. retry un valor booleano verdadero o falso que nos permite saber si debemos volver a intentar una solicitud en caso de que falle.
  2. retryDelay un número entero que representa la cantidad de segundos a esperar antes de volver a intentar una solicitud.
  3. retries un número entero que representa el número de reintentos que debemos hacer antes de detenernos.

Después de estos, hemos escrito ...requestOptions para decir "recoge el resto del objeto en una variable llamada requestOptions que estará disponible debajo de esta línea". Hemos acentuado descanso aquí como el ... se conoce como el operador "rest/spread" en JavaScript. En este contexto, ... literalmente dice "obtén el descanso del objeto."

Para redondear nuestro código fundamental, devolvemos una llamada a fetch() pasando el url cadena como primer argumento y el options objeto pasado a nuestro retryFetch funcionan como el segundo argumento.

Esto nos da los conceptos básicos, pero por el momento nuestro retryFetch la función es un envoltorio inútil alrededor de fetch() . Ampliemos este código para incluir la función "reintentar":

/lib/node/retryFetch.js

import fetch from 'node-fetch';

let attempts = 0;

const retryFetch = async (url = '', options = {}) => {
  const { retry = null, retryDelay = 0, retries = 5, ...requestOptions } = options;

  attempts += 1;

  return fetch(url, requestOptions).then((response) => response).catch((error) => {
    if (retry && attempts <= retries) {
      console.warn({
        message: `Request failed, retrying in ${retryDelay} seconds...`,
        error: error?.message,
      });

      return retryFetch(url, options, retry, retryDelay);
    } else {
      throw new Error(error);
    }
  });
};

export default retryFetch;

Esta es la mayor parte del código para esta función. Volviendo a centrarnos en el cuerpo de nuestro retryFetch función hemos agregado algo más de código. Primero, justo debajo de nuestra desestructuración de options , hemos agregado una línea attempts += 1 que incrementa el attempts variable inicializada arriba de nuestro retryFetch función. La idea aquí es que queremos realizar un seguimiento de cada llamada a retryFetch para que podamos "rescatar" si hemos alcanzado el máximo retries permitido (si se especifica).

Vale la pena señalar, en la desestructuración de options , notará que "arrancamos" retries como retries = 5 . Lo que estamos diciendo aquí es "arrancar el retries propiedad del options objeto, y si no está definido, asígnele un valor predeterminado de 5 ." Esto significa que incluso si no pasar un número específico de retries , de forma predeterminada, lo intentaremos 5 veces y luego nos detendremos (esto evita que nuestro código se ejecute infinitamente y desperdicie recursos en una solicitud que no se puede resolver).

A continuación, observe que extendimos nuestra llamada a fetch() , aquí agregando el .then() y .catch() devoluciones de llamada para una promesa de JavaScript (esperamos fetch() para devolver una promesa de JavaScript).

Porque nuestro objetivo es manejar solo un fallido solicitud, para el .then() devolución de llamada, simplemente tomamos el response pasado e inmediatamente devolverlo (aunque técnicamente no es necesario, podríamos omitir .then() —esto agrega claridad a nuestro código por motivos de mantenimiento).

Para el .catch() —lo que realmente nos importa—comprobamos si retry es cierto y que nuestro attempts el valor actual de la variable es menor o igual que el número especificado de retries (ya sea lo que hemos pasado o el valor predeterminado de 5 ).

Si ambas cosas son verdad , primero, queremos darnos un aviso de que la solicitud falló llamando a console.warn() pasar un objeto con dos cosas:un mensaje que nos informa que la solicitud falló y que intentaremos en el retryDelay asignado y el mensaje de error que recibimos de la solicitud.

Lo más importante, en la parte inferior, hacemos una llamada recursiva a retryFetch() pasando exactamente los mismos argumentos con los que se llamó inicialmente.

Este es el "truco" de esta función. Aunque estamos dentro del retryFetch función, todavía podemos llamarlo desde dentro de sí mismo:trippy. Tenga en cuenta que hemos antepuesto un return en el frente también. Porque llamamos return frente a nuestro fetch() original llamar, el return delante de nuestro recursivo retryFetch la llamada volverá a "burbujear" al return fetch() y, en última instancia, ser el valor de retorno de nuestro retryFetch() inicial llamar.

En caso de que no lo hayamos funcionalidad de reintento habilitada o nos hemos quedado sin intentos, tomamos el error que ocurrió y tirarlo (esto le permite burbujear al .catch() de la llamada a retryFetch() correctamente).

Antes de que podamos decir "hecho", hay un pequeño error. Tal como está este código, tenga en cuenta que no haciendo uso del retryDelay anticipamos ser pasado. Para hacer uso de esto, vamos a escribir otra función arriba de nuestro retryFetch definición que nos permitirá "pausar" nuestro código durante un número arbitrario de segundos antes de continuar.

/lib/node/retryFetch.js

import fetch from 'node-fetch';

let attempts = 0;

const wait = (time = 0) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, time * 1000);
  });
};

const retryFetch = async (url = '', options = {}) => {
  const { retry = null, retryDelay = 0, retries = 5, ...requestOptions } = options;

  attempts += 1;

  return fetch(url, requestOptions).then((response) => response).catch(async (error) => {
    if (retry && attempts <= retries) {
      console.warn({
        message: `Request failed, retrying in ${retryDelay} seconds...`,
        error: error?.message,
      });

      await wait(retryDelay);

      return retryFetch(url, options, retry, retryDelay);
    } else {
      throw new Error(error);
    }
  });
};

export default retryFetch;

Este es ahora el código completo. Por encima de retryFetch , hemos añadido otra función wait que toma un time como un número entero en segundos y devuelve una promesa de JavaScript. Si miramos de cerca, dentro de la Promesa devuelta hay una llamada a setTimeout() tomando el pasado time y multiplicándolo por 1000 (para obtener los segundos en milisegundos que espera JavaScript). Dentro del setTimeout() función de devolución de llamada, llamamos al resolve() función de la Promesa devuelta.

Como sugiere el código, cuando JavaScript llama al wait() función, si le decimos usando el await palabra clave, JavaScript "esperará" a que se resuelva la Promesa. Aquí, esa Promesa se resolverá después del time especificado ha transcurrido. Genial, ¿eh? Con esto, obtenemos una pausa asíncrona en nuestro código sin cuellos de botella en Node.js.

Poner esto en uso es bastante simple. Justo encima de nuestra llamada recursiva a retryFetch() , llamamos al await wait(retryDelay) . Tenga en cuenta también que hemos agregado el async palabra clave a la función que estamos pasando a .catch() para que el await here no activa un error de tiempo de ejecución en JavaScript (await se conoce como "palabra clave reservada" en JavaScript y no funcionará a menos que el contexto principal donde se usa esté marcado como async ).

¡Eso es todo! Escribamos un código de prueba para probar esto.

Llamando a la función contenedora

Para probar nuestro código, pasemos al /index.server.js archivo en la raíz del proyecto que se creó para nosotros anteriormente cuando ejecutamos joystick create .

/index.servidor.js

import node from "@joystick.js/node";
import api from "./api";
import retryFetch from './lib/node/retryFetch';

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
}).then(async () => {
  retryFetch('https://thisdoesnotexistatallsowillfail.com', {
    retry: true,
    retryDelay: 5,
    retries: 3,
    method: 'GET', // NOTE: Unnecessary, just showcasing passing regular Fetch options.
  }).then(async (response) => {
    // NOTE: If all is well, handle the response.
    console.log(response);
  }).catch((error) => {
    // NOTE: If the alotted number of retry attempts fails, catch the final error.
    console.warn(error);
  });
});

La parte en la que queremos centrarnos aquí es el .then() hemos agregado el final de node.app() cerca de la parte inferior del archivo. En el interior, podemos ver que llamamos al retryFetch() importado función, pasando el url queremos llamar como una cadena y un objeto de opciones que se pasará a fetch() . Recuerde que en el objeto de opciones, le hemos dicho a nuestro código que espere tres opciones adicionales:retry , retryDelay y retries .

Aquí, hemos especificado el comportamiento de nuestra función junto con un fetch() estándar opción method . Al final de nuestra llamada a retryFetch() , agregamos un .then() para manejar un caso de uso exitoso y un .catch() para manejar el error que se devuelve si nos quedamos sin reintentos antes de obtener una respuesta exitosa.

Si abrimos la terminal donde iniciamos nuestra aplicación, deberíamos ver un error que se imprime en la terminal (la URL pasada no existe y fallará de inmediato). Con la configuración anterior, deberíamos ver 3 errores impresos con 5 segundos de diferencia y luego un error final que nos informa que la solicitud finalmente falló.

Terminando

En este tutorial, aprendimos cómo escribir una función contenedora alrededor de Node.js fetch() implementación que nos permitió especificar la lógica de reintento. Aprendimos a envolver el fetch() función mientras le proporciona argumentos desde el contenedor, así como también cómo llamar recursivamente a la función contenedora en caso de que nuestra solicitud falle. Finalmente, aprendimos cómo crear una función para retrasar nuestro código por un número arbitrario de segundos para pausar entre intentos de solicitud.