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:
[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:
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?
- Registra
err.cause, noerr.message. Si un framework se traga la causa, envuelve tú mismo la llamada. - Lee
cause.code. Los códigos que empiezan porE(ENOTFOUND,ECONNRESET) vienen del sistema operativo, los que empiezan porUND_ERR_vienen de undici, y códigos comoUNABLE_TO_GET_ISSUER_CERT_LOCALLYvienen de la capa TLS. - Comprueba si hay un
AggregateError. Paralocalhost, Node.js prueba varias direcciones. En nuestra prueba,cause.messageestaba vacío ycause.errorscontenía unECONNREFUSEDpara::1y otro para127.0.0.1. - 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 enerr.cause.cause:Proxy response (407) !== 200 when HTTP Tunneling. - 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 mensaje | Qué pasó | Qué revisar primero |
|---|---|---|
ENOTFOUND getaddrinfo ENOTFOUND host | El nombre de host no se resuelve | Ortografía, DNS, VPN |
ECONNREFUSED connect ECONNREFUSED 127.0.0.1:8999 | Nada escucha en ese puerto | Servicio, puerto, puerto del proxy |
ECONNREFUSED dentro de un AggregateError | Lo mismo, para cada dirección de localhost | Inicia el servidor |
UND_ERR_CONNECT_TIMEOUT Connect Timeout Error | Sin conexión TCP en 10 s | Dirección, firewall |
ECONNRESET read ECONNRESET | Cortada con un restablecimiento TCP | Proxy, firewall; reintento |
UND_ERR_SOCKET other side closed | Cerrada antes de cualquier respuesta | Logs del servidor; reintento |
UND_ERR_HEADERS_TIMEOUT Headers Timeout Error | Conectado, pero sin cabeceras a tiempo | Tu límite de tiempo |
UNABLE_TO_GET_ISSUER_CERT_LOCALLY, SELF_SIGNED_CERT_IN_CHAIN | Certificado raíz no confiable | Certificado raíz de la empresa |
UNABLE_TO_VERIFY_LEAF_SIGNATURE | El servidor no envió el intermedio | Cadena de certificados del servidor |
ERR_SSL_WRONG_VERSION_NUMBER | TLS enviado a un puerto HTTP sin cifrar | https:// en la URL del proxy |
UND_ERR_INVALID_ARG invalid onError method | Agent de otra versión mayor de undici | El fetch propio de undici |
| Sin código: Request was cancelled. | El proxy rechazó el túnel | Credenciales 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 ECONNRESETen los tres. - Un servidor que cerró la conexión sin responder produjo
UND_ERR_SOCKET(other side closed) en fetch, ysocket hang upcon el códigoECONNRESETenhttpy 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:
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.
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" NO_PROXY="localhost,127.0.0.1" node app.mjsEn PowerShell:
$env:NODE_USE_ENV_PROXY = "1"
$env:HTTPS_PROXY = "http://user:pass@pr.proxynet.io:8000"
node app.mjsNuestro 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 destinoshttps://. La petición del túnel es HTTP sin cifrar y TLS va dentro de ella; ponerhttps://delante de un proxy HTTP sin cifrar dioERR_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 undici | Node.js 22.23.3 | Node.js 24.21.0 | Node.js 26.10.0 |
|---|---|---|---|
| 5.29.0 | funciona | funciona | invalid onError method |
| 6.29.0 | funciona | funciona | invalid onError method |
| 7.30.0 | funciona | funciona | funciona |
| 8.11.2 | invalid onRequestStart method | invalid onRequestStart method | funciona |
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:
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 localEsto 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.
// 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):
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 200Luego, 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:
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 dicefetch failed. - Esperar que fetch lea
HTTPS_PROXY. No lo hace sinNODE_USE_ENV_PROXY=1o--use-env-proxy. - Pasar un agent de undici de npm al fetch global. Importa
fetchdel mismo paquete. - Desactivar la verificación de certificados.
NODE_TLS_REJECT_UNAUTHORIZED=0desactiva 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
ECONNRESETpuede 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.cause | Qué hacer |
|---|---|
ENOTFOUND | Corrige el nombre de host o el DNS; sin reintento |
ECONNREFUSED con la dirección de tu proxy | Vuelve a copiar el host y el puerto del proxy |
ECONNREFUSED en localhost | Inicia el servidor; revisa el puerto y Docker |
ECONNRESET, UND_ERR_SOCKET | Reintenta las peticiones GET con backoff |
UND_ERR_CONNECT_TIMEOUT | Revisa el firewall; detrás de un proxy corporativo, define NODE_USE_ENV_PROXY=1 |
Espera larga y luego UND_ERR_HEADERS_TIMEOUT | Añade AbortSignal.timeout() |
| Un código de certificado | NODE_EXTRA_CA_CERTS o --use-system-ca |
Proxy response (407) un nivel más abajo | Revisa user:pass o la lista de IP autorizadas |
invalid onError method | Importa 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.




