Ya sea que esté realizando llamadas a la API desde Node.js o en el navegador, eventualmente se producirán fallas en la conexión. Algunos errores de solicitud son válidos. Tal vez el punto final estaba equivocado o el cliente envió los datos incorrectos. Otras veces puede estar seguro de que el error es el resultado de un problema con la conexión al servidor o uno de los muchos saltos intermedios. Si bien el monitoreo de la API y el servicio web puede informarle sobre el problema, una solución más activa puede solucionarlo por usted.
Para solucionar esto, puede mejorar su biblioteca de solicitudes HTTP agregando la funcionalidad de reintento inteligente. Este tipo de remediación es crucial para garantizar que sus llamadas API sean exitosas. Algunas bibliotecas, como la compatibilidad con el reintento de solicitudes fallidas de forma inmediata, mientras que otras, como axios, requieren un complemento separado. Si su biblioteca preferida no los admite, este artículo es para usted. Exploraremos la adición de reintentos específicos del código de estado a una solicitud, haciéndolos cada vez más largos con una técnica llamada "retroceso", y más.
Para decidir cuándo volver a intentar una solicitud, debemos considerar qué buscar. Hay un puñado de códigos de estado HTTP que puede verificar. Esto permitirá que su lógica de reintento diferencie entre una solicitud fallida que es apropiada para reintentar, como un error de puerta de enlace, y otra que no lo es, como un 404. Para nuestros ejemplos, usaremos 408, 500, 502, 503, 504, 522 y 524. También puede verificar 429 , siempre que incorpore el encabezado Reintentar después en la lógica de retroceso.
La siguiente consideración que queremos es con qué frecuencia volver a intentarlo. Comenzaremos con un retraso, luego lo aumentaremos cada vez más. Este es un concepto conocido como "retroceso". El tiempo entre solicitudes crecerá con cada intento. Finalmente, también tendremos que decidir cuántos intentos hacer antes de rendirnos.
Aquí hay un ejemplo de la lógica que usaremos en pseudocódigo:
También podríamos verificar cosas como códigos de error (de Node.js) y limitar los reintentos a ciertos métodos. Por ejemplo, ignorar POST suele ser una buena idea para garantizar que no se creen entradas duplicadas.
Para que todo esto funcione, realizaremos una solicitud desde una solicitud fallida. Esto requiere el uso de la recursividad. La recursividad es cuando una función se llama a sí misma.
Por ejemplo, si quisiéramos seguir intentando realizar una solicitud infinitamente, podría verse así:
function myRequest ( url, options = {} ) { return requests(url, options, response => { if (response.ok) { return response } else { return myRequest(url, options) } }) }
Note que el
else
bloque devuelve el myRequest
función. Dado que la mayoría de las implementaciones modernas de solicitudes HTTP se basan en promesas, podemos devolver el resultado. Esto significa que, para el usuario final, todo el proceso parece una llamada normal. Por ejemplo: myRequest( "https://example.com" ).then( console .log(response))
Con un plan en mente, veamos cómo implementar reintentos en javascript.
Primero, comenzaremos con la API Fetch del navegador. La implementación de obtención será similar al ejemplo de recursividad anterior. Implementemos ese mismo ejemplo, pero usando fetch y una verificación de estado.
function fetchRetry ( url, options ) { // Return a fetch request return fetch(url, options).then( res => { // check if successful. If so, return the response transformed to json if (res.ok) return res.json() // else, return a call to fetchRetry return fetchRetry(url, options) }) }
Esto funcionará para reintentar infinitamente las solicitudes fallidas. Nota: un retorno saldrá del bloque actual, por lo que no necesitamos una declaración else después de return res.json() .
Ahora agreguemos un número máximo de reintentos.
function fetchRetry ( url, options = {}, retries = 3 ) { return fetch(url, options) .then( res => { if (res.ok) return res.json() if (retries > 0 ) { return fetchRetry(url, options, retries - 1 ) } else { throw new Error (res) } }) .catch( console .error) }
El código es casi el mismo, excepto que agregamos un nuevo argumento y una nueva condición. Añadir el re
t
ries argumento a la función, con un valor predeterminado de 3. Luego, en lugar de llamar automáticamente a la función en caso de falla, verifique si quedan reintentos. Si es así, llame fetchRetry
.El nuevo valor de reintentos pasado al siguiente intento es el actual reintentos menos 1. Esto asegura que nuestro "bucle" disminuya y finalmente se detenga. Sin esto, se ejecutaría infinitamente hasta que la solicitud tenga éxito. Finalmente, si
retries
no es mayor que cero, lanza un nuevo error para .catch
manejar.Para probarlo, puede hacer una solicitud a
https://status-codes.glitch.me/status/400
. Por ejemplo: fetchRetry( "https://status-codes.glitch.me/status/400" ) .then( console .log) .catch( console .error)
Si verifica el tráfico de su red, debería ver cuatro llamadas en total. El original, más tres reintentos. A continuación, agreguemos una verificación de los códigos de estado que queremos volver a intentar.
function fetchRetry ( url, options = {}, retries = 3 ) { const retryCodes = [ 408 , 500 , 502 , 503 , 504 , 522 , 524 ] return fetch(url, options) .then( res => { if (res.ok) return res.json() if (retries > 0 && retryCodes.includes(res.status)) { return fetchRetry(url, options, retries - 1 ) } else { throw new Error (res) } }) .catch( console .error) }
Primero, declare una matriz de códigos de estado que queremos verificar. También podría agregar esto como parte de la configuración, especialmente si implementó esto como una clase con una configuración más formal. A continuación, la condición de reintento verifica si el estado de la respuesta existe en la matriz utilizando ECMAScript.
array.includes()
. Si es así, intente la solicitud. Si no, lanza un error.Hay una última característica para agregar. El retardo de retroceso incremental entre cada solicitud. Vamos a implementarlo.
function fetchRetry ( url, options = {}, retries = 3 , backoff = 300 ) { /* 1 */ const retryCodes = [ 408 , 500 , 502 , 503 , 504 , 522 , 524 ] return fetch(url, options) .then( res => { if (res.ok) return res.json() if (retries > 0 && retryCodes.includes(res.status)) { setTimeout( () => { /* 2 */ return fetchRetry(url, options, retries - 1 , backoff * 2 ) /* 3 */ }, backoff) /* 2 */ } else { throw new Error (res) } }) .catch( console .error) }
Para manejar la mecánica de "esperar" antes de volver a intentar la solicitud, puede usar setTimeout. Primero, agregamos nuestro nuevo argumento de configuración (1). Luego, configure setTimeout y use el valor de retroceso como retraso. Finalmente, cuando ocurre el reintento, también pasamos el back-off con un modificador. En este caso, backoff * 2. Esto significa que cada nuevo intento esperará el doble que el anterior.
Ahora, si probamos la función llamando a fetchRetry('https://status-codes.glitch.me/status/500'), el código realizará la primera solicitud de inmediato, el primer reintento después de esperar 300 ms, los siguientes 600 ms después la primera respuesta y el intento final 900 ms después de la segunda respuesta. Puede probarlo con cualquier código de estado usando https://status-codes.glitch.me/status/${STATUS_CODE}.
Esta es una gran solución para solicitudes únicas o aplicaciones pequeñas, pero para implementaciones más grandes, podría mejorarse. La creación de una clase configurable (u objeto similar a una clase) le dará más control y permitirá configuraciones separadas para cada integración de API. También podría aplicar esta lógica a un disyuntor o cualquier otro patrón de remediación.
Otra opción es utilizar una herramienta que observe y reaccione ante anomalías en sus llamadas API. En Bearer , nuestro equipo está construyendo precisamente eso. En lugar de configurar todo esto en código para cada API, el Bearer Agent lo maneja todo por usted. Pruébelo hoy y háganos saber lo que piensa @BearerSH
http
móduloLa implementación de búsqueda anterior funciona para el navegador, pero ¿qué pasa con Node.js? Podría usar una biblioteca equivalente a fetch como node-fetch . Para hacer las cosas interesantes, veamos la aplicación de los mismos conceptos anteriores al nativo de Node.js.
http
módulo.Para hacer las cosas un poco más fáciles, usaremos la abreviatura
http.get
método. La lógica de reintento seguirá siendo la misma, así que consulte nuestro artículo sobre cómo realizar llamadas API con http.request si desea realizar solicitudes que no sean GET.Antes de comenzar, necesitaremos cambiar http.get de basado en eventos a basado en promesas para que podamos interactuar con él de la misma manera que lo hicimos con fetch. Si es nuevo en las promesas, son un concepto subyacente que usan las implementaciones asincrónicas modernas. Cada vez que usas
.then
o async/await , está usando promesas bajo el capó. Para los propósitos de este artículo, todo lo que necesita saber es que una promesa puede resolverse o rechazarse; en otras palabras, el código pasa o falla. Veamos un código sin ninguna lógica de reintento.Aquí hay un GET básico usando
http.get
let https = require ( "https" ) https.get(url, res => { let data = "" let { statusCode } = res if (statusCode < 200 || statusCode > 299 ) { throw new Error (res) } else { res.on( "data" , d => { data += d }) res.end( "end" , () => { console .log(data) }) } })
Para resumir, solicita una url. Si el
statusCode
no está en un "rango de éxito" definido (Fetch tiene el ok
propiedad para manejar esto) arroja un error. De lo contrario, genera una respuesta y se registra en la consola. Veamos cómo se ve esto "prometido". Para que sea más fácil de seguir, omitiremos parte del manejo de errores adicional. function retryGet ( url ) { return new Promise ( ( resolve, reject ) => { https.get(url, res => { let data = "" const { statusCode } = res if (statusCode < 200 || statusCode > 299 ) { reject( Error (res)) } else { res.on( "data" , d => { data += d }) res.on( "end" , () => { resolve(data) }) } }) }) }
Las partes clave aquí son:
Entonces podemos probarlo llamando
retryGet("https://status-codes.glitch.me/status/500").then(console.log).catch(console.error)
. Cualquier cosa fuera del rango de 200 aparecerá en nuestra captura, mientras que cualquier cosa dentro del rango aparecerá en ese momento.A continuación, traigamos toda la lógica del ejemplo de búsqueda a
retryGet
. function retryGet ( url, retries = 3 , backoff = 300 ) { /* 1 */ const retryCodes = [ 408 , 500 , 502 , 503 , 504 , 522 , 524 ] /* 2 */ return new Promise ( ( resolve, reject ) => { https.get(url, res => { let data = "" const { statusCode } = res if (statusCode < 200 || statusCode > 299 ) { if (retries > 0 && retryCodes.includes(statusCode)) { /* 3 */ setTimeout( () => { return retryGet(url, retries - 1 , backoff * 2 ) }, backoff) } else { reject( Error (res)) } } else { res.on( "data" , d => { data += d }) res.on( "end" , () => { resolve(data) }) } }) }) }
Esto es similar a la
fetch
ejemplo. Primero, configure los nuevos argumentos (1). Luego, defina la retryCodes (2)
. Finalmente, configure la lógica de reintento y regrese retryGet
. Esto asegura que cuando el usuario llama retryGet(...)
y espera que le devuelvan una promesa, la recibirán.¡Te quedaste con él a través de la sección de bonificación 🎉! Con los mismos conceptos de este artículo, puede aplicar la funcionalidad de reintento a su biblioteca favorita si aún no la incluye. ¿Buscas algo más sustancioso?
Pruebe Bearer y consulte el Blog de Bearer para obtener más información sobre Node.js, integraciones de API, mejores prácticas de monitoreo y más.
Publicado anteriormente en https://blog.bearer.sh/add-retry-to-api-calls-javascript-node/