Ваш скрипт задаёт HTTPS_PROXY, вызывает fetch(), ждёт десять секунд и падает вот с такой ошибкой:
node:internal/modules/run_main:107
triggerUncaughtException(
^
[TypeError: fetch failed] {
[cause]: ConnectTimeoutError: Connect Timeout Error (attempted address: shop.example.de:443, timeout: 10000ms)
at onConnectTimeout (node:internal/deps/undici/undici:1991:23)
at Immediate._onImmediate (node:internal/deps/undici/undici:1972:11)
at process.processImmediate (node:internal/timers:574:21) {
code: 'UND_ERR_CONNECT_TIMEOUT'
}
}
Node.js v24.21.0curl, запущенный в той же оболочке с той же HTTPS_PROXY, отрабатывает успешно:
curl -sS -o /dev/null -w '%{http_code}\n' https://shop.example.de/p/espressomuehle-k2
# 200С прокси всё в порядке. Встроенный fetch в Node не читает HTTP_PROXY и HTTPS_PROXY, пока вы явно об этом не попросите, поэтому он пытался достучаться до магазина напрямую. Короткое исправление — для Node 22.21 и новее и для Node 24 и новее:
NODE_USE_ENV_PROXY=1 node price-check.mjsНа более старых выпусках, включая Node 20, вместо этого нужен диспетчер undici. Оба способа описаны ниже вместе со связанными с ними ловушками. Все команды на этой странице выполнены, а их вывод получен 2 октября 2026 года с логирующим тестовым прокси на версиях Node, перечисленных в разделе Как это проверялось.
Скрипт, который падает по таймауту
Сквозной пример — небольшое задание, которое раз в час через резидентный прокси проверяет цену одного товара в немецком интернет-магазине. shop.example.de здесь заменяет настоящий магазин:
// price-check.mjs: log one product's price every hour.
const PRODUCT_URL = 'https://shop.example.de/p/espressomuehle-k2';
const ONE_HOUR = 60 * 60 * 1000;
async function checkPrice() {
const res = await fetch(PRODUCT_URL, { signal: AbortSignal.timeout(30_000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const price = html.match(/itemprop="price" content="([\d.]+)"/)?.[1];
console.log(new Date().toISOString(), price ? `${price} EUR` : 'price not found');
}
await checkPrice();
setInterval(() => checkPrice().catch((err) => console.error(err)), ONE_HOUR);Прокси берётся из окружения — так, как этого ожидают curl, Python Requests и большинство CLI-инструментов. proxy.example.net, USERNAME и PASSWORD — заполнители для шлюза и учётных данных вашего провайдера; закодируйте зарезервированные символы в них через percent-encoding и загружайте реальные значения из менеджера секретов, а не набирайте их вручную, оставляя в истории оболочки:
export HTTPS_PROXY='http://USERNAME:PASSWORD@proxy.example.net:8080'
node price-check.mjsНа каждой протестированной версии Node, от 20.20.2 до 26.10.0, этот запуск так и не обратился к прокси. Он падал примерно через 10,6 секунды с ошибкой, приведённой выше. Node 20 и 22 печатают тот же [cause], но под строкой TypeError: fetch failed со стеком вызовов, а не в форме с квадратными скобками.
Почему fetch в Node игнорирует HTTPS_PROXY
Встроенный fetch в Node — это undici. Его диспетчер по умолчанию открывает прямое соединение с тем хостом, который указан в URL. Переменные прокси читаются, только если вы явно включите эту поддержку: PR #57165 добавил её в 2025 году с включением через переменную окружения NODE_USE_ENV_PROXY, и в его описании явное включение названо первым шагом, а включение по умолчанию оставлено на потом.
Поэтому запрос идёт прямо на shop.example.de:443. В сети, где выходить в интернет может только прокси, попытка соединения остаётся без ответа, и undici сдаётся после своего 10-секундного таймаута подключения с UND_ERR_CONNECT_TIMEOUT. Прочитайте адрес в сообщении: attempted address: shop.example.de:443 — это магазин, значит, прокси вообще не использовался. Если же в сообщении указан хост вашего прокси, недоступен сам прокси, а это другая проблема; Исправляем TypeError: fetch failed в Node.js за прокси расшифровывает её и остальные строки причин.
Там, где прямой трафик разрешён, ошибки нет вовсе. В тесте без явного включения запрос fetch к хосту, до которого машина могла достучаться напрямую, вернул 200, а прокси ничего не записал. Для проверки цен это значит, что магазин видит собственный адрес вашего сервера, а не адрес прокси.
Исправление в одну строку: NODE_USE_ENV_PROXY=1 или --use-env-proxy
Включите для процесса встроенную поддержку прокси в Node:
NODE_USE_ENV_PROXY=1 node price-check.mjs
# 2026-10-02T12:35:44.149Z 49.90 EURФлаг командной строки делает то же самое и работает также в NODE_OPTIONS:
node --use-env-proxy price-check.mjs
NODE_OPTIONS=--use-env-proxy node price-check.mjsС любым из них каждый протестированный выпуск, который это поддерживает, отправлял прокси CONNECT shop.example.de:443 с учётными данными из URL и печатал цену. Два переключателя появились в разных выпусках:
| Способ включения | Добавлен в (документация Node.js) | Результат теста |
|---|---|---|
NODE_USE_ENV_PROXY=1 | v24.0.0, v22.21.0 | Работал через прокси на 22.21.0, 22.23.3, 24.0.0, 24.4.1, 24.5.0, 24.21.0 и 26.10.0. Молча игнорировался на 20.20.2 и 22.20.0, которые, как и прежде, падали по таймауту |
--use-env-proxy | v24.5.0, v22.21.0 | Работал через прокси на 22.21.0, 22.23.3, 24.5.0, 24.21.0 и 26.10.0. Node 20.20.2, 22.20.0, 24.0.0 и 24.4.1 отказывались запускаться: bad option: --use-env-proxy |
NODE_OPTIONS=--use-env-proxy | Указан среди опций, разрешённых в NODE_OPTIONS | Как у флага. Более старые выпуски завершаются с --use-env-proxy is not allowed in NODE_OPTIONS |
Руководство по настройке корпоративной сети добавляет, что начиная с v22.21.0 и v24.5.0 тот же переключатель направляет через прокси и запросы node:http и node:https. Это совпало с тестом: https.get использовал прокси на 22.21.0 и 24.5.0 и по-прежнему шёл напрямую на 24.0.0 и 24.4.1, где поддержка охватывала только fetch.
На чём спотыкались тесты — чтобы не споткнулись вы:
- Задавайте переменные до запуска Node. Node читает их при старте. Скрипт, который сам присваивал
process.env.HTTPS_PROXY, а затем вызывалfetch, на всех версиях шёл напрямую. - Оставляйте схему в URL прокси. С
HTTPS_PROXY=127.0.0.1:3128процесс завершался сTypeError: Invalid URL(ERR_INVALID_URL) ещё до выполнения какого-либо кода. С учётными данными, но без схемы он завершался с ошибкойInvalid URL protocol(UND_ERR_INVALID_ARG). - Для URL https:// задавайте HTTPS_PROXY. Когда была задана только
HTTP_PROXY,fetchна 22.23.3, 24.21.0 и 26.10.0 всё равно использовал её для HTTPS-магазина, ноhttps.getшёл напрямую.HTTPS_PROXYпокрывает оба случая. - Указывайте включение в командной строке, а не в
--env-file. Node читалHTTPS_PROXYиз файла.env, ноNODE_USE_ENV_PROXY=1в том же файле не действовал на 22.21.0, 22.23.3, 24.5.0, 24.21.0 и 26.10.0. Из файла он срабатывал только на 24.0.0 и 24.4.1. Когда прокси был задан в.env, а флаг — в командной строке, прокси использовал каждый выпуск, в котором есть этот флаг:
node --env-file=.env --use-env-proxy price-check.mjs- На Node 22 ожидайте предупреждение. 22.21.0 и 22.23.3 печатали
[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.Запрос при этом всё равно шёл через прокси. - 407 — это проблема учётных данных, а не эта. С неверным паролем ошибка двумя уровнями ниже была
Proxy response (407) !== 200 when HTTP Tunneling. Этот случай разобран в руководстве по исправлению ошибки прокси 407.
Node 20 и более ранние версии: направьте fetch через диспетчер undici
У Node 20 нет встроенного включения, и, согласно графику выпусков Node.js, срок его поддержки истёк 30 апреля 2026 года. Встроенного включения нет и в 22.20 и более ранних версиях. Настоящее исправление — обновление. А пока установите undici из npm и сделайте её EnvHttpProxyAgent глобальным диспетчером. Он читает HTTP_PROXY, HTTPS_PROXY и NO_PROXY в любом регистре, как и встроенная поддержка:
npm install undici@7// proxy-setup.mjs: send the built-in fetch through HTTP(S)_PROXY on older Node.
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new EnvHttpProxyAgent());Загрузите этот модуль перед скриптом, чтобы price-check.mjs остался без изменений:
node --import ./proxy-setup.mjs price-check.mjsС undici 7.30.0 это работало через прокси на всех девяти протестированных выпусках, от 20.20.2 до 26.10.0. Если хотите ограничить прокси одним вызовом, передайте агент как dispatcher отдельного запроса:
import { EnvHttpProxyAgent } from 'undici';
const dispatcher = new EnvHttpProxyAgent();
const res = await fetch('https://shop.example.de/p/espressomuehle-k2', { dispatcher });
console.log(res.status);ProxyAgent принимает один URL прокси и отправляет через него каждый запрос. В отличие от EnvHttpProxyAgent, он не читает NO_PROXY:
import { ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent(process.env.HTTPS_PROXY);
const res = await fetch('https://shop.example.de/p/espressomuehle-k2', { dispatcher });
console.log(res.status);Оба варианта напечатали 200 через прокси на каждом протестированном выпуске, взяв учётные данные из URL.
Мажорную версию undici выбирайте внимательно. undici 7, согласно её package.json, требует Node 20.18.1 или новее. Если же взять undici 6.29.0, все три способа подключения работали на Node 20, 22 и 24, но не на 26.10.0: глобальный диспетчер игнорировался, и запрос шёл напрямую, а диспетчер undici 6 в отдельном запросе падал с UND_ERR_INVALID_ARG. У диспетчеров undici 8 обратная проблема на Node 22 и 24, как записано в руководстве по ошибке fetch failed, которое объясняет это расхождение версий. На Node 22.21 и новее, а также на 24 и новее NODE_USE_ENV_PROXY=1 вообще не требует зависимостей.
Распространённая ошибка: https-proxy-agent
Поищите прокси для Node — и вы найдёте https-proxy-agent. Это агент для node:http, node:https и построенных на них библиотек. У встроенного fetch нет опции agent, и если её передать, он молча её проигнорирует:
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
const res = await fetch('https://shop.example.de/p/espressomuehle-k2', { agent });
console.log(res.status);Ни на одной протестированной версии предупреждения не было. Запрос шёл напрямую и падал с UND_ERR_CONNECT_TIMEOUT примерно через 10,6 секунды — ровно так, как если бы агента не было. Тот же агент (https-proxy-agent 9.1.0) работает там, где ему место. Оба следующих примера напечатали 200 через прокси на всех девяти выпусках:
import https from 'node:https';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
https.get('https://shop.example.de/p/espressomuehle-k2', { agent }, (res) => {
console.log(res.statusCode);
res.resume();
});import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
const res = await fetch('https://shop.example.de/p/espressomuehle-k2', { agent });
console.log(res.status);Второй использует пакет node-fetch (3.3.2), а не встроенный fetch. Если у вас встроенный, используйте NODE_USE_ENV_PROXY или диспетчер undici.
NO_PROXY и ловушка пустой переменной в нижнем регистре
Когда включение активно, NO_PROXY перечисляет хосты, которые обходят прокси. Рядом с проверкой цен на том же хосте обычно работает то, что не должно ходить через платный резидентный прокси, например API локальной базы данных или проверка работоспособности. В тесте без NO_PROXY даже запрос к http://127.0.0.1 шёл через прокси.
Вот какими маршрутами fetch шёл к https://shop.example.de/ при разных значениях NO_PROXY — на Node 22.23.3, 24.21.0 и 26.10.0 с NODE_USE_ENV_PROXY=1 и на Node 20.20.2 с описанной выше настройкой undici 7.30.0:
| NO_PROXY | Маршрут для shop.example.de |
|---|---|
localhost,127.0.0.1 | Через прокси |
shop.example.de, .example.de или *.example.de | Напрямую |
shop.example.de:443 | Напрямую |
shop.example.de:8443 | Через прокси (порт не совпадает) |
* | Напрямую |
example.de | Напрямую на 20 (undici 7.30.0), 24.21.0 и 26.10.0; через прокси на 22.23.3 |
Документация Node по HTTP описывает example.com как точное совпадение хоста. https.get на 24.21.0 и 26.10.0 следовал этому и использовал прокси, тогда как fetch на тех же версиях считал, что example.de покрывает shop.example.de. Это расхождение отслеживается в nodejs/node#65616. Имена сравниваются так, как записаны, поэтому NO_PROXY=localhost не исключал http://127.0.0.1. Перечисляйте именно те хосты, которые имеете в виду, и указывайте и localhost, и 127.0.0.1, если нужны оба.
Документация Node говорит, что, когда заданы переменные в обоих регистрах, побеждает переменная в нижнем регистре. Странности начинаются, когда переменная в нижнем регистре задана, но пуста, — например, из-за export https_proxy=, забытого в профиле оболочки, или пустого значения в конфигурации CI или контейнера. В nodejs/node#66202 сообщается, что fetch и http.request() по-разному читают пустое значение. Этот скрипт отправляет один и тот же запрос обоими способами:
// which-route.mjs: send the same request with https.get and with fetch.
import https from 'node:https';
const url = 'https://shop.example.de/p/espressomuehle-k2';
const viaGet = await new Promise((resolve) => {
const req = https.get(url, { timeout: 15_000 }, (res) => resolve(res.statusCode));
req.on('timeout', () => req.destroy(Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' })));
req.on('error', (err) => resolve(err.code));
});
const viaFetch = await fetch(url, { signal: AbortSignal.timeout(15_000) })
.then((res) => res.status, (err) => err.cause?.code ?? err.name);
console.log({ viaGet, viaFetch });В тестовой сети 200 означает, что запрос прошёл через прокси, а таймаут — что он шёл напрямую. С NODE_USE_ENV_PROXY=1 на 22.21.0, 22.23.3, 24.5.0, 24.21.0 и 26.10.0:
| Окружение | https.get | fetch |
|---|---|---|
Задана HTTPS_PROXY | Через прокси (200) | Через прокси (200) |
Задана HTTPS_PROXY, https_proxy='' | Через прокси (200) | Напрямую (UND_ERR_CONNECT_TIMEOUT) |
Задана HTTPS_PROXY, no_proxy='', NO_PROXY='*' | Напрямую (ETIMEDOUT) | Через прокси (200) |
fetch считает пустую переменную в нижнем регистре заданной, поэтому https_proxy='' отключает для него прокси, а no_proxy='' отменяет NO_PROXY. https.get считает пустое значение незаданным и переходит к переменной в верхнем регистре. EnvHttpProxyAgent из undici 7.30.0 на Node 20 вёл себя как fetch. Вариант из issue с обычным HTTP воспроизвёлся так же. По состоянию на 2 октября 2026 года issue открыт, а предложенное исправление закрыто без слияния, поэтому не полагайтесь ни на одно из этих прочтений. Вместо этого удаляйте пустые переменные. Эта команда печатает, какие переменные прокси видит процесс, не показывая их значений:
node -e "for (const k of ['https_proxy', 'HTTPS_PROXY', 'http_proxy', 'HTTP_PROXY', 'no_proxy', 'NO_PROXY']) console.log(k, process.env[k] === undefined ? 'unset' : process.env[k] === '' ? 'EMPTY' : 'set')"
unset https_proxy no_proxyЗапускайте проверку в том же окружении, что и задание, — в строке cron, юните systemd или контейнере, а не только в своём терминале. Как эти переменные читают curl, Python и Node, разобрано в руководстве о переменных окружения прокси.
Какое исправление для какой версии Node
| Node.js | Встроенная undici (протестированный выпуск) | Что использовать |
|---|---|---|
| 20.x (срок поддержки истёк) | 6.24.1 (20.20.2) | EnvHttpProxyAgent из undici 7 через --import ./proxy-setup.mjs, затем обновление |
| С 22.0 по 22.20 | 6.21.2 (22.20.0) | Диспетчер undici 7 или обновление до последней 22.x |
| 22.21 и новее | от 6.22.0 до 6.28.1 (22.21.0, 22.23.3) | NODE_USE_ENV_PROXY=1 или --use-env-proxy |
| С 24.0 по 24.4 | от 7.8.0 до 7.11.0 (24.0.0, 24.4.1) | NODE_USE_ENV_PROXY=1; флага ещё нет |
| 24.5 и новее | от 7.12.0 до 7.29.1 (24.5.0, 24.21.0) | NODE_USE_ENV_PROXY=1 или --use-env-proxy |
| 26.x | 8.10.2 (26.10.0) | NODE_USE_ENV_PROXY=1 или --use-env-proxy |
Node 18 и более ранние версии, а также нечётные линейки не тестировались. Проверьте версию командой node -v, используя тот же бинарник, которым запускается задание.
Проверка цены от начала до конца
Скрипт — тот же файл, что и в начале. Меняется только способ запуска. На Node 22.21 или новее либо на 24 и новее:
export HTTPS_PROXY='http://USERNAME:PASSWORD@proxy.example.net:8080'
export NO_PROXY='localhost,127.0.0.1'
NODE_USE_ENV_PROXY=1 node price-check.mjs
# 2026-10-02T13:20:12.538Z 49.90 EURНа Node 20, с установленным undici@7 и файлом proxy-setup.mjs рядом со скриптом:
node --import ./proxy-setup.mjs price-check.mjs
# 2026-10-02T13:20:11.398Z 49.90 EURПроцесс продолжает работать, и setInterval повторяет проверку каждый час. В тесте прокси записал для проверки один аутентифицированный CONNECT shop.example.de:443, а магазин получил запрос без заголовка Proxy-Authorization. Один лишь ответ 200 не доказывает маршрут: подтвердите его в панели управления или журнале запросов вашего провайдера либо один раз направьте скрипт на эндпоинт под вашим контролем, который сообщает IP-адрес вызывающей стороны.
Как это проверялось
Тесты выполнялись 2 октября 2026 года на VPS с Ubuntu (linux-x64) с официальными архивами Node.js v20.20.2, v22.20.0, v22.21.0, v22.23.3, v24.0.0, v24.4.1, v24.5.0, v24.21.0 и v26.10.0, каждый из которых сверен с опубликованным SHA-256. Пакеты npm: undici 7.30.0 и 6.29.0, https-proxy-agent 9.1.0 и node-fetch 3.3.2. curl 8.18.0.
Каждый случай выполнялся в собственном процессе внутри отдельного сетевого пространства имён Linux. Там shop.example.de разрешался в 203.0.113.10 — адрес, зарезервированный для документации, — а каждый пакет на адрес вне loopback отбрасывался, так что прямое соединение могло закончиться только таймаутом. Единственным путём к магазину был loopback forward-прокси, который требовал Basic-аутентификацию и записывал каждый CONNECT и каждый пересланный запрос. За ним локальный HTTPS-сервер обслуживал shop.example.de с сертификатом от одноразового CA, которому процессы доверяли через NODE_EXTRA_CA_CERTS, а curl — через CURL_CA_BUNDLE. Тестовые учётные данные были вымышленными, а в примерах выше их заменили заполнители. Маршрут считается «через прокси», только если прокси записал аутентифицированный запрос для этого хоста.
Тест не охватывал macOS и Windows (где имена переменных окружения нечувствительны к регистру), HTTPS- и SOCKS-прокси, сеть реального провайдера, Docker, а также Node 18, 23 и 25. Тайминги получены внутри пространства имён и ничего не говорят о скорости реального прокси.
Связанные руководства
- Переменные окружения прокси: HTTP_PROXY и NO_PROXY объясняет, как эти переменные читают curl, Python Requests и Node.
- Как исправить ошибку прокси 407, не гадая поможет, когда прокси уже используется, но отклоняет ваши учётные данные.
- Как исправить ERR_TUNNEL_CONNECTION_FAILED разбирает те же отказы прокси в Playwright и Puppeteer.
- Исправляем TypeError: fetch failed в Node.js за прокси расшифровывает все остальные причины
fetch failed.
ipvolt — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- Node.js Learn: Enterprise network configuration
- Node.js CLI: NODE_USE_ENV_PROXY=1
- Node.js CLI: --use-env-proxy
- Node.js HTTP: built-in proxy support and the NO_PROXY format
- nodejs/node PR #57165: support HTTP[S]_PROXY environment variables in fetch
- nodejs/node issue #66202: fetch() and http.request() disagree when a lower-cased proxy variable is empty
- nodejs/node issue #65616: NO_PROXY=example.com and subdomains, http.request() vs fetch()
- undici 7.30.0: EnvHttpProxyAgent
- undici 7.30.0: ProxyAgent
- Node.js release schedule (end-of-life dates)