TypeError: fetch failed — это не сама ошибка. Это обёртка, в которую встроенный fetch Node.js (undici) заворачивает каждый сбой сетевого уровня. Причина лежит в err.cause, а иногда ещё на уровень глубже. За прокси причины делятся на четыре группы:
- Прокси вообще не использовался. Встроенный
fetchигнорируетHTTPS_PROXY, пока вы явно не включите поддержку черезNODE_USE_ENV_PROXY=1,--use-env-proxyилиhttp.setGlobalProxyFromEnv(), а Node 26 игнорирует глобальный диспетчер, установленный undici 5, 6 или 7 до версии 7.27.0. Вы видитеgetaddrinfo ENOTFOUND, ошибку подключения с адресом назначения или обычный ответ, которого прокси не зафиксировал в логе. - Несовпадение версий вокруг undici 8. Диспетчер undici 5 или 6, переданный в отдельный запрос
fetchNode 26, или диспетчер undici 8, переданный вfetchNode 22 или 24, падает сinvalid onError methodилиinvalid onRequestStart methodещё до того, как хоть один байт дойдёт до прокси. - Прокси отклонил запрос.
Proxy response (NNN) !== 200 when HTTP Tunnelingлежит двумя уровнями ниже вместе со статусом прокси, например 403, 407, 429 или 502. Обычныеhttp://URL на undici 8.7 и новее ведут себя иначе. - Недоступный или неисправный прокси.
ECONNREFUSED,ETIMEDOUT,UND_ERR_CONNECT_TIMEOUTилиENOTFOUNDс именем прокси,UND_ERR_PRX_CONN,ECONNRESET, ошибка TLS или вовсе никакой ошибки.
Выведите всю цепочку, найдите самую глубокую строку в таблицах ниже и примените исправление для этого уровня. За исключением одной строки, помеченной как сообщённая, каждая строка ошибки в таблицах наблюдалась в 1 965 loopback-ячейках лаборатории, которые ipvolt прогнал 23 и 24 сентября 2026 года на Node.js v22.23.3, v24.21.0 и v26.10.0 с npm undici от 5.29.0 до 8.11.0.
Ответ 4xx или 5xx от целевого сервера никогда не даёт fetch failed: он возвращается как Response. Браузерная TypeError: Failed to fetch — другая ошибка; она возникает при сетевых сбоях и ошибках CORS, детали которых JavaScript увидеть не может.
Что означает «TypeError: fetch failed» в Node.js
undici отклоняет промис при сетевых ошибках через new TypeError('fetch failed', { cause: response.error }). Та же строка есть и в первой, и в последней версии undici, входящей в состав Node 22, 24 и 26 (шесть выпусков, от 6.11.1 до 8.10.2). Причиной может быть ошибка undici со стабильным code, например UND_ERR_INVALID_ARG, системная ошибка Node.js, например ECONNREFUSED, или DOMException с сообщением Request was cancelled., которая оборачивает настоящую ошибку ещё на уровень ниже, как в лаборатории делал каждый отклонённый CONNECT. Исключение — ваш собственный AbortSignal.timeout(): он отклоняет промис голой TimeoutError без обёртки fetch failed и без cause.
Выведите всю цепочку причин
Несколько привычных способов логирования ошибки останавливаются раньше самой глубокой строки. С лабораторным прокси, отвечающим 407, на всех трёх средах выполнения err.message показывал только fetch failed, err.cause.message останавливался на Request was cancelled., а JSON.stringify(err) печатал {}. console.log(err) до 407 доходил, но со стеками вызовов и без какого-либо маскирования. Вместо этого пройдите по цепочке с помощью print-cause.mjs — принтера, который записал каждую ячейку в лаборатории:
// 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('; ')}]` : ''}`);
}
}Вызывайте её в блоке catch, например try { await fetch(url) } catch (err) { printCauseChain(err); process.exitCode = 1; }. Не пробрасывайте err дальше, оставляя его неперехваченным: тогда Node сам печатает всю ошибку целиком, включая незамаскированный [cause], что лаборатория подтвердила на одноразовом пароле. Вот вывод для лабораторного прокси с 407 на Node v26.10.0 с 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 TunnelingПрочитайте вывод, прежде чем делиться им: всё, что не совпало с масками, например user:password@host без схемы впереди, проходит без изменений.
Если ваш код ветвится по ошибке, сравнивайте err.code, а не instanceof. Справочник ошибок undici рекомендует именно это, потому что «встроенный (глобальный) диспетчер может быть из другой версии undici, чем та, которую вы импортируете напрямую». Для UND_ERR_INVALID_ARG проверяйте и сообщение: в лаборатории этот код означал либо несовпадение версий, либо 407 от прокси на обычном http:// URL.
Найдите свою ошибку: строка, глубина, уровень и решение
Глубина 0 — это TypeError: fetch failed, а каждая строка находится на глубине 1, если в строке таблицы не сказано иное. «Прокси ничего не записал» означает, что лабораторный прокси не увидел ни одного соединения для этого fetch.
Сбой до того, как запрос дошёл до прокси
| Что вы видите | Уровень | Решение |
|---|---|---|
invalid onError method (UND_ERR_INVALID_ARG), прокси ничего не записал | Диспетчер undici 5 или 6 передан в fetch Node 26 | Диспетчер undici 7 или 8, собственный fetch undici либо без диспетчера с NODE_USE_ENV_PROXY=1 |
invalid onRequestStart method (UND_ERR_INVALID_ARG), прокси ничего не записал | Диспетчер undici 8 передан в fetch Node 22 или 24 | Dispatcher1Wrapper, собственный fetch undici 8 или его setGlobalDispatcher(), либо агент undici 7 |
getaddrinfo ENOTFOUND с именем назначения, прокси ничего не записал | Прокси не использовался; прямой DNS-запрос не удался | Включите поддержку прокси из окружения. Для глобального диспетчера обновите undici до 7.27.0+ или 8.0.1+ |
getaddrinfo ENOTFOUND с хостом прокси | Имя хоста прокси не разрешается на этой машине | Проверьте хост в URL прокси, а также DNS или VPN, которые должны его разрешать |
connect ECONNREFUSED или connect ETIMEDOUT с адресом назначения, прокси ничего не записал | Прокси не использовался; прямое соединение не удалось | То же, что выше |
Connect Timeout Error (UND_ERR_CONNECT_TIMEOUT) со списком адресов назначения, по сообщению в undici#4960 | То же самое, о чём сообщает таймаут подключения undici | То же, что выше |
| Ошибки нет, ответ обычный, прокси ничего не записал | Прокси был молча обойдён | То же, что выше |
Setting the TLS ServerName to an IP address is not permitted (ERR_INVALID_ARG_VALUE) на Node 26 | URL прокси https:// с IP-адресом, который Node 25 и новее отвергают | http:// для обычного HTTP-прокси или имя хоста, которое покрывает сертификат HTTPS-прокси |
Сбой на прокси или за ним
| Что вы видите | Уровень | Решение |
|---|---|---|
На глубине 2 Proxy response (NNN) !== 200 when HTTP Tunneling под Request was cancelled. на глубине 1 | Прокси отклонил CONNECT со статусом NNN | Прочитайте NNN: 407 — учётные данные (исправление ошибки прокси 407), 403 — его политика, 429 — его ограничение частоты, 502–504 — проблемы на прокси или за ним |
Proxy Authentication Required (407) (UND_ERR_INVALID_ARG) | 407 для обычного http:// URL, который undici 8.7 или новее переслал без CONNECT | То же исправление учётных данных; этот UND_ERR_INVALID_ARG — не несовпадение версий |
Ошибки нет, но response.status равен 403, 429, 502 или 503, а origin не зафиксировал запрос | Собственная ошибка прокси для пересланного http:// URL (undici 8.7 и новее) | Поищите в теле и заголовках признаки прокси |
connect ECONNREFUSED с адресом прокси | На хосте и порту прокси никто не слушает | Проверьте хост, порт и то, что прокси запущен |
AggregateError (ECONNREFUSED) с пустым сообщением и одним отклонённым адресом на каждое семейство IP | То же для имени хоста прокси, например localhost | То же, что выше |
connect ETIMEDOUT с адресом прокси | TCP-рукопожатие с прокси так и не завершилось | Проверьте маршрут и межсетевые экраны на пути к прокси |
Connect Timeout Error (UND_ERR_CONNECT_TIMEOUT) с адресом прокси | Тот же сбой, о котором сообщает таймаут подключения undici | То же, что выше |
ERR_SSL_WRONG_VERSION_NUMBER с wrong version number в сообщении | В URL прокси указано https://, а прокси говорит на обычном HTTP | Используйте http:// в URL прокси |
Proxy Connection failed (UND_ERR_PRX_CONN) над other side closed на глубине 2 | Прокси закрыл соединение, не ответив на CONNECT (undici 8.6 и новее) | Проверьте порт, протокол и поддерживает ли прокси CONNECT |
other side closed (UND_ERR_SOCKET) для http:// URL | Прокси закрыл пересланный запрос, не ответив (undici 8.7 и новее) | Проверьте порт и протокол, затем лог прокси |
| Ошибки нет, fetch никогда не завершается, а прокси записывает CONNECT за CONNECT | Тот же сбой на undici 8.5 и старее: цикл переподключений | Крайний срок на стороне вызывающего кода превращает его в TimeoutError; undici 8.6 и новее — в UND_ERR_PRX_CONN |
Client network socket disconnected before secure TLS connection was established (ECONNRESET) | Прокси ответил 200, а затем оборвал туннель | Смотрите дальше прокси: его upstream или назначение |
SELF_SIGNED_CERT_IN_CHAIN или UNABLE_TO_VERIFY_LEAF_SIGNATURE | Прокси с инспекцией TLS предъявил сертификат от CA, которому Node не доверяет | Добавьте доверие к этому CA через NODE_EXTRA_CA_CERTS или через --use-system-ca, если он есть в хранилище доверия ОС |
Ошибки нет около пяти минут, затем Headers Timeout Error (UND_ERR_HEADERS_TIMEOUT) на глубине 1 | Прокси принял CONNECT и так и не ответил | Задайте собственный крайний срок |
На глубине 0 TimeoutError: The operation was aborted due to timeout, без cause | Сработал ваш AbortSignal.timeout() | Посмотрите в логе прокси, на каком этапе всё зависло |
В каждой ячейке, которая дошла до заданного поведения прокси, — 449 в основной матрице и 803 в наборе проверок, — уровень, который лаборатория вывела только по строкам и счётчикам прокси, совпал с этим поведением.
HTTPS_PROXY задана, но fetch её игнорирует (NODE_USE_ENV_PROXY)
С заданной HTTPS_PROXY и без явного включения все три среды выполнения в лаборатории подключались напрямую, и HTTP_PROXY с http:// URL вела себя так же: назначение, доступное без прокси, возвращало 200, а прокси ничего не записывал. Код запуска Node устанавливает прокси-диспетчер только когда включение активно и задана одна из переменных HTTP_PROXY, HTTPS_PROXY, http_proxy или https_proxy. ALL_PROXY не считается.
| Способ включения | Документирован с | Лаборатория: v22.23.3 / v24.21.0 / v26.10.0 |
|---|---|---|
NODE_USE_ENV_PROXY=1 | v24.0.0, v22.21.0 | через прокси на всех трёх |
node --use-env-proxy | v24.5.0, v22.21.0 | через прокси на всех трёх |
http.setGlobalProxyFromEnv() | v24.14.0, v25.4.0 | на v22.23.3 такой функции нет; через прокси на двух других |
npm undici setGlobalDispatcher(new EnvHttpProxyAgent()) | экспортируется 6.28.1 и каждой undici 7 и 8 в лаборатории | через прокси, кроме v26.10.0 с 6.28.1, 7.16.0 или 7.26.0, где запросы шли напрямую |
- В более старых выпусках включения нет; используйте там диспетчер или собственный
fetchundici. - Указывайте схему. При активном включении значение без
http://, например127.0.0.1:<port>илиlocalhost:<port>, заставляло все три среды выполнения завершиться при запуске сTypeError: Invalid URLилиInvalid URL protocolещё до выполнения кода приложения. Задокументированы только URL проксиhttp://иhttps://; SOCKS5 остаётся открытым пунктом в отслеживающем issue по прокси Node. - Node читает переменные «при запуске», как сказано в документации CLI, поэтому установка
process.env.HTTPS_PROXYиз кода позже не помогает. Альтернатива во время выполнения —http.setGlobalProxyFromEnv(). - На v22.23.3 включение печатало
[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental— уведомление о стабильности, а не ошибку. - Без заданной
NO_PROXYчерез прокси шёл дажеhttps://127.0.0.1. Что именно исключаетNO_PROXY, различается между клиентами; см. матрицу сопоставления NO_PROXY. dispatcherв отдельном запросе перекрывает включение для своего запроса: когдаHTTPS_PROXYуказывала на мёртвый порт, а агент — на рабочий прокси, каждый выполненный запрос использовал агент.
В логе прокси обычный http:// URL на undici 8.7 и новее, включая прокси из окружения в Node 26.5 и новее, не показывает CONNECT — только сам запрос в абсолютной форме, например GET http://origin.test/…. О тех же переменных в curl и Python см. переменные окружения прокси; о явном ProxyAgent — прокси для fetch в Node.js.
invalid onError method: старый диспетчер undici на Node 26
undici 8.0.0 убрала устаревшие обёртки обработчиков и переименовала их колбэки, например onError в onResponseError (руководство по миграции). Каждый выпуск Node 26 включает undici 8, поэтому его fetch передаёт диспетчерам обработчик только с новыми колбэками. В undici 6 первая же провалившаяся проверка, например invalid onConnect method, уходит в ветку ошибки dispatch, которая вызывает handler.onError; этого метода больше нет, поэтому undici бросает invalid onError method, а исходное сообщение теряется.
ProxyAgent из npm undici 5.29.0 и 6.28.1, переданные как dispatcher в fetch v26.10.0, падали во всех 18 ячейках ещё до какого-либо соединения, и добавление NODE_USE_ENV_PROXY=1 не помогало:
[0] TypeError: fetch failed
[1] InvalidArgumentError (UND_ERR_INVALID_ARG): invalid onError methodxen-orchestra#10411 сообщает об UND_ERR_INVALID_ARG для EnvHttpProxyAgent из undici 6.28.1, переданного в fetch Node 26.
Глобальный путь ломается тише. На v26.10.0 setGlobalDispatcher() из npm undici 5.29.0 (с ProxyAgent; в этом выпуске нет EnvHttpProxyAgent), 6.28.1, 7.16.0 или 7.26.0 (с ProxyAgent или EnvHttpProxyAgent) никак не влиял на встроенный fetch. Запросы шли напрямую и заканчивались ENOTFOUND либо ответом 200 с нулём CONNECT, тогда как тот же код на v22.23.3 и v24.21.0 работал через прокси. Эти версии, как и другие теги undici 7 до 7.27.0, у которых проверяли lib/global.js (7.0.0, 7.10.0 и 7.25.0), хранят глобальный диспетчер только под Symbol.for('undici.globalDispatcher.1'), который fetch v26.10.0 игнорировал. undici 7.27.0, выпущенная 2026-06-01 с PR #5319, записывает и .1, и .2, как и 7.29.1 с 8.11.0; с этими тремя глобальный диспетчер работал через прокси на всех трёх средах выполнения. EnvHttpProxyAgent из undici 6.28.1 на v26.10.0 по-прежнему печатал своё предупреждение об экспериментальности, так что предупреждение не доказывает, что агент используется.
invalid onRequestStart method: диспетчер undici 8 на Node 22 или 24
ProxyAgent из npm undici 8.11.0, переданный в отдельный запрос встроенного fetch v22.23.3 или v24.21.0, падал во всех 18 ячейках с InvalidArgumentError (UND_ERR_INVALID_ARG): invalid onRequestStart method на глубине 1, снова до какого-либо соединения. fetch в Node 22 и 24 строит устаревший обработчик, а undici 8 отвергает любой обработчик без onRequestStart. shadcn-vue#1959 сообщает о той же строке из CLI, в который встроена undici 8.10.2, на Node 24.
На v22.23.3 и v24.21.0 оборачивание агента в Dispatcher1Wrapper — задокументированный в undici 8 мост для устаревших потребителей — доходило до прокси:
import { Dispatcher1Wrapper, ProxyAgent } from 'undici'; // undici 8
const dispatcher = new Dispatcher1Wrapper(new ProxyAgent(proxyUrl));
const response = await fetch(url, { dispatcher });Как и setGlobalDispatcher(new ProxyAgent(proxyUrl)) из undici 8.11.0, который к тому же сохраняет обёрнутую копию в устаревшем слоте, откуда её читает встроенный fetch. undici 8.0.0 этого не делала, поэтому на Node 24 и 25 встроенный fetch её игнорировал; PR #4962 исправил это в 8.0.1 2026-04-03.
Выберите исправление: подберите undici под process.versions.undici
Сначала проверьте среду выполнения командой node -p process.versions.undici (документация Node.js). Согласно дереву исходников Node на каждом теге выпуска, каждый выпуск 22.x включает undici 6, каждый выпуск 24.x — 7, а каждый выпуск 26.x — 8. В лаборатории dispatcher в отдельном запросе из npm undici 5.29.0 или 6.28.1 доходил до прокси на v22.23.3 и v24.21.0, но падал на v26.10.0; из undici 7.16.0, 7.26.0, 7.27.0 или 7.29.1 — доходил на всех трёх; а из 8.11.0 — только на v26.10.0. Исправления и цена каждого из них:
- Используйте собственный
fetchundici с его же диспетчером. В лаборатории он работал с четырьмя основными npm-версиями на каждой среде выполнения. Цена: ещё одна зависимость, а классы тел вродеFormDataдолжны быть из того же пакета (документация undici). - Откажитесь от собственного диспетчера и используйте включение в среде выполнения. Оно работало через прокси на каждой среде выполнения, где есть, но действует на весь процесс, покрывает только HTTP(S)-прокси, а
NO_PROXYследует правилам встроенной undici. Corepack 0.35.0 пошёл этим путём. - Совместите мажорную версию. npm undici с той же мажорной версией, что и
process.versions.undici, доходила до прокси на всех трёх средах выполнения. Агент undici 7 в отдельном запросе тоже принимали все три, но это снимок текущего состояния, а не обещание для будущих линеек Node. setGlobalDispatcher()из undici 7.27.0 или более поздней 7.x либо из 8.0.1 и новее. С 7.27.0, 7.29.1 и 8.11.0 он работал через прокси на всех трёх средах выполнения, для каждогоfetchв процессе. С undici 5, 6 или 7 до 7.27.0 на Node 26 он игнорируется, поэтому обновите undici до 7.27.0+ или 8.0.1+. Предпочтите последнюю 7.x: 24 сентября 2026 годаnpm auditпомечал и 7.27.0.Dispatcher1Wrapper(undici 8). Работал через прокси в отдельном запросе на всех трёх средах выполнения.install()(undici 7.11.0 и новее). ЗаменяетglobalThis.fetchи связанные глобальные объекты копией из npm. Только по документации; лаборатория его не запускала.
Proxy response (NNN) !== 200 when HTTP Tunneling
Когда прокси отвечает на CONNECT чем угодно, кроме 200, статус прокси появляется только на глубине 2:
[0] TypeError: fetch failed
[1] Error: Request was cancelled.
[2] AbortError (UND_ERR_ABORTED): Proxy response (403) !== 200 when HTTP TunnelingВ лаборатории каждая связка, дошедшая до прокси, который отвечал на CONNECT кодом 403, 407, 429, 502 или 503, давала эту цепочку с этим статусом — 49 https://-ячеек на каждый статус. По RFC 9110 любой ответ, кроме 2xx, означает, что туннель так и не был установлен. undici отбрасывает остальную часть ответа, включая Retry-After и тело. Чтобы их увидеть, выполните curl через тот же прокси, например curl -v -x http://PROXY_HOST:PORT https://DESTINATION/ -o /dev/null. На лабораторном прокси с 429 curl 8.7.1 и 8.22.0 напечатали его Retry-After: 30. Эту настройку описывает базовое руководство по curl.
| Статус | Что говорит прокси | Куда смотреть |
|---|---|---|
| 407 | Ему нужны учётные данные прокси | Исправить ошибку прокси 407 |
| 403 | Его политика отклоняет запрос; RFC 9110 просит прокси ограничивать CONNECT известными портами или разрешёнными целями | Разрешающие правила прокси для этого назначения и порта |
| 429 | Ограничение частоты; сам по себе статус не говорит, чьё (RFC 6585) | Замедлитесь; прочитайте Retry-After через curl -v |
| 502 или 504 | Его upstream прислал плохой ответ или не прислал вовремя | Повторите в пределах бюджета; проверьте назначение со стороны прокси |
| 503 | Он временно не может обработать запрос | Повторите позже, соблюдая Retry-After, если он есть |
undici строит одно и то же сообщение из любого статуса, отличного от 200. Разбор ошибок прокси рассматривает каждый статус подробнее.
Обычный http:// URL зависит от того, какой undici принадлежит агент. undici 8.6 и старее, включая прокси из окружения в Node 22, 24 и с 26.0 по 26.4, отправляют CONNECT origin.test:80 и падают с той же цепочкой (в лаборатории — каждый путь через undici 7.29.1 и старее, 33 ячейки на статус). undici 8.7 и новее, включая прокси из окружения в Node 26.5 и новее, пересылают сам запрос без CONNECT (PR #5116). Из этих пересланных запросов (16 ячеек на статус) 407 возвращался уровнем ниже как InvalidArgumentError (UND_ERR_INVALID_ARG): Proxy Authentication Required (407) — с тем же кодом, что и несовпадение версий. 403, 429, 502 и 503 вообще не бросали исключение: fetch завершался со статусом и телом прокси, а origin запроса так и не увидел.
ECONNREFUSED, ETIMEDOUT или ENOTFOUND: чей это адрес?
connect ECONNREFUSED с IP-адресом и портом прокси (49 ячеек) означает, что там никто не слушает. Для имени хоста прокси, например localhost, Node пробует каждый адрес и вместо этого сообщает AggregateError с пустым сообщением (49 ячеек):
[0] TypeError: fetch failed
[1] AggregateError (ECONNREFUSED): [ECONNREFUSED: connect ECONNREFUSED ::1:<proxy-port>; ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:<proxy-port>]Если адрес принадлежит назначению, запрос шёл напрямую: с заданной HTTPS_PROXY и без включения отказавшее назначение давало connect ECONNREFUSED 127.0.0.1:<dest-port> на всех трёх средах выполнения.
getaddrinfo ENOTFOUND работает так же: читайте имя хоста. С URL прокси http://proxy.invalid:3128 — зарезервированным именем, которое никогда не разрешается, — каждая связка, использовавшая прокси, падала с getaddrinfo ENOTFOUND proxy.invalid на глубине 1 (49 ячеек), той же формы, что и getaddrinfo ENOTFOUND origin.test при обходе прокси.
connect ETIMEDOUT с адресом прокси (77 ячеек) означает, что TCP-рукопожатие с прокси так и не завершилось; в лаборатории использовался слушатель с заполненной очередью accept. macOS обычно сдавалась примерно через 7,8 секунды — раньше стандартного таймаута подключения undici. Назначение, которое никогда не принимает соединение, за обойдённым прокси давало ту же строку с адресом назначения.
UND_ERR_CONNECT_TIMEOUT: прокси или назначение?
connectTimeout в undici по умолчанию равен 10 секундам (документация Client), и на loopback в macOS обычно первым срабатывал таймаут ОС. Поэтому в опубликованном прогоне UND_ERR_CONNECT_TIMEOUT виден только с connectTimeout: 3000 на агентах undici 8.11.0, примерно через 3,5 секунды (13 ячеек):
[0] TypeError: fetch failed
[1] ConnectTimeoutError (UND_ERR_CONNECT_TIMEOUT): Connect Timeout Error (attempted address: 127.0.0.1:<proxy-port>, timeout: 3000ms)ProxyAgent из undici 5.29.0, 6.28.1 и 7.29.1 с той же опцией всё равно заканчивали таймаутом ОС (28 ячеек), потому что строят соединение с прокси только из опций proxyTls. На Linux или в реальной сети 10-секундный таймер undici может побеждать чаще; лаборатория этого не проверяла. undici 8.10.2 и 8.11.0 также сообщают Connect Timeout Error, когда все адреса имени хоста отказали и хотя бы один — по таймауту, какой бы таймер ни сработал, с настроенным таймаутом в сообщении и AggregateError на глубине 2 (connect.js); лаборатория использовала одиночные адреса и такой формы не получала.
Прочитайте адрес в сообщении. Адрес прокси означает, что прокси недоступен; адрес назначения — что запрос вообще не использовал прокси, как в undici#4960, где в сообщении перечислены шесть адресов назначения. undici 6 и новее печатают attempted address: или attempted addresses:; undici 5 печатает голую Connect Timeout Error без адреса, поэтому вместо этого сверяйтесь с логом прокси. О крайних сроках и бюджетах повторов см. диагностику таймаутов прокси.
ERR_SSL_WRONG_VERSION_NUMBER: URL прокси https:// для обычного прокси
Обычный HTTP-прокси всё равно проводит https://-назначения внутри туннеля CONNECT, но URL прокси, начинающийся с https://, заставляет клиент открывать TLS к самому прокси. В лаборатории обычный HTTP-прокси отвечал на это рукопожатие HTTP 400, и каждая связка, дошедшая до него, падала с Error (ERR_SSL_WRONG_VERSION_NUMBER) на глубине 1 и сообщением OpenSSL, содержащим SSL routines:tls_validate_record_header:wrong version number (85 ячеек).
С IP-адресом в https:// URL прокси 13 связок на v26.10.0 падали ещё до подключения с 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' на глубине 1. Node сделал IP-адрес в имени сервера TLS ошибкой в v25.0.0 (DEP0123). Исправление в обоих случаях — http:// в URL прокси, если только прокси действительно не обслуживает TLS на этом порту.
UND_ERR_PRX_CONN: прокси закрыл соединение, не ответив на CONNECT
Одна из заготовок принимала TCP, читала CONNECT и закрывала соединение без ответа. Результат зависел от того, какой undici принадлежал прокси-агент:
- undici 8.6 и новее (в лаборатории — каждый агент npm 8.11.0, дошедший до прокси, и встроенный прокси из окружения v26.10.0):
ProxyConnectionError (UND_ERR_PRX_CONN): Proxy Connection failedна глубине 1 надSocketError (UND_ERR_SOCKET): other side closedна глубине 2, после одного CONNECT (16 ячеек). - undici 8.5 и старее (в лаборатории — каждый агент npm 5, 6 и 7, дошедший до прокси, и встроенный прокси из окружения Node 22 и 24): ошибки нет. Клиент тут же переподключался, пока обвязка не останавливала его на 25 CONNECT (33 ячейки).
Со стороны приложения этот цикл выглядит как зависание: fetch никогда не завершается. С AbortSignal.timeout(2000) и без остановки зацикленные клиенты отклоняли промис через 2 секунды голой TimeoutError, а прокси в опубликованном прогоне записывал от 18 600 до 20 548 CONNECT для этого единственного запроса (9 ячеек, на loopback).
Для обычного http:// URL undici 8.7 и новее не отправляют CONNECT, поэтому тот же сбой проявлялся как SocketError (UND_ERR_SOCKET): other side closed на глубине 1 (16 ячеек), тогда как undici 7 и старее зацикливались, как описано выше (33 ячейки).
PR #5441 добавил ProxyConnectionError, чтобы такие запросы завершались ошибкой вместо зацикливания (issue #3897). Класс впервые вышел в undici 8.6.0, хотя справочник ошибок 8.11.0 помечает его как v8.10.1, а v26.5.0 — первый выпуск Node 26, встроенная undici которого его содержит.
ECONNRESET после CONNECT 200: туннель оборвался
Когда прокси отвечал 200, а затем закрывал соединение, не отправив ни байта в туннель, каждая связка, дошедшая до него (49 ячеек), сообщала Client network socket disconnected before secure TLS connection was established (ECONNRESET) на глубине 1. CONNECT прошёл успешно, поэтому смотрите дальше «входной двери» прокси: его upstream, выходной узел, назначение или сам прокси, обрывающий туннель. PR #5441 охватывает только установку туннеля, поэтому этот случай не становится UND_ERR_PRX_CONN.
SELF_SIGNED_CERT_IN_CHAIN за прокси с инспекцией TLS
За прокси с инспекцией TLS, чьему CA Node не доверяет, цепочка заканчивается ошибкой сертификата. Лабораторная замена такого прокси отвечала на CONNECT кодом 200, а затем предъявляла для назначения собственный сертификат от одноразового CA. Каждая связка, дошедшая до него, падала на глубине 1 на всех трёх средах выполнения: с self-signed certificate in certificate chain (SELF_SIGNED_CERT_IN_CHAIN), когда прокси присылал вместе с ним сертификат своего CA (49 ячеек), и с unable to verify the first certificate (UNABLE_TO_VERIFY_LEAF_SIGNATURE), когда присылал только собственный сертификат (49 ячеек).
Указание в NODE_EXTRA_CA_CERTS файла с этим CA исправило все 98 ячеек; Node читает его только при старте процесса. Если CA уже есть в хранилище доверия операционной системы, флаг Node --use-system-ca (задокументирован с v22.15.0 и v23.8.0) или NODE_USE_SYSTEM_CA=1 (с v22.19.0 и v24.6.0) заставляет Node доверять и этому хранилищу (настройка корпоративной сети); лаборатория их не проверяла. Не отключайте проверку через NODE_TLS_REJECT_UNAUTHORIZED=0.
Пять минут без ошибки: задайте собственный крайний срок
Прокси, который принимал CONNECT и никогда не отвечал, не давал ошибки в пределах 15-секундного лимита обвязки (49 ячеек). В опубликованном длинном прогоне все 49 таких ячеек падали через 301–302 секунды с HeadersTimeoutError (UND_ERR_HEADERS_TIMEOUT): Headers Timeout Error, что соответствует задокументированному 300-секундному значению headersTimeout по умолчанию в undici. С signal: AbortSignal.timeout(2000) те же связки отклоняли промис примерно через 2 секунды голой TimeoutError. Передавайте signal в каждый запрос через прокси.
Когда падает код инструмента, который писали не вы
CLI, SDK и MCP-серверы часто поставляются с собственной undici и передают её агент во встроенный fetch или устанавливают его через setGlobalDispatcher(). Несовпадение возникает, когда сдвигается любая из сторон: старый инструмент на Node 26 или инструмент, перешедший на undici 8, на Node 22 или 24.
- Выполните
node -p process.versions.undiciтем же бинарникомnode, который использует инструмент. - Найдите копии в инструменте командой
npm explain undici. В лабораторииnpm ls undiciпечатал(empty)для копий, установленных под npm-псевдонимом (npm:undici@…), которыеnpm explainпоказывал. Копия, вкомпилированная в бандл инструмента, не видна ни там, ни там; смотрите журнал изменений или трекер issue инструмента. - Сначала обновите инструмент. Если его документация поддерживает включение в Node, используйте
NODE_USE_ENV_PROXY=1. Иначе временно запускайте его на той линейке Node, которая принимает его копию (см. исправления выше), и сообщите напечатанную цепочку разработчикам инструмента.
Вот сообщённые примеры, которые ipvolt не воспроизводил:
- vercel/vercel#17629, открыт на 2026-09-23: встроенная в Vercel CLI undici 5.29.0 падает с
invalid onError methodна Node 26.8.2, когда задана переменная прокси, и работает на Node 24.21.0. - nodejs/corepack#834: встроенный
ProxyAgentиз undici 6 падал на Node 26; Corepack 0.35.0 исправил это, потребовавNODE_USE_ENV_PROXY=1. - openclaw#155840:
invalid onRequestStart methodот диспетчера undici 8, переданного в WebSocket из undici 7 одной из зависимостей; несовпадение не ограничиваетсяfetch.
Методика, ограничения и файлы для загрузки
ipvolt выполнил эти проверки локально 23 и 24 сентября 2026 года на macOS 15.7.4 (arm64) с официальными архивами Node.js v22.23.3, v24.21.0 и v26.10.0 и npm undici 5.29.0, 6.28.1, 7.29.1 и 8.11.0, плюс 7.16.0, 7.26.0 и 7.27.0 для проверок глобального диспетчера. Каждая ячейка выполняла один fetch в собственном дочернем процессе через loopback-прокси с одним заданным поведением, который считал соединения и запросы. Большинство ячеек запрашивали зарезервированное имя origin.test (RFC 6761), которое разрешал только лабораторный прокси. В основной матрице была 591 ячейка, в дополнительном наборе — 282 (http:// URL, URL прокси с localhost, недоступные назначения), в наборе проверок — 1 029; отдельный прогон из 63 ячеек давал никогда не отвечающему прокси работать 330 секунд. Полная методика, результаты по наборам и история прогонов — в README.
Чистый повторный прогон только из итогового архива по README классифицировал все 591 основную, 282 дополнительные, 1 029 проверочных и 63 ячейки длинного прогона так же, как опубликованные результаты. Ожидайте, что повторный прогон разойдётся в нескольких ячейках с незавершающимся рукопожатием, где таймауты подключения ОС и undici соревнуются друг с другом, а также в проверках loop-deadline, если запускать их подряд: зацикливающаяся ячейка оставляет тысячи сокетов в состоянии TIME_WAIT, поэтому следующая может записать ноль CONNECT, пока порты не освободятся примерно через 40 секунд.
Лаборатория не охватывала Linux и реальные сети, настоящие HTTPS- или SOCKS-прокси, прокси, принимающие учётные данные, 504, сопоставление NO_PROXY, http.request, Deno, Bun и реальные сторонние инструменты. Результаты показывают, как эти версии Node.js и undici сообщают о каждом заданном поведении, а не то, как ведёт себя какой-либо провайдер прокси. Lockfile намеренно фиксирует старые версии undici, и npm ci сообщает о них как об уязвимости высокой серьёзности; не копируйте эти фиксации в приложение.
Все файлы лежат по адресу https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/:
- fix-node-fetch-failed-proxy-lab.zip содержит все файлы, перечисленные ниже.
- README.md объясняет, как запустить лабораторию и читать ячейку; print-cause.mjs — принтер.
- run-matrix.mjs, run-checks.mjs, cell.mjs, cause-forms.mjs и compare.mjs — обвязка.
- get-runtimes.mjs, runtimes.json, package.json и package-lock.json фиксируют среды выполнения и пакеты.
- results.json, results.csv, results-extra.json, results-extra.csv, results-long-hang.json, results-long-hang.csv, results-checks.json и results-checks.csv содержат цепочку причин и счётчики прокси каждой ячейки.
Чтобы воспроизвести результаты, распакуйте архив и следуйте README: npm ci, node get-runtimes.mjs, затем каждый набор и node compare.mjs против копии опубликованного файла. Аккаунт прокси не нужен.
Если хотите узнать, когда откроется доступ к ipvolt, присоединяйтесь к списку раннего доступа. Одно письмо, когда доступ откроется. Больше ничего.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- 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