TypeError: fetch failed en Node.js: causas y soluciones

Publicado:

16 min de lectura

Acar Diveroli
Autor: Acar Diveroli
await fetch(url) con un cursor azul; la traza se corta en un RESET rojo tras TLS y la salida muestra cause: ECONNRESET.

Tu script de Node.js lleva semanas llamando a la misma API cada diez minutos. Lo pasas a un runner de CI, o un compañero lo arranca en la red de la oficina, y de pronto cada llamada termina con una sola línea: TypeError: fetch failed. Sin nombre de host, sin código de estado. La URL se abre en el navegador y curl llega a ella desde la misma máquina, así que el mensaje parece no decir nada.

Sí dice algo, solo que no en el mensaje. Esta guía muestra dónde guarda Node.js el motivo real, las causas que reprodujimos en Node.js 22, 24 y 26, por qué fetch ignora las variables de proxy, la incompatibilidad de undici que rompe los agents de proxy en Node.js 26 y un script probado que nombra la causa.

¿Qué significa «TypeError: fetch failed»?

El fetch() global de Node.js, disponible desde la versión 18, está construido sobre undici, un cliente HTTP que viene dentro de cada versión de Node.js. El estándar Fetch establece que una petición que termina en un error de red se rechaza con un TypeError. Undici usa un único mensaje fijo para todos esos errores, fetch failed, y adjunta el error real como cause.

Un error de red es cualquier cosa que falla antes de una respuesta: la resolución del nombre, la conexión TCP, la negociación TLS, el túnel del proxy o una conexión que se cierra antes de que lleguen las cabeceras. Un 404 o un 500 no lo es; fetch se resuelve con normalidad y res.ok es false.

Cuando nada captura el error, Node.js imprime la causa por sí mismo. Esta es la salida real de un script de una línea con un host mal escrito en Node.js 26.10.0:

text
[TypeError: fetch failed] {
  [cause]: Error: getaddrinfo ENOTFOUND api.example-typo.invalid
      at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:122:26) {
    errno: -3008,
    code: 'ENOTFOUND',
    syscall: 'getaddrinfo',
    hostname: 'api.example-typo.invalid'
  }
}

El problema empieza cuando el código captura el error y solo registra el mensaje, como hacen muchos frameworks y SDK. Imprimir la causa cuesta una línea más:

js
try {
  await fetch("https://api.example-typo.invalid/v1/items");
} catch (err) {
  console.log(err.message);                         // fetch failed
  console.log(err.cause?.code, err.cause?.message); // ENOTFOUND getaddrinfo ENOTFOUND api.example-typo.invalid
}

¿Cómo encuentras el error real detrás de fetch failed?

  1. Registra err.cause, no err.message. Si un framework se traga la causa, envuelve tú mismo la llamada.
  2. Lee cause.code. Los códigos que empiezan por E (ENOTFOUND, ECONNRESET) vienen del sistema operativo, los que empiezan por UND_ERR_ vienen de undici, y códigos como UNABLE_TO_GET_ISSUER_CERT_LOCALLY vienen de la capa TLS.
  3. Comprueba si hay un AggregateError. Para localhost, Node.js prueba varias direcciones. En nuestra prueba, cause.message estaba vacío y cause.errors contenía un ECONNREFUSED para ::1 y otro para 127.0.0.1.
  4. Baja un nivel más en los errores de proxy. Un inicio de sesión rechazado por el proxy dio Request was cancelled. sin código; el estado estaba en err.cause.cause: Proxy response (407) !== 200 when HTTP Tunneling.
  5. Fíjate en el tiempo. Unos milisegundos indican un rechazo, un restablecimiento o una respuesta DNS; unos 10 segundos son el tiempo de espera de conexión; cinco minutos, la espera predeterminada de las cabeceras.

Códigos de err.cause: qué significa cada uno y qué corregir

