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:
url
cuál es la URL que vamos a "obtener".options
cuál es el objeto de opciones que se entregará afetch()
.
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:
retry
un valor booleano verdadero o falso que nos permite saber si debemos volver a intentar una solicitud en caso de que falle.retryDelay
un número entero que representa la cantidad de segundos a esperar antes de volver a intentar una solicitud.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.