# Node.js fetch Ignores HTTPS_PROXY: Fix UND_ERR_CONNECT_TIMEOUT in One Line

Source: https://ipvolt.com/guides/node-fetch-ignores-https-proxy
Markdown: https://ipvolt.com/guides/node-fetch-ignores-https-proxy.md

[Home](https://ipvolt.com/index.md) / [Guides](https://ipvolt.com/guides.md) / [Node.js fetch Ignores HTTPS_PROXY: Fix UND_ERR_CONNECT_TIMEOUT in One Line](https://ipvolt.com/guides/node-fetch-ignores-https-proxy.md)

Category: Troubleshooting
Reading time: 11 minutes
Author: ipvolt

Node's built-in fetch silently skips HTTP_PROXY and HTTPS_PROXY, so requests time out with UND_ERR_CONNECT_TIMEOUT while curl works. Here's why, and how to fix it on every Node version.

Your script sets `HTTPS_PROXY`, calls `fetch()`, waits ten seconds and dies with this:

```text
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.0
```

curl, run in the same shell with the same `HTTPS_PROXY`, gets through:

```sh
curl -sS -o /dev/null -w '%{http_code}\n' https://shop.example.de/p/espressomuehle-k2
# 200
```

Nothing is wrong with the proxy. Node's built-in `fetch` doesn't read `HTTP_PROXY` or `HTTPS_PROXY` unless you tell it to, so it tried to reach the shop directly. The short fix, on Node 22.21 or later and on Node 24 and later:

```sh
NODE_USE_ENV_PROXY=1 node price-check.mjs
```

On older releases, including Node 20, you need an undici dispatcher instead. Both are below, along with the traps around them. Every command and output on this page was run on 2 October 2026 against a logging test proxy, on the Node versions listed in [How this was tested](#how-this-was-tested).

## The script that times out

The running example is a small job that checks the price of one product on a German shop every hour, through a residential proxy. `shop.example.de` stands in for the real shop:

```js
// 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);
```

The proxy comes from the environment, the way curl, Python Requests and most CLI tools expect it. `proxy.example.net`, `USERNAME` and `PASSWORD` are placeholders for your provider's gateway and credentials; percent-encode any reserved characters in them, and load the real values from your secret manager rather than typing them into shell history:

```sh
export HTTPS_PROXY='http://USERNAME:PASSWORD@proxy.example.net:8080'
node price-check.mjs
```

On every Node version tested, from 20.20.2 to 26.10.0, this run never contacted the proxy. It failed after about 10.6 seconds with the error above. Node 20 and 22 print the same `[cause]`, under a `TypeError: fetch failed` line with a stack trace instead of the bracketed form.

## Why Node's fetch ignores HTTPS_PROXY

Node's built-in `fetch` is [undici](https://github.com/nodejs/undici). Its default dispatcher opens a direct connection to whatever host the URL names. Proxy variables are only read when you opt in: [PR #57165](https://github.com/nodejs/node/pull/57165) added that support in 2025 behind `NODE_USE_ENV_PROXY`, and its description calls the opt-in a first step, with turning it on by default left for later.

So the request goes straight to `shop.example.de:443`. In a network where only the proxy may reach the internet, the connection attempt gets no answer, and undici gives up after its 10-second connect timeout with `UND_ERR_CONNECT_TIMEOUT`. Read the address in the message: `attempted address: shop.example.de:443` is the shop, so the proxy was never used. If the message names your proxy's host instead, the proxy itself is unreachable, which is a different problem; [Fix Node.js TypeError: fetch failed behind a proxy](/guides/fix-node-fetch-failed-proxy) decodes that and the other cause strings.

Where direct traffic is allowed, there is no error at all. In the test, with no opt-in, a fetch to a host the machine could reach directly returned 200 and the proxy logged nothing. For a price check, that means the shop sees your server's own address instead of the proxy's.

## The one-line fix: NODE_USE_ENV_PROXY=1 or --use-env-proxy

Turn on Node's built-in proxy support for the process:

```sh
NODE_USE_ENV_PROXY=1 node price-check.mjs
# 2026-10-02T12:35:44.149Z 49.90 EUR
```

The command-line flag does the same thing, and it also works in `NODE_OPTIONS`:

```sh
node --use-env-proxy price-check.mjs
NODE_OPTIONS=--use-env-proxy node price-check.mjs
```

With either one, every tested release that supports it sent `CONNECT shop.example.de:443` to the proxy, with the credentials from the URL, and printed the price. The two switches arrived in different releases:

| Opt-in | Added in (Node.js docs) | Test result |
| --- | --- | --- |
| [`NODE_USE_ENV_PROXY=1`](https://nodejs.org/api/cli.html#node_use_env_proxy1) | v24.0.0, v22.21.0 | Proxied on 22.21.0, 22.23.3, 24.0.0, 24.4.1, 24.5.0, 24.21.0 and 26.10.0. Silently ignored on 20.20.2 and 22.20.0, which timed out as before |
| [`--use-env-proxy`](https://nodejs.org/api/cli.html#--use-env-proxy) | v24.5.0, v22.21.0 | Proxied on 22.21.0, 22.23.3, 24.5.0, 24.21.0 and 26.10.0. Node 20.20.2, 22.20.0, 24.0.0 and 24.4.1 refused to start: `bad option: --use-env-proxy` |
| `NODE_OPTIONS=--use-env-proxy` | Listed among the options allowed in `NODE_OPTIONS` | Same as the flag. Older releases exit with `--use-env-proxy is not allowed in NODE_OPTIONS` |

The [Enterprise network configuration](https://nodejs.org/learn/http/enterprise-network-configuration) guide adds that the same switch routes `node:http` and `node:https` requests from v22.21.0 and v24.5.0. That matched the test: `https.get` used the proxy on 22.21.0 and 24.5.0, and still went direct on 24.0.0 and 24.4.1, where only `fetch` was covered.

Things that tripped the test, so they don't trip you:

- **Set the variables before Node starts.** Node reads them at startup. A script that assigned `process.env.HTTPS_PROXY` itself and then called `fetch` went direct on every version.
- **Keep the scheme in the proxy URL.** With `HTTPS_PROXY=127.0.0.1:3128`, the process exited before running any code with `TypeError: Invalid URL` (`ERR_INVALID_URL`). With credentials but no scheme, it exited with an `Invalid URL protocol` error (`UND_ERR_INVALID_ARG`).
- **Set HTTPS_PROXY for https:// URLs.** With only `HTTP_PROXY` set, `fetch` still used it for the HTTPS shop on 22.23.3, 24.21.0 and 26.10.0, but `https.get` went direct. `HTTPS_PROXY` covers both.
- **Put the opt-in on the command line, not in an `--env-file`.** Node read `HTTPS_PROXY` from a `.env` file, but `NODE_USE_ENV_PROXY=1` in the same file had no effect on 22.21.0, 22.23.3, 24.5.0, 24.21.0 or 26.10.0. It only worked from the file on 24.0.0 and 24.4.1. With the proxy in `.env` and the flag on the command line, every release that has the flag used the proxy:

```sh
node --env-file=.env --use-env-proxy price-check.mjs
```

- **Expect a warning on Node 22.** 22.21.0 and 22.23.3 printed `[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.` The request still went through the proxy.
- **A 407 is a credentials problem, not this one.** With a wrong password, the error two levels down was `Proxy response (407) !== 200 when HTTP Tunneling`. [Fix proxy error 407](/guides/fix-proxy-error-407) covers it.

## Node 20 and older: route fetch through an undici dispatcher

Node 20 has no built-in opt-in, and it reached end-of-life on 30 April 2026 according to the [Node.js release schedule](https://github.com/nodejs/Release/blob/main/schedule.json). The same applies to 22.20 and earlier. Upgrading is the real fix. Until then, install undici from npm and set its `EnvHttpProxyAgent` as the global dispatcher. It reads `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`, in either case, like the built-in support does:

```sh
npm install undici@7
```

```js
// proxy-setup.mjs: send the built-in fetch through HTTP(S)_PROXY on older Node.
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new EnvHttpProxyAgent());
```

Load it before the script, so `price-check.mjs` stays unchanged:

```sh
node --import ./proxy-setup.mjs price-check.mjs
```

With undici 7.30.0, this used the proxy on all nine tested releases, from 20.20.2 to 26.10.0. If you'd rather keep the proxy to one call, pass the agent as a per-request `dispatcher`:

```js
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` takes one proxy URL and sends every request through it. Unlike `EnvHttpProxyAgent`, it doesn't read `NO_PROXY`:

```js
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);
```

Both printed `200` through the proxy on every tested release, with the credentials taken from the URL.

Pick the undici major with care. undici 7 requires Node 20.18.1 or later, according to its `package.json`. With undici 6.29.0 instead, all three wirings worked on Node 20, 22 and 24, but not on 26.10.0: the global dispatcher was ignored and the request went direct, and a per-request undici 6 dispatcher failed with `UND_ERR_INVALID_ARG`. undici 8 dispatchers have the opposite problem on Node 22 and 24, as recorded in [the fetch failed guide](/guides/fix-node-fetch-failed-proxy), which explains that version skew. On Node 22.21 and later, and on 24 and later, `NODE_USE_ENV_PROXY=1` needs no dependency at all.

## A common wrong turn: https-proxy-agent

Search for a Node proxy and you'll find `https-proxy-agent`. It is an agent for `node:http`, `node:https` and libraries built on them. The built-in `fetch` has no `agent` option and silently ignores one:

```js
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);
```

There was no warning on any tested version. The request went direct and failed with `UND_ERR_CONNECT_TIMEOUT` after about 10.6 seconds, exactly as if the agent weren't there. The same agent (https-proxy-agent 9.1.0) works where it belongs. Both of these printed 200 through the proxy on all nine releases:

```js
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();
});
```

```js
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);
```

The second one uses the `node-fetch` package (3.3.2), not the built-in `fetch`. If you're on the built-in one, use `NODE_USE_ENV_PROXY` or an undici dispatcher.

## NO_PROXY, and the empty lower-case variable trap

Once the opt-in is on, `NO_PROXY` lists the hosts that skip the proxy. A price-check host usually runs next to things that shouldn't go through a paid residential proxy, such as a local database API or a health check. In the test, without `NO_PROXY`, even a request to `http://127.0.0.1` went through the proxy.

These are the routes `fetch` took to `https://shop.example.de/` with different `NO_PROXY` values, on Node 22.23.3, 24.21.0 and 26.10.0 with `NODE_USE_ENV_PROXY=1`, and on Node 20.20.2 with the undici 7.30.0 setup above:

| NO_PROXY | Route for shop.example.de |
| --- | --- |
| `localhost,127.0.0.1` | Proxy |
| `shop.example.de`, `.example.de` or `*.example.de` | Direct |
| `shop.example.de:443` | Direct |
| `shop.example.de:8443` | Proxy (the port doesn't match) |
| `*` | Direct |
| `example.de` | Direct on 20 (undici 7.30.0), 24.21.0 and 26.10.0; proxy on 22.23.3 |

Node's HTTP docs describe `example.com` as an exact host match. `https.get` followed that on 24.21.0 and 26.10.0 and used the proxy, while `fetch` on the same versions treated `example.de` as covering `shop.example.de`. [nodejs/node#65616](https://github.com/nodejs/node/issues/65616) tracks the difference. Names are compared as written, so `NO_PROXY=localhost` didn't exempt `http://127.0.0.1`. List the exact hosts you mean, and include both `localhost` and `127.0.0.1` when you need both.

Node's docs say the lower-case variable wins when both cases are set. That gets odd when the lower-case one is set but empty, for example by an `export https_proxy=` left in a shell profile or by an empty value in a CI or container config. [nodejs/node#66202](https://github.com/nodejs/node/issues/66202) reports that `fetch` and `http.request()` read an empty value differently. This script sends the same request both ways:

```js
// 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 });
```

In the test network, 200 means the request went through the proxy, and a timeout means it went direct. With `NODE_USE_ENV_PROXY=1` on 22.21.0, 22.23.3, 24.5.0, 24.21.0 and 26.10.0:

| Environment | `https.get` | `fetch` |
| --- | --- | --- |
| `HTTPS_PROXY` set | Proxy (200) | Proxy (200) |
| `HTTPS_PROXY` set, `https_proxy=''` | Proxy (200) | Direct (`UND_ERR_CONNECT_TIMEOUT`) |
| `HTTPS_PROXY` set, `no_proxy=''`, `NO_PROXY='*'` | Direct (`ETIMEDOUT`) | Proxy (200) |

`fetch` treats an empty lower-case variable as set, so `https_proxy=''` switches the proxy off for it and `no_proxy=''` cancels `NO_PROXY`. `https.get` treats an empty value as unset and falls back to the upper-case one. The undici 7.30.0 `EnvHttpProxyAgent` on Node 20 behaved like `fetch`. The plain-HTTP version from the issue reproduced the same way. On 2 October 2026 the issue is open and [the proposed fix](https://github.com/nodejs/node/pull/66210) was closed without merging, so don't rely on either reading. Unset the empty variables instead. This prints which proxy variables a process sees, without their values:

```sh
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
```

Run the check in the same environment as the job, such as the cron line, systemd unit or container, not just your terminal. For how curl, Python and Node each read these variables, see [proxy environment variables](/guides/proxy-environment-variables).

## Which fix for which Node version

| Node.js | Bundled undici (tested release) | What to use |
| --- | --- | --- |
| 20.x (end-of-life) | 6.24.1 (20.20.2) | undici 7 `EnvHttpProxyAgent` via `--import ./proxy-setup.mjs`, then upgrade |
| 22.0 to 22.20 | 6.21.2 (22.20.0) | The undici 7 dispatcher, or update to the latest 22.x |
| 22.21 and later | 6.22.0 to 6.28.1 (22.21.0, 22.23.3) | `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` |
| 24.0 to 24.4 | 7.8.0 to 7.11.0 (24.0.0, 24.4.1) | `NODE_USE_ENV_PROXY=1`; the flag doesn't exist yet |
| 24.5 and later | 7.12.0 to 7.29.1 (24.5.0, 24.21.0) | `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` |
| 26.x | 8.10.2 (26.10.0) | `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` |

Node 18 and earlier, and the odd-numbered lines, were not tested. Check your version with `node -v`, using the same binary the job runs.

## The price check, end to end

The script is the same file as at the top. Only the way it starts changes. On Node 22.21 or later, or 24 and later:

```sh
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
```

On Node 20, with `undici@7` installed and `proxy-setup.mjs` next to the script:

```sh
node --import ./proxy-setup.mjs price-check.mjs
# 2026-10-02T13:20:11.398Z 49.90 EUR
```

The process stays running, and `setInterval` repeats the check every hour. In the test, the proxy logged one authenticated `CONNECT shop.example.de:443` for the check, and the shop received the request without the `Proxy-Authorization` header. A 200 alone doesn't prove the route: confirm it in your provider's dashboard or request log, or point the script once at an endpoint you control that reports the caller's IP address.

## How this was tested

On 2 October 2026, on an Ubuntu VPS (linux-x64), with the official Node.js tarballs v20.20.2, v22.20.0, v22.21.0, v22.23.3, v24.0.0, v24.4.1, v24.5.0, v24.21.0 and v26.10.0, each checked against its published SHA-256. npm packages: undici 7.30.0 and 6.29.0, https-proxy-agent 9.1.0 and node-fetch 3.3.2. curl 8.18.0.

Each case ran as its own process inside a separate Linux network namespace. There, `shop.example.de` resolved to 203.0.113.10, an address reserved for documentation, and every packet to a non-loopback address was dropped, so a direct connection could only time out. The only route to the shop was a loopback forward proxy that required Basic authentication and logged every `CONNECT` and forwarded request. Behind it, a local HTTPS server answered for `shop.example.de` with a certificate from a throwaway CA, which the processes trusted through `NODE_EXTRA_CA_CERTS` and curl through `CURL_CA_BUNDLE`. The test credentials were invented and the placeholders above replaced them. A route counts as "proxy" only when the proxy logged an authenticated request for that host.

The test didn't cover macOS or Windows (where environment variable names aren't case-sensitive), HTTPS or SOCKS proxies, a real provider's network, Docker, or Node 18, 23 and 25. Timings come from the namespace and say nothing about a real proxy's speed.

## Related guides

- [Proxy environment variables: HTTP_PROXY and NO_PROXY](/guides/proxy-environment-variables) explains how curl, Python Requests and Node each read the variables.
- [Fix proxy error 407 without guessing](/guides/fix-proxy-error-407) helps once the proxy is in use but rejects your credentials.
- [Fix ERR_TUNNEL_CONNECTION_FAILED](/guides/fix-err-tunnel-connection-failed) covers the same proxy refusals in Playwright and Puppeteer.
- [Fix Node.js TypeError: fetch failed behind a proxy](/guides/fix-node-fetch-failed-proxy) decodes every other cause behind `fetch failed`.

ipvolt is a proxy service for developers that isn't open yet; [join the early-access list](https://ipvolt.com/#waitlist-closing) for one email when it opens.

## Sources & further reading

- [Node.js Learn: Enterprise network configuration](https://nodejs.org/learn/http/enterprise-network-configuration)
- [Node.js CLI: NODE_USE_ENV_PROXY=1](https://nodejs.org/api/cli.html#node_use_env_proxy1)
- [Node.js CLI: --use-env-proxy](https://nodejs.org/api/cli.html#--use-env-proxy)
- [Node.js HTTP: built-in proxy support and the NO_PROXY format](https://nodejs.org/api/http.html#built-in-proxy-support)
- [nodejs/node PR #57165: support HTTP[S]_PROXY environment variables in fetch](https://github.com/nodejs/node/pull/57165)
- [nodejs/node issue #66202: fetch() and http.request() disagree when a lower-cased proxy variable is empty](https://github.com/nodejs/node/issues/66202)
- [nodejs/node issue #65616: NO_PROXY=example.com and subdomains, http.request() vs fetch()](https://github.com/nodejs/node/issues/65616)
- [undici 7.30.0: EnvHttpProxyAgent](https://github.com/nodejs/undici/blob/v7.30.0/docs/docs/api/EnvHttpProxyAgent.md)
- [undici 7.30.0: ProxyAgent](https://github.com/nodejs/undici/blob/v7.30.0/docs/docs/api/ProxyAgent.md)
- [Node.js release schedule (end-of-life dates)](https://github.com/nodejs/Release/blob/main/schedule.json)

## Related guides

- [Proxy environment variables: HTTP_PROXY and NO_PROXY](https://ipvolt.com/guides/proxy-environment-variables.md)
- [Fix proxy error 407 without guessing](https://ipvolt.com/guides/fix-proxy-error-407.md)
- [Fix ERR_TUNNEL_CONNECTION_FAILED in Playwright and Puppeteer](https://ipvolt.com/guides/fix-err-tunnel-connection-failed.md)
- [Fix Node.js TypeError: fetch failed behind a proxy](https://ipvolt.com/guides/fix-node-fetch-failed-proxy.md)
- [Use a proxy with Node.js fetch](https://ipvolt.com/guides/nodejs-fetch-proxy.md)

## About ipvolt

Examples use generic proxy settings, with links to the original technical documentation. Product-specific behavior must be checked with your provider. ipvolt is still in development.

## Know when access opens.

ipvolt · In development

We’re building proxy infrastructure for developers and data teams. Join the interest list for a heads-up when ipvolt is ready.

Consent: One email when access opens. Nothing else.

[Get early access](https://ipvolt.com/guides/node-fetch-ignores-https-proxy#waitlist-closing). Use the email form on this page to join the interest list.

[Privacy](https://ipvolt.com/privacy)

