# Исправляем TypeError: fetch failed в Node.js за прокси

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

[ipvolt — главная](https://ipvolt.com/ru.md) / [Руководства](https://ipvolt.com/ru/guides.md) / Исправляем TypeError: fetch failed в Node.js за прокси

Диагностика
Проверено: 2026-09-24
Опубликовано: 2026-09-24
22 мин чтения
Автор: ipvolt

Разбираем TypeError: fetch failed в Node.js за прокси: вывод цепочки err.cause, игнорируемый HTTPS_PROXY, несовпадение версий undici и ответы прокси 403, 407 или 502.

`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, переданный в отдельный запрос `fetch` Node 26, или диспетчер undici 8, переданный в `fetch` Node 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 })`. [Та же строка](https://github.com/nodejs/undici/blob/v8.10.2/lib/web/fetch/index.js#L272) есть и в первой, и в последней версии 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](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/print-cause.mjs) — принтера, который записал каждую ячейку в лаборатории:

```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('; ')}]` : ''}`);
  }
}
```
Вызывайте её в блоке `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`:

```text
[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`. [Справочник ошибок](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/Errors.md) 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](https://github.com/nodejs/undici/issues/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](/ru/guides/fix-proxy-error-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, а прокси ничего не записывал. [Код запуска](https://github.com/nodejs/node/blob/v26.10.0/lib/internal/process/pre_execution.js#L314-L333) 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`](https://nodejs.org/api/cli.html#node_use_env_proxy1) | v24.0.0, v22.21.0 | через прокси на всех трёх |
| [`node --use-env-proxy`](https://nodejs.org/api/cli.html#--use-env-proxy) | v24.5.0, v22.21.0 | через прокси на всех трёх |
| [`http.setGlobalProxyFromEnv()`](https://nodejs.org/api/http.html#httpsetglobalproxyfromenvproxyenv) | 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, где запросы шли напрямую |

- В более старых выпусках включения нет; используйте там диспетчер или собственный `fetch` undici.
- Указывайте схему. При активном включении значение без `http://`, например `127.0.0.1:<port>` или `localhost:<port>`, заставляло все три среды выполнения завершиться при запуске с `TypeError: Invalid URL` или `Invalid URL protocol` ещё до выполнения кода приложения. Задокументированы только URL прокси `http://` и `https://`; SOCKS5 остаётся открытым пунктом в [отслеживающем issue по прокси](https://github.com/nodejs/node/issues/57872) 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](/ru/blog/no-proxy-matching-tested).
- `dispatcher` в отдельном запросе перекрывает включение для своего запроса: когда `HTTPS_PROXY` указывала на мёртвый порт, а агент — на рабочий прокси, каждый выполненный запрос использовал агент.

В логе прокси обычный `http://` URL на undici 8.7 и новее, включая прокси из окружения в Node 26.5 и новее, не показывает CONNECT — только сам запрос в абсолютной форме, например `GET http://origin.test/…`. О тех же переменных в curl и Python см. [переменные окружения прокси](/ru/guides/proxy-environment-variables); о явном `ProxyAgent` — [прокси для fetch в Node.js](/ru/guides/nodejs-fetch-proxy).

## invalid onError method: старый диспетчер undici на Node 26

[undici 8.0.0](https://github.com/nodejs/undici/releases/tag/v8.0.0) убрала устаревшие обёртки обработчиков и переименовала их колбэки, например `onError` в `onResponseError` ([руководство по миграции](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/migrating-from-v7-to-v8.md)). Каждый выпуск Node 26 включает undici 8, поэтому его `fetch` передаёт диспетчерам обработчик только с новыми колбэками. В undici 6 первая же провалившаяся проверка, например `invalid onConnect method`, уходит в [ветку ошибки dispatch](https://github.com/nodejs/undici/blob/v6.28.1/lib/dispatcher/dispatcher-base.js#L168-L196), которая вызывает `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` не помогало:

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

[xen-orchestra#10411](https://github.com/vatesfr/xen-orchestra/issues/10411) сообщает об `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](https://github.com/nodejs/undici/pull/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 [отвергает любой обработчик](https://github.com/nodejs/undici/blob/v8.10.2/lib/core/util.js#L567-L605) без `onRequestStart`. [shadcn-vue#1959](https://github.com/unovue/shadcn-vue/issues/1959) сообщает о той же строке из CLI, в который встроена undici 8.10.2, на Node 24.

На v22.23.3 и v24.21.0 оборачивание агента в `Dispatcher1Wrapper` — задокументированный в undici 8 мост для устаревших потребителей — доходило до прокси:

```js
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](https://github.com/nodejs/undici/pull/4962) исправил это в 8.0.1 2026-04-03.

## Выберите исправление: подберите undici под process.versions.undici

Сначала проверьте среду выполнения командой `node -p process.versions.undici` ([документация Node.js](https://nodejs.org/api/globals.html#custom-dispatcher)). Согласно [дереву исходников Node](https://github.com/nodejs/node/blob/v26.10.0/src/undici_version.h) на каждом теге выпуска, каждый выпуск 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. Исправления и цена каждого из них:

1. **Используйте собственный `fetch` undici с его же диспетчером.** В лаборатории он работал с четырьмя основными npm-версиями на каждой среде выполнения. Цена: ещё одна зависимость, а классы тел вроде `FormData` должны быть из того же пакета ([документация undici](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/best-practices/undici-vs-builtin-fetch.md)).
2. **Откажитесь от собственного диспетчера и используйте включение в среде выполнения.** Оно работало через прокси на каждой среде выполнения, где есть, но действует на весь процесс, покрывает только HTTP(S)-прокси, а `NO_PROXY` следует правилам встроенной undici. [Corepack 0.35.0](https://github.com/nodejs/corepack/releases/tag/v0.35.0) пошёл этим путём.
3. **Совместите мажорную версию.** npm undici с той же мажорной версией, что и `process.versions.undici`, доходила до прокси на всех трёх средах выполнения. Агент undici 7 в отдельном запросе тоже принимали все три, но это снимок текущего состояния, а не обещание для будущих линеек Node.
4. **`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.
5. **`Dispatcher1Wrapper` (undici 8).** Работал через прокси в отдельном запросе на всех трёх средах выполнения.
6. **[`install()`](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/GlobalInstallation.md) (undici 7.11.0 и новее).** Заменяет `globalThis.fetch` и связанные глобальные объекты копией из npm. Только по документации; лаборатория его не запускала.

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

Когда прокси отвечает на CONNECT чем угодно, кроме 200, статус прокси появляется только на глубине 2:

```text
[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](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6) любой ответ, кроме 2xx, означает, что туннель так и не был установлен. undici [отбрасывает](https://github.com/nodejs/undici/blob/v8.10.2/lib/dispatcher/proxy-agent.js#L229-L233) остальную часть ответа, включая `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](/ru/guides/curl-proxy-setup).

| Статус | Что говорит прокси | Куда смотреть |
| --- | --- | --- |
| 407 | Ему нужны учётные данные прокси | [Исправить ошибку прокси 407](/ru/guides/fix-proxy-error-407) |
| 403 | Его политика отклоняет запрос; RFC 9110 просит прокси ограничивать CONNECT известными портами или разрешёнными целями | Разрешающие правила прокси для этого назначения и порта |
| 429 | Ограничение частоты; сам по себе статус не говорит, чьё ([RFC 6585](https://www.rfc-editor.org/rfc/rfc6585#section-4)) | Замедлитесь; прочитайте `Retry-After` через `curl -v` |
| 502 или 504 | Его upstream прислал плохой ответ или не прислал вовремя | Повторите в пределах бюджета; проверьте назначение со стороны прокси |
| 503 | Он временно не может обработать запрос | Повторите позже, соблюдая `Retry-After`, если он есть |

undici строит одно и то же сообщение из любого статуса, отличного от 200. [Разбор ошибок прокси](/ru/blog/proxy-status-codes-407-429-502) рассматривает каждый статус подробнее.

Обычный `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](https://github.com/nodejs/undici/pull/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 ячеек):

```text
[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](https://github.com/nodejs/undici/blob/v8.11.0/docs/docs/api/Client.md)), и на loopback в macOS обычно первым срабатывал таймаут ОС. Поэтому в опубликованном прогоне `UND_ERR_CONNECT_TIMEOUT` виден только с `connectTimeout: 3000` на агентах undici 8.11.0, примерно через 3,5 секунды (13 ячеек):

```text
[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](https://github.com/nodejs/undici/blob/v8.10.2/lib/core/connect.js#L167-L190)); лаборатория использовала одиночные адреса и такой формы не получала.

Прочитайте адрес в сообщении. Адрес прокси означает, что прокси недоступен; адрес назначения — что запрос вообще не использовал прокси, как в [undici#4960](https://github.com/nodejs/undici/issues/4960), где в сообщении перечислены шесть адресов назначения. undici 6 и новее печатают `attempted address:` или `attempted addresses:`; undici 5 печатает голую `Connect Timeout Error` без адреса, поэтому вместо этого сверяйтесь с логом прокси. О крайних сроках и бюджетах повторов см. [диагностику таймаутов прокси](/ru/guides/proxy-timeout-troubleshooting).

## 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](https://nodejs.org/api/deprecations.html#dep0123-setting-the-tls-servername-to-an-ip-address)). Исправление в обоих случаях — `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](https://github.com/nodejs/undici/pull/5441) добавил `ProxyConnectionError`, чтобы такие запросы завершались ошибкой вместо зацикливания ([issue #3897](https://github.com/nodejs/undici/issues/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`](https://nodejs.org/api/cli.html#node_extra_ca_certsfile) файла с этим CA исправило все 98 ячеек; Node читает его только при старте процесса. Если CA уже есть в хранилище доверия операционной системы, флаг Node [`--use-system-ca`](https://nodejs.org/api/cli.html#--use-system-ca) (задокументирован с v22.15.0 и v23.8.0) или `NODE_USE_SYSTEM_CA=1` (с v22.19.0 и v24.6.0) заставляет Node доверять и этому хранилищу ([настройка корпоративной сети](https://nodejs.org/learn/http/enterprise-network-configuration)); лаборатория их не проверяла. Не отключайте проверку через `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.

1. Выполните `node -p process.versions.undici` тем же бинарником `node`, который использует инструмент.
2. Найдите копии в инструменте командой `npm explain undici`. В лаборатории `npm ls undici` печатал `(empty)` для копий, установленных под npm-псевдонимом (`npm:undici@…`), которые `npm explain` показывал. Копия, вкомпилированная в бандл инструмента, не видна ни там, ни там; смотрите журнал изменений или трекер issue инструмента.
3. Сначала обновите инструмент. Если его документация поддерживает включение в Node, используйте `NODE_USE_ENV_PROXY=1`. Иначе временно запускайте его на той линейке Node, которая принимает его копию (см. исправления выше), и сообщите напечатанную цепочку разработчикам инструмента.

Вот сообщённые примеры, которые ipvolt не воспроизводил:

- [vercel/vercel#17629](https://github.com/vercel/vercel/issues/17629), открыт на 2026-09-23: встроенная в Vercel CLI undici 5.29.0 падает с `invalid onError method` на Node 26.8.2, когда задана переменная прокси, и работает на Node 24.21.0.
- [nodejs/corepack#834](https://github.com/nodejs/corepack/issues/834): встроенный `ProxyAgent` из undici 6 падал на Node 26; Corepack 0.35.0 исправил это, потребовав `NODE_USE_ENV_PROXY=1`.
- [openclaw#155840](https://github.com/openclaw/openclaw/issues/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](https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2)), которое разрешал только лабораторный прокси. В основной матрице была 591 ячейка, в дополнительном наборе — 282 (`http://` URL, URL прокси с `localhost`, недоступные назначения), в наборе проверок — 1 029; отдельный прогон из 63 ячеек давал никогда не отвечающему прокси работать 330 секунд. Полная методика, результаты по наборам и история прогонов — в [README](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/README.md).

Чистый повторный прогон только из итогового архива по 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](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/fix-node-fetch-failed-proxy-lab.zip) содержит все файлы, перечисленные ниже.
- [README.md](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/README.md) объясняет, как запустить лабораторию и читать ячейку; [print-cause.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/print-cause.mjs) — принтер.
- [run-matrix.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/run-matrix.mjs), [run-checks.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/run-checks.mjs), [cell.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/cell.mjs), [cause-forms.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/cause-forms.mjs) и [compare.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/compare.mjs) — обвязка.
- [get-runtimes.mjs](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/get-runtimes.mjs), [runtimes.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/runtimes.json), [package.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/package.json) и [package-lock.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/package-lock.json) фиксируют среды выполнения и пакеты.
- [results.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results.json), [results.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results.csv), [results-extra.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-extra.json), [results-extra.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-extra.csv), [results-long-hang.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-long-hang.json), [results-long-hang.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-long-hang.csv), [results-checks.json](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-checks.json) и [results-checks.csv](https://ipvolt.com/downloads/fix-node-fetch-failed-proxy/results-checks.csv) содержат цепочку причин и счётчики прокси каждой ячейки.

Чтобы воспроизвести результаты, распакуйте архив и следуйте README: `npm ci`, `node get-runtimes.mjs`, затем каждый набор и `node compare.mjs` против копии опубликованного файла. Аккаунт прокси не нужен.

Если хотите узнать, когда откроется доступ к ipvolt, [присоединяйтесь к списку раннего доступа](https://ipvolt.com/#waitlist-closing). Одно письмо, когда доступ откроется. Больше ничего.

## Источники и дополнительное чтение

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

## Связанные руководства

- [Прокси для fetch в Node.js](https://ipvolt.com/ru/guides/nodejs-fetch-proxy.md)
- [Переменные окружения прокси: HTTP_PROXY и NO_PROXY](https://ipvolt.com/ru/guides/proxy-environment-variables.md)
- [Диагностика таймаутов прокси: по одному этапу за раз](https://ipvolt.com/ru/guides/proxy-timeout-troubleshooting.md)

## О ipvolt

Примеры используют обобщённые настройки прокси со ссылками на оригинальную техническую документацию. Поведение конкретного продукта уточняйте у своего провайдера. ipvolt пока в разработке.

[Читать оригинал на английском](https://ipvolt.com/guides/fix-node-fetch-failed-proxy.md)

## Узнайте, когда откроется доступ.

ipvolt · В разработке

Мы строим прокси-инфраструктуру для разработчиков и команд, работающих с данными. Оставьте email, чтобы получить уведомление, когда ipvolt будет готов.

Одно письмо, когда откроется доступ. Больше ничего.

[Получить ранний доступ](https://ipvolt.com/ru/guides/fix-node-fetch-failed-proxy#waitlist-closing)

[Конфиденциальность](https://ipvolt.com/privacy)