Generamos cada fila en Windows 11 con Node.js 24.21.0 y 26.10.0, usando un proxy de prueba local y servidores locales que rechazan, restablecen o dejan colgadas las conexiones, o presentan certificados de prueba; ambas versiones dieron los mismos códigos. La referencia de errores de undici documenta los códigos UND_ERR_ y aconseja comparar error.code en lugar de usar instanceof, ya que el dispatcher puede venir de otra copia de undici.

cause.code y mensajeQué pasóQué revisar primero
ENOTFOUND getaddrinfo ENOTFOUND hostEl nombre de host no se resuelveOrtografía, DNS, VPN
ECONNREFUSED connect ECONNREFUSED 127.0.0.1:8999Nada escucha en ese puertoServicio, puerto, puerto del proxy
ECONNREFUSED dentro de un AggregateErrorLo mismo, para cada dirección de localhostInicia el servidor
UND_ERR_CONNECT_TIMEOUT Connect Timeout ErrorSin conexión TCP en 10 sDirección, firewall
ECONNRESET read ECONNRESETCortada con un restablecimiento TCPProxy, firewall; reintento
UND_ERR_SOCKET other side closedCerrada antes de cualquier respuestaLogs del servidor; reintento
UND_ERR_HEADERS_TIMEOUT Headers Timeout ErrorConectado, pero sin cabeceras a tiempoTu límite de tiempo
UNABLE_TO_GET_ISSUER_CERT_LOCALLY, SELF_SIGNED_CERT_IN_CHAINCertificado raíz no confiableCertificado raíz de la empresa
UNABLE_TO_VERIFY_LEAF_SIGNATUREEl servidor no envió el intermedioCadena de certificados del servidor
ERR_SSL_WRONG_VERSION_NUMBERTLS enviado a un puerto HTTP sin cifrarhttps:// en la URL del proxy
UND_ERR_INVALID_ARG invalid onError methodAgent de otra versión mayor de undiciEl fetch propio de undici
Sin código: Request was cancelled.El proxy rechazó el túnelCredenciales del proxy

Hay un mensaje relacionado que no es fetch failed: si llegan las cabeceras pero el cuerpo se detiene, res.text() o res.json() lanza TypeError: terminated, en nuestra prueba con UND_ERR_BODY_TIMEOUT como causa.

ENOTFOUND y ECONNREFUSED: la petición nunca llegó a un servidor

ENOTFOUND viene de la resolución de nombres: el resolutor dijo que el nombre no existe. Busca una errata, una VPN con sus propios servidores DNS o un contenedor que no llega al DNS interno de tu empresa. Detrás de un proxy, es el proxy quien resuelve el nombre, así que un host incorrecto vuelve como código de estado del proxy: nuestro proxy de prueba respondió 502, reportado como Proxy response (502) !== 200 when HTTP Tunneling.

ECONNREFUSED significa que la máquina respondió, pero nada acepta conexiones en ese puerto. Si la dirección del mensaje es la de tu proxy, lo que está mal es la configuración del proxy, no el destino. Con localhost, Node.js prueba tanto ::1 como 127.0.0.1; en nuestra prueba, un servidor vinculado solo a 127.0.0.1 respondió igualmente a http://localhost. Cuando ninguna de las dos responde, el servidor de desarrollo está caído o usa otro puerto, o tu código se ejecuta en Docker, donde localhost es el propio contenedor.

ECONNRESET, «other side closed» y «socket hang up»

Los tres significan que una conexión se abrió y luego se rompió. Comparamos fetch con el módulo http y con Axios 1.20:

  • Un servidor que respondió con un restablecimiento TCP produjo read ECONNRESET en los tres.
  • Un servidor que cerró la conexión sin responder produjo UND_ERR_SOCKET (other side closed) en fetch, y socket hang up con el código ECONNRESET en http y Axios.

