Node ignores HTTP_PROXY unless you opt in
published
TL;DR
Setting HTTP_PROXY does nothing to Node’s fetch() or http.request() by default, even on current Node. Proxy support exists — it is gated behind NODE_USE_ENV_PROXY=1 (or --use-env-proxy), added for fetch() in v24.0.0 and for node:http/node:https in v24.5.0. Every other tool on the box (curl, git, pip, your shell) reads those variables automatically, so the mismatch reads as “Node can’t see the network” rather than “Node isn’t using the proxy”.
The problem
The cheapest way to see it is to point HTTP_PROXY at a port where nothing is listening. If Node used the proxy, the request would have to fail:
// proxytest.mjs
import http from 'node:http';
const url = 'http://example.com/';
try {
const res = await fetch(url);
console.log(`fetch: OK ${res.status} (proxy ignored)`);
} catch (e) {
console.log(`fetch: threw ${e.message} | cause: ${e.cause?.code}`);
}
http.get(url, (r) => {
r.resume();
console.log(`http.get: OK ${r.statusCode} (proxy ignored)`);
}).on('error', (e) => console.log(`http.get: threw ${e.code}`));
On Node 26.2.0:
$ HTTP_PROXY=http://127.0.0.1:9 node proxytest.mjs
fetch: OK 200 (proxy ignored)
http.get: OK 200 (proxy ignored)
$ HTTP_PROXY=http://127.0.0.1:9 NODE_USE_ENV_PROXY=1 node proxytest.mjs
fetch: threw fetch failed | cause: ECONNREFUSED
http.get: threw ECONNREFUSED
Two 200s in the first run mean both clients ignored the proxy entirely and went straight out. The ECONNREFUSED in the second run is the good outcome — it proves the proxy was really dialled.
That first run is the expensive shape, because nothing fails. Your service quietly bypasses the proxy it was supposed to route through, and the auditing, allowlisting or caching that proxy exists to provide simply does not happen. There is no error to search for.
The other shape shows up on a network where only the proxy has egress. There you do get an error, and it blames the destination: TypeError: fetch failed with a cause of ECONNREFUSED or ETIMEDOUT naming the host you asked for. Nothing in it mentions a proxy, HTTP_PROXY is set, and curl to the same URL works — so the debugging you start is about DNS, IPv6 or firewall rules, none of which is the bug.
Why it happens
Node’s fetch() is undici, and undici’s default dispatcher has no proxy behaviour attached. node:http predates the whole convention. Neither ever read the environment, and when support was finally added it was made opt-in on purpose — turning it on rewrites the routing of every outbound request in the process, including calls to services on your own network, so it could not be switched on by default without breaking existing deployments.
What that means in practice depends on your Node version:
| Client | Reads HTTP_PROXY | Since |
|---|---|---|
global fetch() | only with NODE_USE_ENV_PROXY=1 or --use-env-proxy | v24.0.0 |
http.request() / https.request() / default agent | same flag | v24.5.0 |
new https.Agent({ proxyEnv: … }) | per-agent, no global flag | v24.5.0 |
undici’s EnvHttpProxyAgent | always, once installed as the dispatcher | any version with undici available |
| anything before v24 | never | — |
Node reads both spellings of each variable — http_proxy/HTTP_PROXY, https_proxy/HTTPS_PROXY, no_proxy/NO_PROXY. On Windows the distinction is moot anyway, because process.env is case-insensitive there.
What to do
Turn the flag on. This is the whole fix in most cases:
NODE_USE_ENV_PROXY=1 node server.js
# or
node --use-env-proxy server.js
NO_PROXY is honoured, and it is how you keep internal traffic direct. Verified on 26.2.0 — with the proxy pointed at a dead port, adding the exclusion makes the request succeed again:
$ HTTP_PROXY=http://127.0.0.1:9 NODE_USE_ENV_PROXY=1 NO_PROXY=example.com node proxytest.mjs
fetch: OK 200 (proxy ignored)
http.get: OK 200 (proxy ignored)
Proxy one agent instead of the process. When only part of your traffic should be proxied, skip the global flag and put it on the agent:
import https from 'node:https';
const agent = new https.Agent({
proxyEnv: { HTTPS_PROXY: 'http://proxy.example.com:8080' },
});
https.get('https://example.com/', { agent }, (res) => {
console.log(res.statusCode);
});
On older Node, or when you cannot control the launch command, install undici’s env-reading dispatcher yourself. This makes global fetch() honour the variables from inside the program, with no flag:
// undicitest.mjs
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new EnvHttpProxyAgent());
try {
const res = await fetch('http://example.com/'); // now goes through HTTP_PROXY
console.log(`fetch: OK ${res.status} (proxy ignored)`);
} catch (e) {
console.log(`fetch: ${e.message} | cause: ${e.cause?.code}`);
}
$ HTTP_PROXY=http://127.0.0.1:9 node undicitest.mjs
fetch: fetch failed | cause: ECONNREFUSED
That run used undici 8.10.1 as a direct dependency, and note what it does not need: NODE_USE_ENV_PROXY is unset there.
Diagnose it in one line. Before assuming a network problem, point the proxy at a closed port and see whether the request still succeeds. A 200 means nothing is proxying anything:
HTTP_PROXY=http://127.0.0.1:9 node -e "fetch('http://example.com').then(r => console.log(r.status))"
Caveats
- The flag is process-wide. Once on, internal service-to-service calls go through the proxy too —
NO_PROXYis the only carve-out, and it is easy to forget the entries for your own cluster. - Node’s changelog states the variables are parsed during startup, so mutating
process.env.HTTP_PROXYlater in the program is not the way to configure this. - This covers Node’s own clients only.
axios,got,node-fetchand the AWS/GCP SDKs each have their own proxy configuration and are unaffected by the flag unless they happen to use the global dispatcher. - The
proxyEnvoption takes an object shaped like the environment, not a URL string — passing a bare URL is not the same API. - Everything above was run on Node 26.2.0; the version column is from Node’s own changelog, not from testing each release.
References
- Node.js v24 changelog — built-in proxy support in
request()andAgent— documents theNODE_USE_ENV_PROXYbehaviour, theproxyEnvagent option, and thatfetch()gained support in v24.0.0 - nodejs/node#57165 — http: support HTTP[S]_PROXY environment variables in fetch
- nodejs/node#57872 — tracking issue: HTTP_PROXY/HTTPS_PROXY/NO_PROXY support in Node.js
- Node.js CLI documentation — options and environment variables
- undici —
EnvHttpProxyAgent