---
title: "TypeError: fetch failed en Node.js: causas y soluciones"
description: "TypeError: fetch failed en Node.js oculta el error real en err.cause. Aprende a leerlo y a corregir causas de DNS, conexión, timeout, certificado y proxy."
url: https://proxynet.io/es/blog/typeerror-fetch-failed
date: 2026-10-06
author: "Acar Diveroli"
category: "Tutoriales, Web scraping"
lang: es
---

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

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.

> **Nota: Respuesta breve**
>
> `TypeError: fetch failed` es lo que lanza el fetch integrado de Node.js ante cualquier fallo anterior a la llegada de una respuesta HTTP; el motivo está en `err.cause`, así que registra eso en lugar de `err.message`. `ENOTFOUND` significa que el nombre de host no se resolvió, `ECONNREFUSED` que nada escucha en el puerto, `ECONNRESET` o `UND_ERR_SOCKET` que la conexión se cortó, y `UND_ERR_CONNECT_TIMEOUT` que no se abrió ninguna conexión en 10 segundos. Detrás de un proxy, fetch ignora `HTTPS_PROXY` salvo que Node.js se ejecute con `NODE_USE_ENV_PROXY=1`. `invalid onError method` significa que un agent del paquete undici de npm no coincide con el undici que trae Node.js.

## ¿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](https://fetch.spec.whatwg.org/#fetch-method) 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](https://github.com/nodejs/undici/blob/main/docs/docs/api/Errors.md) 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 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()](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static), 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](https://nodejs.org/api/http.html#built-in-proxy-support) 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](https://proxynet.io/es/residential-proxy), 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](/es/blog/nodejs-proxy).

## 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](https://github.com/nodejs/Release), 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:

```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](/es/blog/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](/es/blog/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](https://proxynet.io/es/datacenter-proxy) 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.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](/es/proxy).
