Solución de problemas22 min de lectura

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

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.

En esta página

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 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, 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:

code
[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 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 vesCapaSolución
invalid onError method (UND_ERR_INVALID_ARG), el proxy no registró nadaUn dispatcher de undici 5 o 6 entregado al fetch de Node 26Un 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ó nadaUn dispatcher de undici 8 entregado al fetch de Node 22 o 24Dispatcher1Wrapper, el fetch propio de undici 8 o setGlobalDispatcher(), o un agente de undici 7
getaddrinfo ENOTFOUND nombrando al destino, el proxy no registró nadaEl 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 proxyEl nombre de host del proxy no se resuelve en esta máquinaRevisa 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ó nadaEl 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#4960Lo mismo, reportado por el timeout de conexión de undiciComo arriba
Sin error y una respuesta normal, el proxy no registró nadaEl proxy se omitió silenciosamenteComo arriba
Setting the TLS ServerName to an IP address is not permitted (ERR_INVALID_ARG_VALUE), en Node 26Una URL de proxy https:// con una dirección IP, que Node 25 y posteriores rechazanhttp:// 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 vesCapaSolució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 NNNLee NNN: 407 son credenciales (corrige el error de proxy 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 CONNECTLa 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ónEl 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 proxyNada escucha en el host y el puerto del proxyRevisa 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 IPLo mismo, para un nombre de host de proxy como localhostComo arriba
connect ETIMEDOUT más la dirección del proxyEl 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 proxyEl mismo fallo, reportado por el timeout de conexión de undiciComo arriba
ERR_SSL_WRONG_VERSION_NUMBER, con wrong version number en el mensajeLa URL del proxy dice https://, pero el proxy habla HTTP planoUsa http:// en la URL del proxy
Proxy Connection failed (UND_ERR_PRX_CONN) sobre la profundidad 2 other side closedEl 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 CONNECTEl mismo fallo en undici 8.5 y anteriores: un bucle de reconexiónUn 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únelMira más allá del proxy: su upstream o el destino
SELF_SIGNED_CERT_IN_CHAIN o UNABLE_TO_VERIFY_LEAF_SIGNATUREUn proxy que inspecciona TLS presentó un certificado de una CA en la que Node no confíaConfí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 causaTu 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 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ónDocumentada desdeLaboratorio: v22.23.3 / v24.21.0 / v26.10.0
NODE_USE_ENV_PROXY=1v24.0.0, v22.21.0pasó por el proxy en los tres
node --use-env-proxyv24.5.0, v22.21.0pasó por el proxy en los tres
http.setGlobalProxyFromEnv()v24.14.0, v25.4.0no 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 laboratoriopasó 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 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.
  • 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; para un ProxyAgent explícito, usar un proxy con fetch de Node.js.

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

undici 8.0.0 eliminó las envolturas de handlers heredadas y renombró los callbacks del handler, por ejemplo onError a onResponseError (guía de migración). 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, 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ó:

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

xen-orchestra#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, 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 sin onRequestStart. shadcn-vue#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 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). Según el árbol de fuentes de Node 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).
  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 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() (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:

code
[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, cualquier respuesta distinta de un 2xx significa que el túnel nunca se formó. undici descarta 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 cubre esa configuración.

EstadoQué está diciendo el proxyDónde mirar
407Quiere credenciales de proxyCorrige el error de proxy 407
403Su política rechaza la petición; el RFC 9110 pide a los proxies limitar el CONNECT a puertos conocidos o destinos permitidosLas reglas de permiso del proxy para este destino y puerto
429Un límite de peticiones; el estado por sí solo no dice de quién (RFC 6585)Reduce el ritmo; lee Retry-After con curl -v
502 o 504Su upstream envió una respuesta incorrecta, o ninguna a tiempoReintenta dentro de un presupuesto; comprueba el destino desde el lado del proxy
503Temporalmente no puede atender la peticiónReintenta más tarde, respetando cualquier Retry-After

undici construye el mismo mensaje a partir de cualquier estado distinto de 200. Errores de proxy explicados 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). 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):

code
[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), 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):

code
[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); 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, 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.

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). 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 añadió ProxyConnectionError para que estas peticiones fallen en vez de entrar en bucle (issue #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 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 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); 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, 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: un ProxyAgent de undici 6 incluido falló en Node 26; Corepack 0.35.0 lo corrigió exigiendo NODE_USE_ENV_PROXY=1.
  • openclaw#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), 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 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/:

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. Un correo cuando se abra el acceso. Nada más.

Fuentes y lecturas adicionales

Referencias técnicas usadas para esta guía. Consulta la documentación de tu versión instalada y la configuración compatible de tu proveedor.