Motivos típicos: el servidor se cayó a mitad de la petición, un balanceador de carga o un proxy cerró una conexión keep-alive inactiva justo cuando tu cliente la reutilizaba, o un firewall cortó una conexión larga. Algunos servidores y filtros de bots también cierran las conexiones que no quieren atender; eso significa ir más despacio o pedir acceso, no reintentar con más insistencia.

Reintenta las peticiones idempotentes (GET, HEAD) una o dos veces con una espera creciente. No reintentes a ciegas un POST que crea un pedido: puede que el servidor ya hubiera hecho el trabajo antes de que se rompiera la conexión.

Tiempos de espera: UND_ERR_CONNECT_TIMEOUT, UND_ERR_HEADERS_TIMEOUT y AbortSignal.timeout()

Fetch en Node.js no tiene un límite de tiempo global. Undici abandona un intento de conexión TCP a los 10 segundos, luego espera hasta 300 segundos las cabeceras y hasta 300 segundos entre fragmentos del cuerpo. Un servidor que acepta la conexión y se queda colgado puede retener una petición cinco minutos. Pon tu propio límite:

js
try {
  const res = await fetch("https://api.example.com/v1/items", { signal: AbortSignal.timeout(5_000) });
  console.log(res.status);
} catch (err) {
  if (err.name === "TimeoutError") console.log("gave up after 5 s");
  else console.log(err.message, err.cause?.code);
}

Contra un servidor local que nunca responde, imprimió gave up after 5 s en Node.js 24 y 26. Como explica MDN sobre AbortSignal.timeout(), la señal aborta con un TimeoutError, no con fetch failed, así que comprueba err.name. El límite también cubre el cuerpo: un cuerpo detenido hizo que res.text() lanzara el mismo TimeoutError. Cambiar los límites propios de undici requiere un Agent del paquete de npm, y eso plantea la cuestión de versiones que vemos más abajo.

¿Por qué fetch ignora HTTP_PROXY y HTTPS_PROXY?

curl, pip y muchas otras herramientas leen las variables de entorno del proxy; el fetch de Node.js no. Con HTTPS_PROXY definida, fetch en Node.js 22.23.3, 24.21.0 y 26.10.0 fue directo al destino y el registro de nuestro proxy quedó vacío, mientras que curl sí usó el proxy. Donde solo el proxy llega a internet, la causa nombra entonces el destino (ENOTFOUND, UND_ERR_CONNECT_TIMEOUT), nunca el proxy que se saltó.

El soporte de proxy integrado de Node.js activa esto: NODE_USE_ENV_PROXY=1 (Node.js 22.21.0 y 24.0.0 o posteriores) o la opción --use-env-proxy (22.21.0 y 24.5.0 o posteriores). Node.js lee entonces HTTP_PROXY, HTTPS_PROXY y NO_PROXY al arrancar, para fetch y para los módulos http y https. La documentación todavía marca la función como en desarrollo activo.

bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" NO_PROXY="localhost,127.0.0.1" node app.mjs

En PowerShell:

powershell
$env:NODE_USE_ENV_PROXY = "1"
$env:HTTPS_PROXY = "http://user:pass@pr.proxynet.io:8000"
node app.mjs

Nuestro proxy de prueba registró entonces CONNECT example.com:443 en las tres versiones; Node.js 22 además avisó de que EnvHttpProxyAgent es experimental. Hay tres detalles que suelen despistar:

  • La URL del proxy empieza por http://, incluso para destinos https://. La petición del túnel es HTTP sin cifrar y TLS va dentro de ella; poner https:// delante de un proxy HTTP sin cifrar dio ERR_SSL_WRONG_VERSION_NUMBER.
  • http.setGlobalProxyFromEnv() hace lo mismo desde el código, a partir de Node.js 24.14.0 y 25.4.0; Node.js 22 no la tiene.
  • Una contraseña incorrecta se esconde un nivel más abajo, en err.cause.cause.

