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
fetchintegrado ignoraHTTPS_PROXYhasta que lo activas conNODE_USE_ENV_PROXY=1,--use-env-proxyohttp.setGlobalProxyFromEnv(), y Node 26 ignora un dispatcher global establecido por undici 5, 6 o 7 anterior a 7.27.0. Vesgetaddrinfo 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
fetchde Node 26, o un dispatcher de undici 8 pasado alfetchde Node 22 o 24, falla coninvalid onError methodoinvalid onRequestStart methodantes de que ningún byte llegue al proxy. - El proxy rechazó la petición.
Proxy response (NNN) !== 200 when HTTP Tunnelingestá dos niveles más abajo, con el estado del proxy, como 403, 407, 429 o 502. Las URLhttp://planas en undici 8.7 y posteriores se comportan de otra forma. - Un proxy inalcanzable o que falla.
ECONNREFUSED,ETIMEDOUT,UND_ERR_CONNECT_TIMEOUToENOTFOUNDnombrando 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:
// 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:
[0] TypeError: fetch failed
[1] Error: Request was cancelled.
[2] AbortError (UND_ERR_ABORTED): Proxy response (407) !== 200 when HTTP TunnelingLee 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 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 | 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), 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 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 | v24.0.0, v22.21.0 | pasó por el proxy en los tres |
node --use-env-proxy | v24.5.0, v22.21.0 | pasó por el proxy en los tres |
http.setGlobalProxyFromEnv() | 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
fetchpropio de undici. - Incluye el esquema. Con la activación encendida, un valor sin
http://, como127.0.0.1:<port>olocalhost:<port>, hizo que los tres runtimes salieran al arrancar conTypeError: Invalid URLoInvalid URL protocol, antes de que se ejecutara ningún código de la aplicación. Solo están documentadas las URL de proxyhttp://yhttps://; 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_PROXYdesde 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_PROXYdefinida, inclusohttps://127.0.0.1pasó por el proxy. Lo queNO_PROXYexime varía entre clientes; consulta la matriz de coincidencia de NO_PROXY. - Un
dispatcherpor petición se impone a la activación para su petición: conHTTPS_PROXYapuntando 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ó:
[0] TypeError: fetch failed
[1] InvalidArgumentError (UND_ERR_INVALID_ARG): invalid onError methodxen-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:
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:
- Usa el
fetchpropio 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 comoFormDatadeben proceder del mismo paquete (documentación de undici). - 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_PROXYsigue al undici incluido. Corepack 0.35.0 tomó esta vía. - Iguala la versión mayor. Un npm undici con la misma versión mayor que
process.versions.undicillegó 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. 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 cadafetchdel 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 audittambién marcó 7.27.0.Dispatcher1Wrapper(undici 8). Pasó por el proxy por petición en los tres runtimes.install()(undici 7.11.0 y posteriores). ReemplazaglobalThis.fetchy 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:
[0] TypeError: fetch failed
[1] Error: Request was cancelled.
[2] AbortError (UND_ERR_ABORTED): Proxy response (403) !== 200 when HTTP TunnelingEn 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.
| Estado | Qué está diciendo el proxy | Dónde mirar |
|---|---|---|
| 407 | Quiere credenciales de proxy | Corrige el error de proxy 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) | 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 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):
[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):
[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 failedsobre la profundidad 2SocketError (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.
- Ejecuta
node -p process.versions.undicicon el mismo binarionodeque usa la herramienta. - Encuentra las copias de la herramienta con
npm explain undici. En el laboratorio,npm ls undiciimprimió(empty)para copias instaladas bajo un alias de npm (npm:undici@…) quenpm explainsí 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. - 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 methoden Node 26.8.2 cuando hay una variable de proxy definida, y funciona en Node 24.21.0. - nodejs/corepack#834: un
ProxyAgentde undici 6 incluido falló en Node 26; Corepack 0.35.0 lo corrigió exigiendoNODE_USE_ENV_PROXY=1. - openclaw#155840:
invalid onRequestStart methoddesde un dispatcher de undici 8 pasado al WebSocket de undici 7 de una dependencia; el desfase no se limita afetch.
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/:
- fix-node-fetch-failed-proxy-lab.zip contiene todos los archivos de abajo.
- README.md explica cómo ejecutar el laboratorio y leer una celda; print-cause.mjs es la utilidad de impresión.
- run-matrix.mjs, run-checks.mjs, cell.mjs, cause-forms.mjs y compare.mjs son el harness.
- get-runtimes.mjs, runtimes.json, package.json y package-lock.json fijan los runtimes y los paquetes.
- results.json, results.csv, results-extra.json, results-extra.csv, results-long-hang.json, results-long-hang.csv, results-checks.json y 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. 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.
- Node.js HTTP: Built-in Proxy Support and http.setGlobalProxyFromEnv()
- Node.js CLI: NODE_USE_ENV_PROXY=1 and --use-env-proxy
- Node.js globals: fetch with a custom dispatcher and process.versions.undici
- Node.js Learn: Enterprise network configuration
- Node.js deprecations: DEP0123, setting the TLS ServerName to an IP address
- undici 8.11.0: Errors reference
- undici 8.11.0: ProxyAgent (CONNECT for https://, forwarding for http://)
- undici: Migrating from undici 7 to 8
- undici: Undici module vs. Node.js built-in fetch
- undici PR #4962: mirror the legacy global dispatcher for built-in fetch (v8.0.1)
- undici PR #5319: setGlobalDispatcher() writes both global-dispatcher slots, for Node 26 (v7.27.0)
- undici PR #5116: auto-detect HTTP proxy tunneling (v8.7.0)
- undici PR #5441: fail instead of looping when the proxy closes during CONNECT setup (UND_ERR_PRX_CONN, v8.6.0)
- RFC 9110: CONNECT and status codes 403, 407, 502, 503 and 504
- RFC 6585: section 4, 429 Too Many Requests