Quizás no necesites Axios
¡Fetch API al rescate!
Puede que no necesites Axios
Esto no es un ataque a Axios.
Más bien, es defensa del API fetch, que ya se ha vuelto bastante capaz. 🦄
Visión general
Este artículo es una recopilación de los fragmentos de código “faltantes” de fetch y de casos de uso comunes que desearía fueran más fáciles de encontrar.
- Visión general
- Comparación de características
- Recetas de fetch
- Obtener JSON desde una URL
- Encabezados personalizados
- Manejo de errores HTTP
- Ejemplo CORS
- Publicar JSON
- Publicar un
<form>HTML - Datos codificados en formulario
- Subir un archivo
- Subir varios archivos
- Tiempos de espera (timeouts)
- Helper de progreso de descarga
- Helper de reintentos recursivos
- Manejo de redirecciones HTTP
- Cancelar una solicitud fetch ✨nuevo✨
- Compatibilidad
¿Tu caso de uso no está listado? Déjame saber ✉️
Comparación de características
| fetch | axios | request | |
|---|---|---|---|
| Interceptar solicitud y respuesta | ✅ | ✅ | ✅ |
| Transformar datos de solicitud y respuesta | ✅ | ✅ | ✅ |
| Cancelar solicitudes | ✅ | ✅ | ❌ |
| Transformaciones automáticas para datos JSON | helpers manuales | ✅ | ✅ |
| Soporte del lado cliente para proteger contra XSRF | ✅ | ✅ | ✅ |
| Progreso | ✅ | ✅ | ✅ |
| Transmisión | ✅ | ✅ | ✅ |
| Redirecciones | ✅ | ✅ | ✅ |
Al iniciar este artículo (finales de 2018, actualizado en 2024) asumí que terminaría con una tabla de casillas mixtas. Seguramente existen casos de uso especiales que justifican axios, request, r2, superagent, got, etc.
Pues bien, al final, sobreestimé la necesidad de bibliotecas HTTP de terceros.
A pesar de haber usado fetch durante varios años (incluyendo tareas no triviales: cargas de archivos y soporte de errores/reintentos) todavía tenía ideas equivocadas sobre las capacidades y limitaciones de fetch.
El fetch nativo no analiza automáticamente respuestas JSON ni serializa cuerpos de solicitud JSON. Se llama a response.json() al volver y a JSON.stringify() al salir. Axios sigue ganando en esa ergonomía; el argumento a favor de fetch es que un pequeño helper suele cubrir esa brecha.
Bien, veamos qué puede hacer fetch…
Recetas con fetch
Obtener JSON de una URL
fetch('https://api.github.com/orgs/nodejs') .then(response => response.json()) .then(data => { console.log(data) // result from `response.json()` above }) .catch(error => console.error(error))Encabezados personalizados
fetch('https://api.github.com/orgs/nodejs', { headers: new Headers({ 'User-agent': 'Mozilla/4.0 Custom User Agent' })}).then(response => response.json()).then(data => { console.log(data)}).catch(error => console.error(error))Manejo de errores HTTP
const isOk = response => response.ok ? response.json() : Promise.reject(new Error('Failed to load data from server'))
fetch('https://api.github.com/orgs/nodejs') .then(isOk) // <= Use `isOk` function here .then(data => { console.log(data) // Prints result from `response.json()` }) .catch(error => console.error(error))Ejemplo de CORS
CORS se verifica principalmente en el servidor, así que asegúrate de que la configuración sea correcta del lado del servidor.
La opción credentials controla si tus cookies se incluyen automáticamente.
fetch('https://api.github.com/orgs/nodejs', { credentials: 'include', // Useful for including session ID (and, IIRC, authorization headers)}).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()`}).catch(error => console.error(error))Envío de JSON
postRequest('http://example.com/api/v1/users', {user: 'Dan'}) .then(data => console.log(data)) // Result from the `response.json()` call
function postRequest(url, data) { return fetch(url, { credentials: 'same-origin', // 'include', default: 'omit' method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: JSON.stringify(data), // Use correct payload (matching 'Content-Type') headers: { 'Content-Type': 'application/json' }, }) .then(response => response.json()) .catch(error => console.error(error))}Envío de un <form> HTML
postForm('http://example.com/api/v1/users', 'form#userEdit') .then(data => console.log(data))
function postForm(url, formSelector) { const formData = new FormData(document.querySelector(formSelector))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // a FormData will automatically set the 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}Datos codificados de formulario
Para enviar datos con un Content-Type de application/x-www-form-urlencoded utilizaremos URLSearchParams para codificar la información como una cadena de consulta.
Por ejemplo, new URLSearchParams({a: 1, b: 2}) produce a=1&b=2.
postFormData('http://example.com/api/v1/users', {user: 'Mary'}) .then(data => console.log(data))
function postFormData(url, data) { return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: new URLSearchParams(data), headers: new Headers({ 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }) }) .then(response => response.json()) .catch(error => console.error(error))}Subiendo un archivo
postFile('http://example.com/api/v1/users', 'input[type="file"].avatar') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileField = document.querySelector(fileSelector)
formData.append('username', 'abc123') formData.append('avatar', fileField.files[0])
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}Subiendo varios archivos
Configura un elemento de carga de archivos con el atributo multiple:
<input type='file' multiple class='files' name='files' />Luego úsalo con algo como:
postFile('http://example.com/api/v1/users', 'input[type="file"].files') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileFields = document.querySelectorAll(fileSelector)
// Add all files to formData Array.prototype.forEach.call(fileFields.files, f => formData.append('files', f)) // Alternatively for PHPeeps, use `files[]` for the name to support arrays // Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}Timeouts
Aquí tienes un timeout genérico basado en Promise, usando el patrón de Aplicación Parcial. Funciona con cualquier interfaz Promise. No cargues demasiado trabajo en la cadena de promesas suministrada; seguirá ejecutándose y cualquier error puede generar fugas de memoria a largo plazo.
function promiseTimeout(msec) { return promise => { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')) .then(response => response.json()) .then(data => { console.log(data) // Prints result from `response.json()` in getRequest }) .catch(error => console.error(error)) // Catches any timeout (or other failure)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Alternative example:fetchTimeout(5000, 'https://api.github.com/orgs/nodejs') .then(console.log)// Alternative implementation:function fetchTimeout(msec, ...args) { return raceTimeout(fetch(...args))
function raceTimeout(promise) { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}Y un ejemplo más elaborado, que incluye una bandera de seguimiento __timeout para que puedas interceptar cualquier operación costosa.
function promiseTimeout(msec) { return (promise) => { let isDone = false promise.then(() => isDone = true) const timeout = new Promise((yea, nah) => setTimeout(() => { if (!isDone) { promise.__timeout = true nah(new Error('Timeout expired')) } }, msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()` in getRequest}).catch(error => console.error(error))Helper de Progreso de Descarga
El progreso de carga todavía presenta algunos fallos fuera de Chrome.
La técnica del Manejador de Progreso mostrada a continuación evita envolver la llamada a fetch en un cierre. 👍
progressHelper tiene la siguiente interfaz (código fuente disponible más abajo)
const progressHelper = require('./progressHelper.js')
const handler = ({loaded, total}) => { console.log(`Downloaded ${loaded} of ${total}`)}// handler args: ({ loaded = Kb, total = 0-100% })const streamProcessor = progressHelper(handler)// => streamProcessor is a function for use with the response _stream_Veamos un ejemplo de uso:
// The progressHelper could be inline w/ .then() below...const streamProcessor = progressHelper(console.log)
fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg') .then(streamProcessor) // note: NO parentheses because `.then` needs to get a function .then(response => response.blob()) .then(blobData => { // ... set as base64 on an <img src="base64..."> })Un descargador de imágenes reutilizable podría ser getBlob():
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())Por cierto, un Blob es un Binary Large Object.
Es importante elegir UNA de las 2 patrones de uso a continuación (son funcionalmente equivalentes):
// OPTION #1: no temp streamProcessor varfetch(...) .then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console loggerconst streamProcessor = progressHelper(console.log)fetch(...) .then(streamProcessor)Mi preferencia es Option #1. Sin embargo, el diseño de tu alcance podría obligarte a usar Option #2.
Finalmente, aquí está la última parte de esta receta, nuestro progressHelper:
Source: Progress Helper
function progressHelper(onProgress) { return (response) => { if (!response.body) return response
let loaded = 0 const contentLength = response.headers.get('content-length') const total = !contentLength ? -1 : parseInt(contentLength, 10)
return new Response( new ReadableStream({ start(controller) { const reader = response.body.getReader() return read()
function read() { return reader.read() .then(({ done, value }) => { if (done) return void controller.close() loaded += value.byteLength onProgress({ loaded, total }) controller.enqueue(value) return read() }) .catch(error => { console.error(error) controller.error(error) }) } } }) ) }}credit: Agradecimientos especiales a Anthum Chris y su fantástico PoC de Progress+Fetch mostrado aquí
Recursive Retry Helper
/** * A **Smarter** retry wrapper with currying! */function retryCurry(fn, retriesLeft = 5) { const retryFn = (...args) => fn(...args) .catch(err => retriesLeft > 0 ? retryFn(fn, retriesLeft - 1) : Promise.reject(err) }) return retryFn}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageconst retryGetJson = retryCurry(getJson, 3);
// Now you can pass any arguments through to your function!retryGetJson('https://api.github.com/orgs/elite-libs') .then(console.log) .catch(console.error)/** Basic retry wrapper for Promises */function retryPromise(fn, retriesLeft = 5) { return fn() .catch(err => retriesLeft > 0 ? retryPromise(fn, retriesLeft - 1) : Promise.reject(err) })}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageretry(() => getJson('https://api.github.com/orgs/elite-libs')) .then(console.log) .catch(console.error)Manejo de redirecciones HTTP
const checkForRedirect = (response) => { // Check for temporary redirect (307), or permanent (308) if (response.status === 307 || response.status === 308) { const location = response.headers.get('location') if (!location) { return Promise.reject(new Error('Invalid HTTP Redirect! No Location header.')); } // You can change the behavior here to any custom logic: // e.g. open a "confirm" modal, log the redirect url, etc. return fetch(location) // Bonus: this will handle recursive redirects ✨ .then(checkForRedirect) } return response};
fetch('https://api.github.com/orgs/elite-libs') // Next line will handle redirects .then(checkForRedirect) .then(response => response.json()) .then(console.log) .catch(console.error)Cancelar una solicitud fetch
const httpWithTimeout = (timeout = 5000, url) => { const controller = new AbortController(); // Set an Nsec cancellation timeout const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal }) .then(response => { clearTimeout(timer); // not required but closes open ref return response.text(); }).then(text => { console.log(text); });}Compatibilidad
A partir de 2022, la API fetch está ampliamente soportada en todos los navegadores modernos y en versiones más recientes de NodeJS v18+.
Si necesitas compatibilidad con IE, puedes polyfill fetch usando el paquete github/fetch (mantenido por un equipo excelente en GitHub). Es posible retroceder hasta IE8 — los resultados pueden variar.
Las versiones anteriores de NodeJS pueden aprovechar la API fetch con el paquete node-fetch:
npm install node-fetchDespués de polyfill + node-fetch: 99.99 % compatible ✅
Por favor, tuitea a mí si tienes otros Casos de Uso que te gustaría ver. ❤️