La dirección del gateway de un proveedor, como la de nuestros Proxies residenciales, va en la misma variable como http://user:pass@pr.proxynet.io:8000, o sin credenciales cuando la IP de tu servidor ya está en la lista de IP autorizadas.

Para proxies por petición, Axios y SOCKS5, sigue nuestra guía sobre cómo usar un proxy en Node.js.

Node.js 26 y el error «invalid onError method»: la incompatibilidad de versiones de undici

Cada versión de Node.js trae su propio undici, separado del paquete undici de npm: Node.js 22.23.3 incluye undici 6.28.1, 24.21.0 incluye 7.29.1 y 26.10.0 incluye 8.10.2. Undici 8.0.0 eliminó los wrappers que permitían que el código de handlers antiguo se entendiera con el código nuevo. Según el calendario de versiones de Node.js, Node.js 26 pasa a ser la línea Active LTS el 28 de octubre de 2026, así que muchos proyectos se toparán con esto durante la actualización.

El error aparece cuando un ProxyAgent o un Agent del paquete de npm se pasa como dispatcher al fetch global. Probamos cuatro versiones mayores de npm en tres versiones de Node.js:

npm undiciNode.js 22.23.3Node.js 24.21.0Node.js 26.10.0
5.29.0funcionafuncionainvalid onError method
6.29.0funcionafuncionainvalid onError method
7.30.0funcionafuncionafunciona
8.11.2invalid onRequestStart methodinvalid onRequestStart methodfunciona

Cada fallo fue TypeError: fetch failed con UND_ERR_INVALID_ARG como causa. Hay una variante más silenciosa y peor: en Node.js 26, setGlobalDispatcher(new ProxyAgent(...)) de undici 5 o 6 no dio ningún error, fetch lo ignoró y la petición salió directa mientras nuestro proxy no veía nada.

La solución es tomar fetch del mismo paquete que el agent:

js
import { fetch, ProxyAgent } from "undici"; // fetch y el agent del mismo paquete

const proxy = new ProxyAgent("http://user:pass@pr.proxynet.io:8000");
const res = await fetch("https://example.com/", { dispatcher: proxy });
console.log(res.status); // 200 en nuestra prueba, a través de un proxy de prueba local

Esto funcionó con undici 8.11.2 en Node.js 24 y 26. Si la incompatibilidad está en una herramienta que no escribiste, como una CLI que incluye un undici antiguo y crea un agent a partir de tus variables de proxy, actualiza la herramienta o mantenla en Node.js 24 hasta que se corrija.

Errores de certificado detrás de fetch failed

Una verificación TLS fallida también llega como fetch failed, con el código del certificado en cause.code. En el trabajo, el origen habitual es un proxy corporativo o un antivirus que inspecciona HTTPS y vuelve a firmar el tráfico con su propio certificado raíz. Node.js comprueba contra su propia lista integrada de autoridades raíz, no contra la del sistema operativo, así que rechaza esa raíz con UNABLE_TO_GET_ISSUER_CERT_LOCALLY o SELF_SIGNED_CERT_IN_CHAIN.

Apunta NODE_EXTRA_CA_CERTS a un archivo PEM con el certificado raíz de la empresa, o ejecuta Node.js con --use-system-ca (desde 22.15.0 y 23.8.0) si la raíz está instalada en la máquina. En nuestra prueba, NODE_EXTRA_CA_CERTS resolvió el primer caso pero no UNABLE_TO_VERIFY_LEAF_SIGNATURE, en el que el servidor omite su certificado intermedio y solo su propietario puede corregirlo. Las soluciones para npm, Git, Python y curl están en Unable to get local issuer certificate.

Un wrapper de fetch que nombra la causa y reintenta

El script usa el fetch global, así que no necesita paquetes. Recorre toda la cadena de causas, limita cada intento con AbortSignal.timeout() y solo reintenta restablecimientos, sockets cerrados y tiempos de espera agotados, con backoff exponencial más jitter. Los fallos de DNS, los rechazos, los errores de certificado y un inicio de sesión de proxy rechazado fallan igual cada vez, así que se detiene ante ellos. Con 429 y 503 espera lo que indique Retry-After.

Un 429 es el servidor pidiéndote que vayas más despacio; qué significa la cabecera y cómo regular el ritmo de un trabajo se explica en HTTP 429 Too Many Requests.

js
// fetch-check.mjs: nombra la causa real detrás de "TypeError: fetch failed" y reintenta solo lo que puede recuperarse.
// Probado en Node.js 22, 24 y 26. Proxy opcional: NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://user:pass@pr.proxynet.io:8000

const RETRY_CODES = new Set(["ECONNRESET", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT"]);
const CERT_CODES = /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/** El error y cada error envuelto dentro de él, del más externo al más interno. */
function causes(err) {
  const chain = [];
  for (let e = err; e && chain.length < 8; e = e.cause) {
    chain.push(e);
    if (e instanceof AggregateError) chain.push(...e.errors); // "localhost": un error por dirección
  }
  return chain;
}

/** El primer código de error de tipo string en la cadena, como "ECONNRESET". */
const codeOf = (err) => causes(err).map((e) => e.code).find((c) => typeof c === "string") ?? "";

/** Una línea: qué falló y qué revisar primero. */
function explain(err) {
  if (err.name === "TimeoutError") return "no answer before our AbortSignal.timeout(): slow server or proxy";
  const chain = causes(err);
  const text = chain.map((e) => e.message).join(" | ");
  const code = codeOf(err);
  const tunnel = text.match(/Proxy response \((\d{3})\)/);
  if (tunnel?.[1] === "407") return "proxy wants credentials (407): check user:pass or the IP whitelist";
  if (tunnel) return `proxy refused the tunnel (${tunnel[1]}): the proxy could not or would not reach the target`;
  if (code === "ENOTFOUND") return `name ${chain.find((e) => e.hostname)?.hostname} does not resolve: typo, DNS or VPN`;
  if (code === "ECONNREFUSED") return "nothing listens on that address and port: wrong port, service down or wrong proxy port";
  if (code === "ECONNRESET") return "the connection was cut (TCP reset): server, proxy or firewall dropped it";
  if (code === "UND_ERR_SOCKET") return "the other side closed the connection before answering";
  if (code === "UND_ERR_CONNECT_TIMEOUT") return "no TCP connection within 10 s: wrong IP, firewall or blocked outbound port";
  if (code === "UND_ERR_HEADERS_TIMEOUT") return "connected, but no response headers in time";
  if (code === "ERR_SSL_WRONG_VERSION_NUMBER") return "TLS spoken to a plain-HTTP port: the proxy URL should start with http://";
  if (CERT_CODES.test(code)) return `certificate not trusted (${code}): add your CA with NODE_EXTRA_CA_CERTS`;
  return `unrecognised, read the innermost error: ${chain.at(-1).message}`;
}

/** GET con un límite de tiempo estricto, backoff ante cortes de red y Retry-After para 429/503. */
async function getWithRetry(url, { attempts = 3, timeoutMs = 15_000 } = {}) {
  for (let attempt = 1; ; attempt++) {
    const backoff = 500 * 2 ** (attempt - 1) + Math.random() * 250; // 0,5 s, 1 s, 2 s ... más jitter
    let res;
    try {
      res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
    } catch (err) {
      const code = codeOf(err);
      const retryable = RETRY_CODES.has(code) || err.name === "TimeoutError";
      if (!retryable || attempt === attempts) throw err;
      console.log(`  attempt ${attempt}: ${code || err.name}, retrying in ${Math.round(backoff)} ms`);
      await sleep(backoff);
      continue;
    }
    if ((res.status === 429 || res.status === 503) && attempt < attempts) {
      const wait = Number(res.headers.get("retry-after")) * 1000 || backoff; // solo el formato en segundos
      await res.body?.cancel();
      console.log(`  attempt ${attempt}: HTTP ${res.status}, waiting ${Math.round(wait)} ms`);
      await sleep(wait);
      continue;
    }
    return res;
  }
}

for (const url of process.argv.slice(2)) {
  console.log(url);
  try {
    const res = await getWithRetry(url, { timeoutMs: 5_000 });
    console.log(`  OK: HTTP ${res.status}`);
  } catch (err) {
    console.log(`  FAILED: ${err.name}: ${err.message} -> ${explain(err)}`);
  }
}

Cómo es la salida

Lo ejecutamos en Node.js 26.10.0 contra un host mal escrito, un puerto cerrado en localhost y servidores locales que restablecen cada conexión (puerto 8902), nunca responden (8901), usan un certificado de nuestra propia autoridad de prueba (8906) y responden 429 dos veces antes de que la petición salga bien (8910):

text
https://api.example-typo.invalid/
  FAILED: TypeError: fetch failed -> name api.example-typo.invalid does not resolve: typo, DNS or VPN
http://localhost:8999/
  FAILED: TypeError: fetch failed -> nothing listens on that address and port: wrong port, service down or wrong proxy port
http://127.0.0.1:8902/
  attempt 1: ECONNRESET, retrying in 652 ms
  attempt 2: ECONNRESET, retrying in 1115 ms
  FAILED: TypeError: fetch failed -> the connection was cut (TCP reset): server, proxy or firewall dropped it
http://127.0.0.1:8901/
  attempt 1: TimeoutError, retrying in 658 ms
  attempt 2: TimeoutError, retrying in 1024 ms
  FAILED: TimeoutError: The operation was aborted due to timeout -> no answer before our AbortSignal.timeout(): slow server or proxy
https://127.0.0.1:8906/
  FAILED: TypeError: fetch failed -> certificate not trusted (UNABLE_TO_GET_ISSUER_CERT_LOCALLY): add your CA with NODE_EXTRA_CA_CERTS
http://127.0.0.1:8910/items
  attempt 1: HTTP 429, waiting 1000 ms
  attempt 2: HTTP 429, waiting 1000 ms
  OK: HTTP 200

Luego, a través del proxy de prueba local con NODE_USE_ENV_PROXY=1: la contraseña correcta, una incorrecta y https:// delante de la dirección del proxy:

text
https://example.com/
  OK: HTTP 200
https://example.com/
  FAILED: TypeError: fetch failed -> proxy wants credentials (407): check user:pass or the IP whitelist
https://example.com/
  FAILED: TypeError: fetch failed -> TLS spoken to a plain-HTTP port: the proxy URL should start with http://

Node.js 22.23.3 y 24.21.0 imprimieron las mismas líneas, salvo los valores aleatorios del backoff.

Dónde te encuentras este error

  • Next.js y otros frameworks del lado del servidor, cuyas páginas de error a menudo muestran solo el mensaje.
  • Servidores MCP y herramientas de agentes de IA, que a menudo se ejecutan detrás de una VPN o un proxy corporativo.
  • SDK construidos sobre fetch, algunos de los cuales descartaban la causa en versiones antiguas.
  • Runners de CI y builds de Docker que no tienen las variables de proxy ni el certificado raíz de la empresa que sí tiene el host.
  • Trabajos de scraping y de monitorización, que en ejecuciones largas se topan con casi toda la tabla.

Los trabajos largos de recogida de datos son los que más ganan con el wrapper. Si un trabajo así funciona con unos pocos destinos conocidos y necesita IP de salida que se mantengan iguales durante semanas, los Proxies de centro de datos te dan direcciones IPv4 dedicadas con tráfico sin cuota.

Errores comunes

  • Registrar err.message. Siempre dice fetch failed.
  • Esperar que fetch lea HTTPS_PROXY. No lo hace sin NODE_USE_ENV_PROXY=1 o --use-env-proxy.
  • Pasar un agent de undici de npm al fetch global. Importa fetch del mismo paquete.
  • Desactivar la verificación de certificados. NODE_TLS_REJECT_UNAUTHORIZED=0 desactiva la verificación para todo el proceso, así que un atacante en el camino se ve igual que tu proxy corporativo.
  • Reintentarlo todo. Una errata falla igual cada vez; un POST tras ECONNRESET puede ejecutarse dos veces.
  • No poner límite de tiempo. Un servidor colgado puede retener una petición cinco minutos.

Guía de decisión

Lo que ves en err.causeQué hacer
ENOTFOUNDCorrige el nombre de host o el DNS; sin reintento
ECONNREFUSED con la dirección de tu proxyVuelve a copiar el host y el puerto del proxy
ECONNREFUSED en localhostInicia el servidor; revisa el puerto y Docker
ECONNRESET, UND_ERR_SOCKETReintenta las peticiones GET con backoff
UND_ERR_CONNECT_TIMEOUTRevisa el firewall; detrás de un proxy corporativo, define NODE_USE_ENV_PROXY=1
Espera larga y luego UND_ERR_HEADERS_TIMEOUTAñade AbortSignal.timeout()
Un código de certificadoNODE_EXTRA_CA_CERTS o --use-system-ca
Proxy response (407) un nivel más abajoRevisa user:pass o la lista de IP autorizadas
invalid onError methodImporta fetch del paquete undici del agent

Preguntas frecuentes

¿Por qué fetch no lanza un error con un 404 o un 500?

Porque el servidor respondió. Fetch solo rechaza ante errores de red; un error HTTP se resuelve con res.ok en false, así que compruébalo antes de leer el cuerpo.

¿Es «fetch failed» lo mismo que «Failed to fetch» en el navegador?

Están relacionados, pero no son lo mismo. Chrome rechaza con TypeError: Failed to fetch y no le dice a la página por qué, así que un bloqueo de CORS se ve idéntico; abre la pestaña Red en DevTools. Node.js pone el motivo en cause.

¿Cómo pongo un tiempo de espera a fetch en Node.js?

Pasa signal: AbortSignal.timeout(ms). Cuando se dispara, fetch rechaza con un TimeoutError, y el límite también detiene un cuerpo que se ha quedado colgado.

¿El fetch de Node.js usa HTTP_PROXY y HTTPS_PROXY?

Solo con NODE_USE_ENV_PROXY=1 (22.21.0, 24.0.0 y posteriores) o --use-env-proxy (22.21.0, 24.5.0 y posteriores). Si no, las variables se ignoran en silencio.

¿Por qué curl funciona cuando fetch falla en la misma máquina?

Normalmente por uno de dos motivos: curl lee las variables de proxy y fetch no, y muchas compilaciones de curl, incluida la de Windows, usan el almacén de certificados del sistema, mientras que Node.js usa su propia lista salvo que pases --use-system-ca.

¿Puede un proxy solucionar «TypeError: fetch failed»?

Solo cuando la causa es la ruta de red, como en una red corporativa donde solo el proxy llega a internet. No arregla una errata, un problema de certificado ni un servidor caído, y no es una forma de esquivar un sitio que te corta la conexión o te aplica un límite de solicitudes: ve más despacio, usa su API oficial o pide acceso.

En resumen

TypeError: fetch failed es solo un envoltorio: el motivo está en err.cause, a veces un nivel más abajo. Lee el código, corrige lo que señala, da a cada llamada un AbortSignal.timeout() y reintenta solo los restablecimientos y los tiempos de espera agotados. Detrás de un proxy, define NODE_USE_ENV_PROXY=1, mantén http:// en la URL del proxy y toma fetch y su agent del mismo paquete undici, sobre todo en Node.js 26. Cuando tu código esté listo para un proxy, compara las opciones en nuestra página de servicios de proxy.