Fetch avanzado en JavaScript: streaming, AbortController, Headers y estrategias de caché

La Fetch API reemplazó a XMLHttpRequest con una interfaz basada en promesas, más limpia y componible. Pero la mayoría de los tutoriales solo cubren el fetch básico. Para producción necesitas cancelar peticiones que tardan demasiado, leer respuestas grandes sin bloquear la UI, gestionar cabeceras de forma programática y controlar la caché del navegador.

Cancelar peticiones con AbortController

AbortController expone una señal que puedes pasar a fetch y otras APIs. Al llamar a .abort(), la petición se cancela y la promesa rechaza con un AbortError.

async function buscar(termino) {
  const ac = new AbortController();

  // Cancelar peticiones anteriores al iniciar una nueva
  buscar._actual?.abort();
  buscar._actual = ac;

  try {
    const res = await fetch(`/api/buscar?q=${termino}`, {
      signal: ac.signal,
    });
    return await res.json();
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
    return null; // petición cancelada, sin error
  }
}

Timeout propio sobre fetch

Fetch no tiene timeout nativo. Puedes construirlo combinando AbortController con setTimeout:

async function fetchConTimeout(url, opciones = {}, ms = 5000) {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(new Error(`Timeout ${ms}ms`)), ms);

  try {
    const res = await fetch(url, { ...opciones, signal: ac.signal });
    clearTimeout(timer);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res;
  } catch (err) {
    clearTimeout(timer);
    throw err;
  }
}

// Uso
const datos = await fetchConTimeout('/api/lento', {}, 3000).then(r => r.json());

Leer el body como stream para barras de progreso

La respuesta de fetch expone res.body como un ReadableStream. Leerlo manualmente permite calcular el progreso de descarga cuando el servidor envía el header Content-Length:

async function descargarConProgreso(url, onProgreso) {
  const res = await fetch(url);
  const total = +res.headers.get('Content-Length') || 0;
  const reader = res.body.getReader();
  const chunks = [];
  let recibido = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    recibido += value.length;
    if (total) onProgreso(recibido / total);
  }

  return new Blob(chunks);
}

const blob = await descargarConProgreso('/archivo.zip', p =>
  console.log(`${(p * 100).toFixed(1)}%`)
);

Gestionar Headers de forma programática

La clase Headers permite construir, leer, modificar y clonar cabeceras HTTP de forma robusta, sin manipular strings:

const headers = new Headers({
  'Content-Type': 'application/json',
  'Authorization': `Bearer ${token}`,
});

headers.append('X-Custom', 'valor1');
headers.append('X-Custom', 'valor2'); // múltiples valores
console.log(headers.get('X-Custom')); // "valor1, valor2"
console.log(headers.has('Authorization')); // true

// Iterar todas las cabeceras
for (const [nombre, valor] of headers) {
  console.log(`${nombre}: ${valor}`);
}

const res = await fetch('/api/datos', { headers });

Subir ficheros con FormData

FormData permite construir peticiones multipart/form-data para subir ficheros combinados con otros campos del formulario:

const form = document.getElementById('mi-form');
form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const fd = new FormData(form); // captura todos los campos
  fd.append('timestamp', Date.now()); // añadir campo extra

  const res = await fetch('/api/subir', {
    method: 'POST',
    body: fd,
    // NO pongas Content-Type manualmente: fetch lo establece con el boundary
  });
  const { url } = await res.json();
  console.log('Archivo subido a:', url);
});

Controlar la caché del navegador

La opción cache de fetch controla cómo interactúa la petición con la caché HTTP del navegador. Los modos más útiles son no-store (nunca cachear, siempre al servidor), force-cache (usar caché aunque esté obsoleta) y no-cache (revalidar con el servidor antes de usar la caché):

// Siempre del servidor, sin caché
const datos = await fetch('/api/tiempo-real', { cache: 'no-store' });

// Usar caché aunque esté obsoleta (modo offline-first)
const config = await fetch('/api/config', { cache: 'force-cache' });

// Revalidar con el servidor (usa ETag o Last-Modified)
const perfil = await fetch('/api/perfil', { cache: 'no-cache' });

Combinar no-cache con cabeceras de revalidación del servidor (ETag, Last-Modified) es la estrategia más eficiente para datos que cambian ocasionalmente: el navegador hace la petición pero recibe un 304 Not Modified sin cuerpo cuando los datos no han cambiado, ahorrando ancho de banda.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP