# Corregir TypeError: fetch failed de Node.js detrás de un proxy

Source: https://ipvolt.com/es/guides/fix-node-fetch-failed-proxy
Markdown: https://ipvolt.com/es/guides/fix-node-fetch-failed-proxy.md
Language: es

[Inicio de ipvolt](https://ipvolt.com/es.md) / [Guías](https://ipvolt.com/es/guides.md) / Corregir TypeError: fetch failed de Node.js detrás de un proxy

Solución de problemas
Revisado: 2026-09-24
Publicado: 2026-09-24
22 min de lectura
Por ipvolt

Descifra TypeError: fetch failed de Node.js tras un proxy: imprime la cadena err.cause, corrige un HTTPS_PROXY ignorado o el desfase de undici y lee un 403, 407 o 502.

`TypeError: fetch failed` no es el error en sí. Es la envoltura que el `fetch` integrado de Node.js (undici) pone alrededor de cada fallo a nivel de red. El motivo está en `err.cause`, y a veces un nivel más abajo. Detrás de un proxy, las causas se dividen en cuatro grupos:

- **El proxy nunca se usó.** El `fetch` integrado ignora `HTTPS_PROXY` hasta que lo activas con `NODE_USE_ENV_PROXY=1`, `--use-env-proxy` o `http.setGlobalProxyFromEnv()`, y Node 26 ignora un dispatcher global establecido por undici 5, 6 o 7 anterior a 7.27.0. Ves `getaddrinfo ENOTFOUND`, un error de conexión que nombra la dirección del destino, o una respuesta normal que el proxy nunca registró.
- **Desfase de versiones en torno a undici 8.** Un dispatcher de undici 5 o 6 pasado por petición al `fetch` de Node 26, o un dispatcher de undici 8 pasado al `fetch` de Node 22 o 24, falla con `invalid onError method` o `invalid onRequestStart method` antes de que ningún byte llegue al proxy.
- **El proxy rechazó la petición.** `Proxy response (NNN) !== 200 when HTTP Tunneling` está dos niveles más abajo, con el estado del proxy, como 403, 407, 429 o 502. Las URL `http://` planas en undici 8.7 y posteriores se comportan de otra forma.
- **Un proxy inalcanzable o que falla.** `ECONNREFUSED`, `ETIMEDOUT`, `UND_ERR_CONNECT_TIMEOUT` o `ENOTFOUND` nombrando al proxy, `UND_ERR_PRX_CONN`, `ECONNRESET`, un error TLS, o ningún error en absoluto.

Imprime toda la cadena, busca el mensaje más profundo en las tablas de abajo y aplica la solución de esa capa. Salvo la única fila marcada como reportada, todos los mensajes de error de las tablas se observaron en 1.965 celdas de laboratorio en loopback que ipvolt ejecutó el 23 y el 24 de septiembre de 2026 con Node.js v22.23.3, v24.21.0 y v26.10.0 y npm undici de 5.29.0 a 8.11.0.

Un 4xx o 5xx del destino nunca produce `fetch failed`; se resuelve como una `Response`. El `TypeError: Failed to fetch` del navegador es un error distinto, lanzado ante fallos de red y de CORS cuyos detalles JavaScript no puede ver.

## Qué significa «TypeError: fetch failed» en Node.js

undici rechaza los errores de red con `new TypeError('fetch failed', { cause: response.error })`. La [misma línea](https://github.com/nodejs/undici/blob/v8.10.2/lib/web/fetch/index.js#L272) está en el primer y en el último undici incluidos en cada uno de Node 22, 24 y 26 (seis versiones, de la 6.11.1 a la 8.10.2). La causa puede ser un error de undici con un `code` estable como `UND_ERR_INVALID_ARG`, un error de sistema de Node.js como `ECONNREFUSED`, o una `DOMException` con el mensaje `Request was cancelled.` que envuelve el error real un nivel más abajo, como ocurrió con cada CONNECT rechazado en el laboratorio. Tu propio `AbortSignal.timeout()` es la excepción: rechaza con un `TimeoutError` a secas que no tiene envoltura `fetch failed` ni causa.

## Imprime toda la cadena de causas

Varias formas habituales de registrar el error se detienen antes del mensaje más profundo. Con el proxy 407 del laboratorio en los tres runtimes, `err.message` mostró solo `fetch failed`, `err.cause.message` se detuvo en `Request was cancelled.` y `JSON.stringify(err)` imprimió `{}`. `console.log(err)` sí llegó al 407, pero con trazas de pila y sin enmascarar nada. Recorre la cadena en su lugar con [print-cause.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/print-cause.mjs), la utilidad de impresión que registró cada celda del laboratorio:

```js
// print-cause.mjs: walk err.cause and report name, code and message at each depth.
// Reads only name, code, message, cause and an AggregateError's errors (never headers,
// bodies or stacks). Masks URL userinfo, Bearer tokens and key:value or key=value pairs
// whose key names a credential. Regex masking is not exhaustive: review before sharing.
const KEY = String.raw`[\w-]*(?:authorization|cookie|token|secret|passw(?:or)?d|api[-_]?key)[\w-]*`;
const mask = (s) => String(s)
  .replace(/\/\/[^/\s]*@/g, '//***@')
  .replace(/\bBearer\s+[\w.~+/-]+=*/gi, 'Bearer ***')
  .replace(new RegExp(String.raw`(["']?\b${KEY}["']?\s*[:=]\s*)(["']?)(?:(?:Basic|Bearer|Digest)\s+)?[^\s"'&,;]+`, 'gi'), '$1$2***');

export function causeChain(err, maxDepth = 10) {
  const chain = [];
  for (let e = err, depth = 0; e != null && depth < maxDepth; e = e.cause, depth++) {
    const entry = { depth, name: e.name ?? typeof e, code: typeof e.code === 'string' ? e.code : undefined, message: mask(e.message ?? e).slice(0, 300) };
    if (Array.isArray(e.errors)) entry.errors = e.errors.slice(0, 5).map((x) => `${x?.code ?? x?.name}: ${mask(x?.message ?? x).slice(0, 200)}`);
    chain.push(entry);
  }
  return chain;
}

export function printCauseChain(err, log = console.error) {
  for (const { depth, name, code, message, errors } of causeChain(err)) {
    log(`${'  '.repeat(depth)}[${depth}] ${name}${code ? ` (${code})` : ''}: ${message}${errors ? ` [${errors.join('; ')}]` : ''}`);
  }
}
```
Llámala en el bloque `catch`, por ejemplo `try { await fetch(url) } catch (err) { printCauseChain(err); process.exitCode = 1; }`. No relances `err` y lo dejes sin capturar: Node imprime entonces el error completo por sí mismo, incluido un `[cause]` sin enmascarar, como confirmó el laboratorio con una contraseña desechable. Esta es la salida para el proxy 407 del laboratorio en Node v26.10.0 con `NODE_USE_ENV_PROXY=1`:

```text
[0] TypeError: fetch failed
  [1] Error: Request was cancelled.
    [2] AbortError (UND_ERR_ABORTED): Proxy response (407) !== 200 when HTTP Tunneling
```

Lee la salida antes de compartirla: todo lo que las máscaras no reconozcan, como `user:password@host` sin un esquema delante, pasa sin cambios.

Si tu código se ramifica según el error, compara `err.code`, no `instanceof`. La [referencia de errores](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/Errors.md) de undici lo recomienda porque «el dispatcher (global) incluido puede proceder de una versión de undici distinta de la que importas directamente». Con `UND_ERR_INVALID_ARG`, comprueba también el mensaje: en el laboratorio significó desfase de versiones o un 407 del proxy en una URL `http://` plana.

## Encuentra tu error: mensaje, profundidad, capa y solución

La profundidad 0 es `TypeError: fetch failed`, y cada mensaje está en la profundidad 1, salvo que una fila indique lo contrario. «El proxy no registró nada» significa que el proxy del laboratorio no vio ninguna conexión para ese fetch.

### Falló antes de llegar al proxy

| Lo que ves | Capa | Solución |
| --- | --- | --- |
| `invalid onError method` (`UND_ERR_INVALID_ARG`), el proxy no registró nada | Un dispatcher de undici 5 o 6 entregado al `fetch` de Node 26 | Un dispatcher de undici 7 u 8, el `fetch` propio de undici, o ningún dispatcher más `NODE_USE_ENV_PROXY=1` |
| `invalid onRequestStart method` (`UND_ERR_INVALID_ARG`), el proxy no registró nada | Un dispatcher de undici 8 entregado al `fetch` de Node 22 o 24 | `Dispatcher1Wrapper`, el `fetch` propio de undici 8 o `setGlobalDispatcher()`, o un agente de undici 7 |
| `getaddrinfo ENOTFOUND` nombrando al destino, el proxy no registró nada | El proxy nunca se usó; la resolución DNS directa falló | Activa el proxy de entorno. Para un dispatcher global, actualiza undici a 7.27.0+ u 8.0.1+ |
| `getaddrinfo ENOTFOUND` nombrando al host del proxy | El nombre de host del proxy no se resuelve en esta máquina | Revisa el host en la URL del proxy, y el DNS o la VPN que deberían resolverlo |
| `connect ECONNREFUSED` o `connect ETIMEDOUT` con la dirección del destino, el proxy no registró nada | El proxy nunca se usó; la conexión directa falló | Como arriba |
| `Connect Timeout Error` (`UND_ERR_CONNECT_TIMEOUT`) listando las direcciones del destino, reportado en [undici#4960](https://github.com/nodejs/undici/issues/4960) | Lo mismo, reportado por el timeout de conexión de undici | Como arriba |
| Sin error y una respuesta normal, el proxy no registró nada | El proxy se omitió silenciosamente | Como arriba |
| `Setting the TLS ServerName to an IP address is not permitted` (`ERR_INVALID_ARG_VALUE`), en Node 26 | Una URL de proxy `https://` con una dirección IP, que Node 25 y posteriores rechazan | `http://` para un proxy HTTP plano, o un nombre de host que cubra el certificado del proxy HTTPS |

### Falló en el proxy o más allá

| Lo que ves | Capa | Solución |
| --- | --- | --- |
| Profundidad 2 `Proxy response (NNN) !== 200 when HTTP Tunneling` bajo la profundidad 1 `Request was cancelled.` | El proxy rechazó el CONNECT con el estado NNN | Lee NNN: 407 son credenciales ([corrige el error de proxy 407](/es/guides/fix-proxy-error-407)), 403 su política, 429 su límite de peticiones, 502 a 504 problemas en el proxy o más allá |
| `Proxy Authentication Required (407)` (`UND_ERR_INVALID_ARG`) | Un 407 para una URL `http://` plana que undici 8.7 o posterior reenvió sin CONNECT | La misma solución de credenciales; este `UND_ERR_INVALID_ARG` no es desfase de versiones |
| Sin error, pero `response.status` es 403, 429, 502 o 503, y el origen nunca registró la petición | El error propio del proxy para una URL `http://` reenviada (undici 8.7 y posteriores) | Busca la firma del proxy en el cuerpo y las cabeceras |
| `connect ECONNREFUSED` más la dirección del proxy | Nada escucha en el host y el puerto del proxy | Revisa el host, el puerto y que el proxy esté en ejecución |
| `AggregateError` (`ECONNREFUSED`) con un mensaje vacío y una dirección rechazada por cada familia de IP | Lo mismo, para un nombre de host de proxy como `localhost` | Como arriba |
| `connect ETIMEDOUT` más la dirección del proxy | El handshake TCP con el proxy nunca se completó | Revisa la ruta y los cortafuegos hacia el proxy |
| `Connect Timeout Error` (`UND_ERR_CONNECT_TIMEOUT`) con la dirección del proxy | El mismo fallo, reportado por el timeout de conexión de undici | Como arriba |
| `ERR_SSL_WRONG_VERSION_NUMBER`, con `wrong version number` en el mensaje | La URL del proxy dice `https://`, pero el proxy habla HTTP plano | Usa `http://` en la URL del proxy |
| `Proxy Connection failed` (`UND_ERR_PRX_CONN`) sobre la profundidad 2 `other side closed` | El proxy cerró la conexión sin responder al CONNECT (undici 8.6 y posteriores) | Revisa el puerto, el protocolo y si el proxy admite CONNECT |
| `other side closed` (`UND_ERR_SOCKET`) para una URL `http://` | El proxy cerró una petición reenviada sin responder (undici 8.7 y posteriores) | Revisa el puerto y el protocolo, y después el registro del proxy |
| Sin error, y el fetch nunca termina, mientras el proxy registra CONNECT tras CONNECT | El mismo fallo en undici 8.5 y anteriores: un bucle de reconexión | Un plazo fijado por quien llama lo convierte en un `TimeoutError`; undici 8.6 o posterior lo convierte en `UND_ERR_PRX_CONN` |
| `Client network socket disconnected before secure TLS connection was established` (`ECONNRESET`) | El proxy respondió 200 y después cortó el túnel | Mira más allá del proxy: su upstream o el destino |
| `SELF_SIGNED_CERT_IN_CHAIN` o `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | Un proxy que inspecciona TLS presentó un certificado de una CA en la que Node no confía | Confía en esa CA con `NODE_EXTRA_CA_CERTS`, o con `--use-system-ca` si está en el almacén de confianza del sistema operativo |
| Sin error durante unos cinco minutos, y después profundidad 1 `Headers Timeout Error` (`UND_ERR_HEADERS_TIMEOUT`) | El proxy aceptó el CONNECT y nunca respondió | Fija tu propio plazo |
| Profundidad 0 `TimeoutError: The operation was aborted due to timeout`, sin causa | Tu `AbortSignal.timeout()` se disparó | Revisa el registro del proxy para ver la etapa pendiente |

En cada celda que llegó hasta el comportamiento inyectado del proxy, 449 en la matriz principal y 803 en la suite de comprobaciones, la capa que el laboratorio dedujo únicamente a partir de los mensajes y de los contadores del proxy coincidió con ese comportamiento.

## HTTPS_PROXY está definida pero fetch la ignora (NODE_USE_ENV_PROXY)

Con `HTTPS_PROXY` definida y sin activación, los tres runtimes del laboratorio conectaron directamente, y `HTTP_PROXY` con URL `http://` se comportó igual; un destino alcanzable sin el proxy devolvió 200 mientras el proxy no registraba nada. El [código de arranque](https://github.com/nodejs/node/blob/v26.10.0/lib/internal/process/pre_execution.js#L314-L333) de Node instala un dispatcher de proxy solo cuando la activación está encendida y una de `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy` o `https_proxy` está definida. `ALL_PROXY` no cuenta.

| Activación | Documentada desde | Laboratorio: v22.23.3 / v24.21.0 / v26.10.0 |
| --- | --- | --- |
| [`NODE_USE_ENV_PROXY=1`](https://nodejs.org/api/cli.html#node_use_env_proxy1) | v24.0.0, v22.21.0 | pasó por el proxy en los tres |
| [`node --use-env-proxy`](https://nodejs.org/api/cli.html#--use-env-proxy) | v24.5.0, v22.21.0 | pasó por el proxy en los tres |
| [`http.setGlobalProxyFromEnv()`](https://nodejs.org/api/http.html#httpsetglobalproxyfromenvproxyenv) | v24.14.0, v25.4.0 | no es una función en v22.23.3; pasó por el proxy en los otros dos |
| npm undici `setGlobalDispatcher(new EnvHttpProxyAgent())` | exportado por 6.28.1 y por todos los undici 7 y 8 del laboratorio | pasó por el proxy, salvo en v26.10.0 con 6.28.1, 7.16.0 o 7.26.0, que conectaron directamente |

- Las versiones más antiguas no tienen la activación; usa ahí un dispatcher o el `fetch` propio de undici.
- Incluye el esquema. Con la activación encendida, un valor sin `http://`, como `127.0.0.1:<port>` o `localhost:<port>`, hizo que los tres runtimes salieran al arrancar con `TypeError: Invalid URL` o `Invalid URL protocol`, antes de que se ejecutara ningún código de la aplicación. Solo están documentadas las URL de proxy `http://` y `https://`; SOCKS5 sigue siendo un punto abierto en el [issue de seguimiento de proxies](https://github.com/nodejs/node/issues/57872) de Node.
- Node analiza las variables «durante el arranque», según la documentación de la CLI, así que definir `process.env.HTTPS_PROXY` desde el código más tarde no sirve. `http.setGlobalProxyFromEnv()` es la alternativa en tiempo de ejecución.
- En v22.23.3, la activación imprimió `[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental`, un aviso de estabilidad, no un error.
- Sin `NO_PROXY` definida, incluso `https://127.0.0.1` pasó por el proxy. Lo que `NO_PROXY` exime varía entre clientes; consulta la [matriz de coincidencia de NO_PROXY](/es/blog/no-proxy-matching-tested).
- Un `dispatcher` por petición se impone a la activación para su petición: con `HTTPS_PROXY` apuntando a un puerto muerto y el agente al proxy que funcionaba, todas las peticiones que se ejecutaron usaron el agente.

En el registro del proxy, una URL `http://` plana en undici 8.7 y posteriores, incluido el proxy de entorno de Node 26.5 y posteriores, no muestra ningún CONNECT, solo la petición en sí en forma absoluta, como `GET http://origin.test/…`. Para las mismas variables en curl y Python, consulta [variables de entorno de proxy](/es/guides/proxy-environment-variables); para un `ProxyAgent` explícito, [usar un proxy con fetch de Node.js](/es/guides/nodejs-fetch-proxy).

## invalid onError method: un dispatcher de undici antiguo en Node 26

[undici 8.0.0](https://github.com/nodejs/undici/releases/tag/v8.0.0) eliminó las envolturas de handlers heredadas y renombró los callbacks del handler, por ejemplo `onError` a `onResponseError` ([guía de migración](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/migrating-from-v7-to-v8.md)). Todas las versiones de Node 26 incluyen undici 8, así que su `fetch` entrega a los dispatchers un handler con solo los callbacks nuevos. En undici 6, la primera comprobación fallida, como `invalid onConnect method`, va a la [ruta de error del dispatch](https://github.com/nodejs/undici/blob/v6.28.1/lib/dispatcher/dispatcher-base.js#L168-L196), que llama a `handler.onError`; ese método ya no existe, así que undici lanza `invalid onError method` y el mensaje original se pierde.

Los `ProxyAgent` de npm undici 5.29.0 y 6.28.1 pasados como `dispatcher` al `fetch` de v26.10.0 fallaron en las 18 celdas, antes de ninguna conexión, y añadir `NODE_USE_ENV_PROXY=1` no ayudó:

```text
[0] TypeError: fetch failed
  [1] InvalidArgumentError (UND_ERR_INVALID_ARG): invalid onError method
```

[xen-orchestra#10411](https://github.com/vatesfr/xen-orchestra/issues/10411) reporta `UND_ERR_INVALID_ARG` para un `EnvHttpProxyAgent` de undici 6.28.1 pasado al `fetch` de Node 26.

La ruta global falla de forma más silenciosa. En v26.10.0, `setGlobalDispatcher()` de npm undici 5.29.0 (con un `ProxyAgent`; esa versión no tiene `EnvHttpProxyAgent`), 6.28.1, 7.16.0 o 7.26.0 (con un `ProxyAgent` o un `EnvHttpProxyAgent`) no tuvo ningún efecto en el `fetch` integrado. Las peticiones fueron directas y terminaron en `ENOTFOUND`, o en un 200 con cero CONNECT, mientras que el mismo código pasó por el proxy en v22.23.3 y v24.21.0. Estas versiones, como las demás etiquetas de undici 7 anteriores a 7.27.0 cuyo `lib/global.js` se revisó (7.0.0, 7.10.0 y 7.25.0), guardan el dispatcher global solo bajo `Symbol.for('undici.globalDispatcher.1')`, que el `fetch` de v26.10.0 ignoró. undici 7.27.0, publicado el 2026-06-01 con el [PR #5319](https://github.com/nodejs/undici/pull/5319), escribe tanto `.1` como `.2`, igual que 7.29.1 y 8.11.0; con esos tres el dispatcher global pasó por el proxy en los tres runtimes. El `EnvHttpProxyAgent` de undici 6.28.1 siguió imprimiendo su aviso experimental en v26.10.0, así que el aviso no demuestra que el agente esté en uso.

## invalid onRequestStart method: un dispatcher de undici 8 en Node 22 o 24

Un `ProxyAgent` de npm undici 8.11.0 pasado por petición al `fetch` integrado de v22.23.3 o v24.21.0 falló en las 18 celdas con `InvalidArgumentError (UND_ERR_INVALID_ARG): invalid onRequestStart method` en la profundidad 1, de nuevo antes de ninguna conexión. El `fetch` de Node 22 y 24 construye un handler heredado, y undici 8 [rechaza cualquier handler](https://github.com/nodejs/undici/blob/v8.10.2/lib/core/util.js#L567-L605) sin `onRequestStart`. [shadcn-vue#1959](https://github.com/unovue/shadcn-vue/issues/1959) reporta el mismo mensaje desde una CLI que incluye undici 8.10.2 en Node 24.

En v22.23.3 y v24.21.0, envolver el agente en `Dispatcher1Wrapper`, el puente documentado de undici 8 para consumidores heredados, llegó al proxy:

```js
import { Dispatcher1Wrapper, ProxyAgent } from 'undici'; // undici 8
const dispatcher = new Dispatcher1Wrapper(new ProxyAgent(proxyUrl));
const response = await fetch(url, { dispatcher });
```

También lo hizo `setGlobalDispatcher(new ProxyAgent(proxyUrl))` de undici 8.11.0, que además guarda una copia envuelta en la ranura heredada que lee el `fetch` integrado. undici 8.0.0 no lo hacía, así que en Node 24 y 25 el `fetch` integrado lo ignoró; el [PR #4962](https://github.com/nodejs/undici/pull/4962) lo corrigió en 8.0.1 el 2026-04-03.

## Elige una solución: alinea undici con process.versions.undici

Comprueba primero el runtime con `node -p process.versions.undici` ([documentación de Node.js](https://nodejs.org/api/globals.html#custom-dispatcher)). Según el [árbol de fuentes de Node](https://github.com/nodejs/node/blob/v26.10.0/src/undici_version.h) en cada etiqueta de versión, todas las versiones 22.x incluyen undici 6, todas las 24.x undici 7 y todas las 26.x undici 8. En el laboratorio, un `dispatcher` por petición de npm undici 5.29.0 o 6.28.1 llegó al proxy en v22.23.3 y v24.21.0 pero falló en v26.10.0; uno de undici 7.16.0, 7.26.0, 7.27.0 o 7.29.1 llegó en los tres; y uno de 8.11.0 llegó solo en v26.10.0. Las soluciones, y lo que cuesta cada una:

1. **Usa el `fetch` propio de undici con su propio dispatcher.** Funcionó con las cuatro versiones principales de npm en todos los runtimes del laboratorio. Los costes: una dependencia más, y las clases de cuerpo como `FormData` deben proceder del mismo paquete ([documentación de undici](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/undici-vs-builtin-fetch.md)).
2. **Elimina el dispatcher personalizado y usa la activación del runtime.** Pasó por el proxy en todos los runtimes que la tienen, pero afecta a todo el proceso, cubre solo proxies HTTP(S), y `NO_PROXY` sigue al undici incluido. [Corepack 0.35.0](https://github.com/nodejs/corepack/releases/tag/v0.35.0) tomó esta vía.
3. **Iguala la versión mayor.** Un npm undici con la misma versión mayor que `process.versions.undici` llegó al proxy en los tres runtimes. Un agente de undici 7 por petición también fue aceptado por los tres, pero eso es una instantánea, no una promesa sobre futuras líneas de Node.
4. **`setGlobalDispatcher()` de undici 7.27.0 o un 7.x posterior, o de 8.0.1 y posteriores.** Con 7.27.0, 7.29.1 y 8.11.0, pasó por el proxy en los tres runtimes, para cada `fetch` del proceso. Con undici 5, 6 o 7 anterior a 7.27.0 en Node 26, se ignora, así que actualiza undici a 7.27.0+ u 8.0.1+. Prefiere el 7.x más reciente: el 24 de septiembre de 2026, `npm audit` también marcó 7.27.0.
5. **`Dispatcher1Wrapper` (undici 8).** Pasó por el proxy por petición en los tres runtimes.
6. **[`install()`](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/GlobalInstallation.md) (undici 7.11.0 y posteriores).** Reemplaza `globalThis.fetch` y los globales relacionados con la copia de npm. Solo documentado; el laboratorio no lo ejecutó.

## Proxy response (NNN) !== 200 when HTTP Tunneling

Cuando el proxy responde al CONNECT con cualquier cosa que no sea 200, el estado del proxy aparece solo en la profundidad 2:

```text
[0] TypeError: fetch failed
  [1] Error: Request was cancelled.
    [2] AbortError (UND_ERR_ABORTED): Proxy response (403) !== 200 when HTTP Tunneling
```

En el laboratorio, todas las combinaciones que llegaron a un proxy que respondía al CONNECT con 403, 407, 429, 502 o 503 produjeron esta cadena con ese estado, 49 celdas `https://` por estado. Según el [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6), cualquier respuesta distinta de un 2xx significa que el túnel nunca se formó. undici [descarta](https://github.com/nodejs/undici/blob/v8.10.2/lib/dispatcher/proxy-agent.js#L229-L233) el resto de la respuesta, incluidos `Retry-After` y el cuerpo. Para verlos, ejecuta curl a través del mismo proxy, por ejemplo `curl -v -x http://PROXY_HOST:PORT https://DESTINATION/ -o /dev/null`. Contra el proxy 429 del laboratorio, curl 8.7.1 y 8.22.0 imprimieron su `Retry-After: 30`. La [guía base de curl](/es/guides/curl-proxy-setup) cubre esa configuración.

| Estado | Qué está diciendo el proxy | Dónde mirar |
| --- | --- | --- |
| 407 | Quiere credenciales de proxy | [Corrige el error de proxy 407](/es/guides/fix-proxy-error-407) |
| 403 | Su política rechaza la petición; el RFC 9110 pide a los proxies limitar el CONNECT a puertos conocidos o destinos permitidos | Las reglas de permiso del proxy para este destino y puerto |
| 429 | Un límite de peticiones; el estado por sí solo no dice de quién ([RFC 6585](https://www.rfc-editor.org/rfc/rfc6585#section-4)) | Reduce el ritmo; lee `Retry-After` con `curl -v` |
| 502 o 504 | Su upstream envió una respuesta incorrecta, o ninguna a tiempo | Reintenta dentro de un presupuesto; comprueba el destino desde el lado del proxy |
| 503 | Temporalmente no puede atender la petición | Reintenta más tarde, respetando cualquier `Retry-After` |

undici construye el mismo mensaje a partir de cualquier estado distinto de 200. [Errores de proxy explicados](/es/blog/proxy-status-codes-407-429-502) trata cada estado con más profundidad.

Una URL `http://` plana depende de qué undici posee el agente. undici 8.6 y anteriores, incluido el proxy de entorno de Node 22, 24 y de 26.0 a 26.4, envían `CONNECT origin.test:80` y fallan con la misma cadena (en el laboratorio, todas las rutas de undici 7.29.1 y anteriores, 33 celdas por estado). undici 8.7 y posteriores, incluido el proxy de entorno de Node 26.5 y posteriores, reenvían la petición en sí sin CONNECT ([PR #5116](https://github.com/nodejs/undici/pull/5116)). De esas peticiones reenviadas (16 celdas por estado), un 407 volvió un nivel más abajo como `InvalidArgumentError (UND_ERR_INVALID_ARG): Proxy Authentication Required (407)`, el mismo código que usa el desfase de versiones. Un 403, 429, 502 o 503 no lanzó nada en absoluto: `fetch` se resolvió con el estado y el cuerpo del proxy, y el origen nunca vio la petición.

## ECONNREFUSED, ETIMEDOUT o ENOTFOUND: ¿de quién es la dirección?

`connect ECONNREFUSED` seguido de la IP y el puerto del proxy (49 celdas) significa que nada escucha ahí. Para un nombre de host de proxy como `localhost`, Node prueba cada dirección y reporta en su lugar un `AggregateError` con un mensaje vacío (49 celdas):

```text
[0] TypeError: fetch failed
  [1] AggregateError (ECONNREFUSED):  [ECONNREFUSED: connect ECONNREFUSED ::1:<proxy-port>; ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:<proxy-port>]
```

Si la dirección es la del destino, la petición fue directa: con `HTTPS_PROXY` definida y sin activación, un destino que rechazaba la conexión produjo `connect ECONNREFUSED 127.0.0.1:<dest-port>` en los tres runtimes.

`getaddrinfo ENOTFOUND` funciona igual: lee el nombre de host. Con la URL de proxy `http://proxy.invalid:3128`, un nombre reservado que nunca se resuelve, todas las combinaciones que usaron el proxy fallaron con profundidad 1 `getaddrinfo ENOTFOUND proxy.invalid` (49 celdas), la misma forma que el `getaddrinfo ENOTFOUND origin.test` de una omisión del proxy.

`connect ETIMEDOUT` seguido de la dirección del proxy (77 celdas) significa que el handshake TCP con el proxy nunca se completó; el laboratorio usó un listener cuya cola de aceptación estaba llena. macOS normalmente se rindió tras unos 7,8 segundos, antes del timeout de conexión predeterminado de undici. Un destino que nunca aceptaba, detrás de un proxy omitido, produjo el mismo mensaje con la dirección del destino.

## UND_ERR_CONNECT_TIMEOUT: ¿proxy o destino?

El `connectTimeout` de undici es de 10 segundos por defecto ([documentación de Client](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/Client.md)), y en el loopback de macOS el timeout del sistema operativo normalmente llegó primero. Por eso la ejecución publicada muestra `UND_ERR_CONNECT_TIMEOUT` solo con `connectTimeout: 3000` en agentes de undici 8.11.0, tras unos 3,5 segundos (13 celdas):

```text
[0] TypeError: fetch failed
  [1] ConnectTimeoutError (UND_ERR_CONNECT_TIMEOUT): Connect Timeout Error (attempted address: 127.0.0.1:<proxy-port>, timeout: 3000ms)
```

Los `ProxyAgent` de undici 5.29.0, 6.28.1 y 7.29.1 con la misma opción terminaron igualmente en el timeout del sistema operativo (28 celdas), porque construyen la conexión con el proxy solo a partir de las opciones de `proxyTls`. En Linux o en una red real, el temporizador de 10 segundos de undici puede ganar más a menudo; el laboratorio no lo probó. undici 8.10.2 y 8.11.0 también reportan `Connect Timeout Error` cuando todas las direcciones de un nombre de host fallan y una agotó el tiempo, sea cual sea el temporizador que se disparó, con el timeout configurado en el mensaje y el `AggregateError` en la profundidad 2 ([connect.js](https://github.com/nodejs/undici/blob/v8.10.2/lib/core/connect.js#L167-L190)); el laboratorio usó direcciones únicas y nunca produjo esa forma.

Lee la dirección del mensaje. La dirección del proxy significa que el proxy es inalcanzable; la del destino, que la petición nunca usó el proxy, como en [undici#4960](https://github.com/nodejs/undici/issues/4960), donde el mensaje listaba seis direcciones de destino. undici 6 y posteriores imprimen `attempted address:` o `attempted addresses:`; undici 5 imprime un `Connect Timeout Error` a secas, sin dirección, así que compara en su lugar el registro del proxy. Para plazos y presupuestos de reintento, consulta [diagnóstico de timeouts de proxy](/es/guides/proxy-timeout-troubleshooting).

## ERR_SSL_WRONG_VERSION_NUMBER: una URL de proxy https:// para un proxy plano

Un proxy de reenvío HTTP plano sigue transportando destinos `https://` dentro del túnel CONNECT, pero una URL de proxy que empieza por `https://` hace que el cliente abra TLS contra el propio proxy. En el laboratorio, un proxy HTTP plano respondió a ese handshake con un HTTP 400, y todas las combinaciones que llegaron a él fallaron con profundidad 1 `Error (ERR_SSL_WRONG_VERSION_NUMBER)` y un mensaje de OpenSSL que contenía `SSL routines:tls_validate_record_header:wrong version number` (85 celdas).

Con una dirección IP en la URL de proxy `https://`, 13 combinaciones en v26.10.0 fallaron antes de conectar con profundidad 1 `TypeError (ERR_INVALID_ARG_VALUE): The property 'options.servername' Setting the TLS ServerName to an IP address is not permitted.. Received '127.0.0.1'`. Node convirtió en error un nombre de servidor TLS con dirección IP en v25.0.0 ([DEP0123](https://nodejs.org/api/deprecations.html#dep0123-setting-the-tls-servername-to-an-ip-address)). La solución para ambos es `http://` en la URL del proxy, a menos que el proxy realmente sirva TLS en ese puerto.

## UND_ERR_PRX_CONN: el proxy cerró antes de responder al CONNECT

Uno de los proxies de prueba aceptó TCP, leyó el CONNECT y cerró sin responder. El resultado dependió de qué undici poseía el agente del proxy:

- **undici 8.6 y posteriores** (en el laboratorio, todos los agentes de npm 8.11.0 que llegaron al proxy y el proxy de entorno integrado de v26.10.0): profundidad 1 `ProxyConnectionError (UND_ERR_PRX_CONN): Proxy Connection failed` sobre la profundidad 2 `SocketError (UND_ERR_SOCKET): other side closed`, tras un CONNECT (16 celdas).
- **undici 8.5 y anteriores** (en el laboratorio, todos los agentes de npm 5, 6 y 7 que llegaron al proxy y el proxy de entorno integrado de Node 22 y 24): ningún error. El cliente se reconectó de inmediato hasta que el harness lo detuvo en 25 CONNECT (33 celdas).

Desde la aplicación, ese bucle parece un cuelgue: el `fetch` nunca termina. Con `AbortSignal.timeout(2000)` y sin parada, los clientes en bucle rechazaron tras 2 segundos con un `TimeoutError` a secas, y el proxy registró de 18.600 a 20.548 CONNECT para esa única petición en la ejecución publicada (9 celdas, en loopback).

Para una URL `http://` plana, undici 8.7 y posteriores no envían CONNECT, así que el mismo fallo apareció como profundidad 1 `SocketError (UND_ERR_SOCKET): other side closed` (16 celdas), mientras que undici 7 y anteriores entraron en bucle como arriba (33 celdas).

El [PR #5441](https://github.com/nodejs/undici/pull/5441) añadió `ProxyConnectionError` para que estas peticiones fallen en vez de entrar en bucle ([issue #3897](https://github.com/nodejs/undici/issues/3897)). La clase se publicó por primera vez en undici 8.6.0, aunque la referencia de errores de 8.11.0 la etiqueta como v8.10.1, y v26.5.0 es la primera versión de Node 26 cuyo undici incluido la contiene.

## ECONNRESET tras un CONNECT 200: el túnel se cortó

Cuando el proxy respondió 200 y después cerró antes de enviar ningún byte del túnel, todas las combinaciones que llegaron a él (49 celdas) reportaron `Client network socket disconnected before secure TLS connection was established` (`ECONNRESET`) en la profundidad 1. El CONNECT tuvo éxito, así que mira más allá de la puerta de entrada del proxy: su upstream, la salida, el destino, o el propio proxy cortando el túnel. El PR #5441 cubre solo el establecimiento del túnel, así que este caso no se convierte en `UND_ERR_PRX_CONN`.

## SELF_SIGNED_CERT_IN_CHAIN detrás de un proxy que inspecciona TLS

Detrás de un proxy que inspecciona TLS y cuya CA Node no reconoce como de confianza, la cadena termina en un error de certificado. El proxy simulado del laboratorio respondió al CONNECT con 200 y después presentó su propio certificado para el destino, emitido por una CA desechable. Todas las combinaciones que llegaron a él fallaron en la profundidad 1 en los tres runtimes, con `self-signed certificate in certificate chain` (`SELF_SIGNED_CERT_IN_CHAIN`) cuando el proxy enviaba también su certificado de CA (49 celdas) y `unable to verify the first certificate` (`UNABLE_TO_VERIFY_LEAF_SIGNATURE`) cuando enviaba solo su propio certificado (49 celdas).

Apuntar [`NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#node_extra_ca_certsfile) a un archivo que contenía esa CA corrigió las 98 celdas; Node lo lee solo al iniciar el proceso. Si la CA ya está en el almacén de confianza del sistema operativo, el flag [`--use-system-ca`](https://nodejs.org/api/cli.html#--use-system-ca) de Node (documentado desde v22.15.0 y v23.8.0) o `NODE_USE_SYSTEM_CA=1` (desde v22.19.0 y v24.6.0) hace que Node confíe también en ese almacén ([configuración de red empresarial](https://nodejs.org/learn/http/enterprise-network-configuration)); el laboratorio no probó estas opciones. No desactives la verificación con `NODE_TLS_REJECT_UNAUTHORIZED=0`.

## Sin error durante cinco minutos: fija tu propio plazo

Un proxy que aceptó el CONNECT y nunca respondió no produjo ningún error dentro del límite de 15 segundos del harness (49 celdas). En la ejecución larga publicada, las 49 celdas fallaron tras 301 a 302 segundos con `HeadersTimeoutError (UND_ERR_HEADERS_TIMEOUT): Headers Timeout Error`, lo que coincide con el valor predeterminado documentado de `headersTimeout` de undici, 300 segundos. Con `signal: AbortSignal.timeout(2000)`, las mismas combinaciones rechazaron tras unos 2 segundos con un `TimeoutError` a secas. Pasa una señal en cada petición que pase por un proxy.

## Cuando el código que falla es una herramienta que no escribiste

Las CLI, los SDK y los servidores MCP suelen incluir su propio undici y entregar su agente al `fetch` integrado o instalarlo con `setGlobalDispatcher()`. El desfase aparece cuando cualquiera de los dos lados se mueve: una herramienta antigua en Node 26, o una herramienta que adoptó undici 8 en Node 22 o 24.

1. Ejecuta `node -p process.versions.undici` con el mismo binario `node` que usa la herramienta.
2. Encuentra las copias de la herramienta con `npm explain undici`. En el laboratorio, `npm ls undici` imprimió `(empty)` para copias instaladas bajo un alias de npm (`npm:undici@…`) que `npm explain` sí listó. Una copia compilada dentro del bundle de la herramienta no aparece en ninguno de los dos; revisa el changelog o el gestor de issues de la herramienta.
3. Actualiza primero la herramienta. Si su documentación admite la activación de Node, usa `NODE_USE_ENV_PROXY=1`. Si no, ejecútala temporalmente en una línea de Node que acepte su copia (mira las soluciones de arriba) y reporta la cadena impresa a sus desarrolladores.

Estos son ejemplos reportados que ipvolt no reprodujo:

- [vercel/vercel#17629](https://github.com/vercel/vercel/issues/17629), abierto el 2026-09-23: el undici 5.29.0 incluido en la CLI de Vercel falla con `invalid onError method` en Node 26.8.2 cuando hay una variable de proxy definida, y funciona en Node 24.21.0.
- [nodejs/corepack#834](https://github.com/nodejs/corepack/issues/834): un `ProxyAgent` de undici 6 incluido falló en Node 26; Corepack 0.35.0 lo corrigió exigiendo `NODE_USE_ENV_PROXY=1`.
- [openclaw#155840](https://github.com/openclaw/openclaw/issues/155840): `invalid onRequestStart method` desde un dispatcher de undici 8 pasado al WebSocket de undici 7 de una dependencia; el desfase no se limita a `fetch`.

## Método, limitaciones y descargas

ipvolt ejecutó estas comprobaciones localmente el 23 y el 24 de septiembre de 2026 en macOS 15.7.4 (arm64), con los tarballs oficiales de Node.js v22.23.3, v24.21.0 y v26.10.0 y npm undici 5.29.0, 6.28.1, 7.29.1 y 8.11.0, más 7.16.0, 7.26.0 y 7.27.0 para las comprobaciones del dispatcher global. Cada celda ejecutó un `fetch` en su propio proceso hijo a través de un proxy en loopback de un solo comportamiento que contaba conexiones y peticiones. La mayoría de las celdas pidieron el nombre reservado `origin.test` ([RFC 6761](https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2)), que solo el proxy del laboratorio resolvía. La matriz principal tuvo 591 celdas, la suite extra 282 (URL `http://`, una URL de proxy `localhost`, destinos inalcanzables) y la suite de comprobaciones 1.029; una ejecución aparte de 63 celdas dejó correr el proxy que nunca responde durante 330 segundos. El [README](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/README.md) contiene el método completo, los resultados por suite y el historial de ejecuciones.

Una reejecución limpia solo a partir del archivo final, siguiendo el README, clasificó las 591 celdas principales, las 282 extra, las 1.029 de comprobaciones y las 63 de la ejecución larga igual que los resultados publicados. Espera que una reejecución difiera en unas pocas celdas de handshake que nunca se completa, donde compiten los timeouts de conexión del sistema operativo y de undici, y en las comprobaciones loop-deadline cuando se ejecutan una tras otra: una celda que entra en bucle deja miles de sockets en TIME_WAIT, así que la siguiente puede registrar cero CONNECT hasta que los puertos se liberan, unos 40 segundos después.

El laboratorio no cubrió Linux ni redes reales, proxies HTTPS o SOCKS reales, proxies que aceptan credenciales, el 504, la coincidencia de `NO_PROXY`, `http.request`, Deno, Bun ni herramientas reales de terceros. Los resultados muestran cómo reportan estas versiones de Node.js y undici cada comportamiento inyectado, no cómo se comporta ningún proveedor de proxies. El lockfile fija a propósito versiones antiguas de undici, y `npm ci` las reporta como una vulnerabilidad de gravedad alta; no copies estas versiones fijadas en una aplicación.

Todos los archivos están bajo `https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/`:

- [fix-node-fetch-failed-proxy-lab.zip](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/fix-node-fetch-failed-proxy-lab.zip) contiene todos los archivos de abajo.
- [README.md](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/README.md) explica cómo ejecutar el laboratorio y leer una celda; [print-cause.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/print-cause.mjs) es la utilidad de impresión.
- [run-matrix.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/run-matrix.mjs), [run-checks.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/run-checks.mjs), [cell.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/cell.mjs), [cause-forms.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/cause-forms.mjs) y [compare.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/compare.mjs) son el harness.
- [get-runtimes.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/get-runtimes.mjs), [runtimes.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/runtimes.json), [package.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/package.json) y [package-lock.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/package-lock.json) fijan los runtimes y los paquetes.
- [results.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results.json), [results.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results.csv), [results-extra.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-extra.json), [results-extra.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-extra.csv), [results-long-hang.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-long-hang.json), [results-long-hang.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-long-hang.csv), [results-checks.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-checks.json) y [results-checks.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-checks.csv) contienen la cadena de causas y los contadores del proxy de cada celda.

Para reproducir los resultados, descomprime el archivo y sigue el README: `npm ci`, `node get-runtimes.mjs`, después cada suite y `node compare.mjs` contra una copia del archivo publicado. No se necesita ninguna cuenta de proxy.

Si quieres enterarte de cuándo se abre el acceso a ipvolt, [únete a la lista de acceso anticipado](https://ipvolt.com/#waitlist-closing). Un correo cuando se abra el acceso. Nada más.

## Fuentes y lecturas adicionales

- [Node.js HTTP: Built-in Proxy Support and http.setGlobalProxyFromEnv()](https://nodejs.org/api/http.html#built-in-proxy-support)
- [Node.js CLI: NODE_USE_ENV_PROXY=1 and --use-env-proxy](https://nodejs.org/api/cli.html#node_use_env_proxy1)
- [Node.js globals: fetch with a custom dispatcher and process.versions.undici](https://nodejs.org/api/globals.html#custom-dispatcher)
- [Node.js Learn: Enterprise network configuration](https://nodejs.org/learn/http/enterprise-network-configuration)
- [Node.js deprecations: DEP0123, setting the TLS ServerName to an IP address](https://nodejs.org/api/deprecations.html#dep0123-setting-the-tls-servername-to-an-ip-address)
- [undici 8.11.0: Errors reference](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/Errors.md)
- [undici 8.11.0: ProxyAgent (CONNECT for https://, forwarding for http://)](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/ProxyAgent.md)
- [undici: Migrating from undici 7 to 8](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/migrating-from-v7-to-v8.md)
- [undici: Undici module vs. Node.js built-in fetch](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/undici-vs-builtin-fetch.md)
- [undici PR #4962: mirror the legacy global dispatcher for built-in fetch (v8.0.1)](https://github.com/nodejs/undici/pull/4962)
- [undici PR #5319: setGlobalDispatcher() writes both global-dispatcher slots, for Node 26 (v7.27.0)](https://github.com/nodejs/undici/pull/5319)
- [undici PR #5116: auto-detect HTTP proxy tunneling (v8.7.0)](https://github.com/nodejs/undici/pull/5116)
- [undici PR #5441: fail instead of looping when the proxy closes during CONNECT setup (UND_ERR_PRX_CONN, v8.6.0)](https://github.com/nodejs/undici/pull/5441)
- [RFC 9110: CONNECT and status codes 403, 407, 502, 503 and 504](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6)
- [RFC 6585: section 4, 429 Too Many Requests](https://www.rfc-editor.org/rfc/rfc6585#section-4)

## Guías relacionadas

- [Usar un proxy con fetch de Node.js](https://ipvolt.com/es/guides/nodejs-fetch-proxy.md)
- [Variables de entorno de proxy: HTTP_PROXY y NO_PROXY](https://ipvolt.com/es/guides/proxy-environment-variables.md)
- [Diagnostica los timeouts de proxy etapa por etapa](https://ipvolt.com/es/guides/proxy-timeout-troubleshooting.md)

## Sobre ipvolt

Los ejemplos usan configuraciones de proxy genéricas, con enlaces a la documentación técnica original. El comportamiento específico de cada producto debe comprobarse con tu proveedor. ipvolt sigue en desarrollo.

[Leer el original en inglés](https://ipvolt.com/guides/fix-node-fetch-failed-proxy.md)

## Entérate cuando se abra el acceso.

ipvolt · En desarrollo

Estamos construyendo infraestructura de proxies para desarrolladores y equipos de datos. Únete a la lista de interés para recibir un aviso cuando ipvolt esté listo.

Un correo cuando se abra el acceso. Nada más.

[Solicitar acceso anticipado](https://ipvolt.com/es/guides/fix-node-fetch-failed-proxy#waitlist-closing)

[Privacidad](https://ipvolt.com/privacy)

