Node Fetch Proxy: Native fetch, node-fetch and Agents

There are three ways to send a Node.js fetch() call through a proxy, and the right one depends on which fetch you use. Native fetch (built into Node.js) takes an Undici ProxyAgent as its dispatcher, or reads proxy environment variables on recent Node versions. The node-fetch package takes an http.Agent such as HttpsProxyAgent through its agent option. This guide gives you working code for each, explains why they differ, and shows how to debug the failures you will hit first.
TL;DR
- Native fetch: pass
{ dispatcher: new ProxyAgent(proxyUrl) }from theundicipackage, or callsetGlobalDispatcheronce for the whole process. - Environment variables: on Node.js v24.0.0+ or v22.21.0+, set
HTTP_PROXY,HTTPS_PROXYandNODE_USE_ENV_PROXY=1and fetch follows them. - node-fetch: it ignores proxy variables, so pass an
HttpsProxyAgentas theagentoption. - Credentials: put them in the proxy URL, but load them from a secret manager or environment, never from committed code.
Table of Contents
- Why fetch needs extra setup to use a proxy
- Method 1: native fetch with Undici ProxyAgent
- Method 2: environment variables and built-in proxy support
- Method 3: node-fetch with an HTTPS proxy agent
- Handling credentials safely
- Debugging a proxied fetch
- What changes when the proxy is a mobile carrier IP
- Our perspective
- Try it with real US carrier IPs
- FAQ
- Sources
Why fetch needs extra setup to use a proxy
Browsers hide proxies from your code: the operating system or browser decides. In Node.js, the network layer is yours. Historically http.request took an agent, and libraries built on it (node-fetch, Axios, Got) inherited that model. Native fetch() arrived in Node.js through Undici, which uses a different abstraction called a dispatcher. That is why an agent passed to native fetch does nothing, and why a dispatcher passed to node-fetch does nothing either.
The gap was a known pain point: a 2022 GitHub issue is still among the top results for this topic, because the new API could not use an HTTP proxy out of the box. The situation has improved, which is why there are now three routes instead of one.
Pick your route by answering two questions:
| Situation | Best route |
|---|---|
| Native fetch, per-request control, different proxies for different calls | Undici ProxyAgent passed as dispatcher |
| Native fetch, one proxy for the whole process, recent Node.js | Environment variables with NODE_USE_ENV_PROXY=1 |
Existing code using node-fetch | HttpsProxyAgent as agent |
| Existing code that must pick a proxy from environment per URL | proxy-agent package |
Method 1: native fetch with Undici ProxyAgent
This is the most explicit approach and works on any Node.js version that ships native fetch. Install Undici:
npm install undici
Then create a ProxyAgent and hand it to fetch as the dispatcher:
import { ProxyAgent } from 'undici';
const proxyUrl = process.env.PROXY_URL; // http://USER:PASS@host:port
const dispatcher = new ProxyAgent(proxyUrl);
const res = await fetch('https://api.ipify.org?format=json', { dispatcher });
console.log(await res.json());
According to the Undici ProxyAgent documentation, the agent routes requests through HTTP, HTTPS or SOCKS5 proxies and works anywhere a dispatcher is accepted, fetch included. Because the dispatcher is per call, you can send one request through a US city exit and the next one direct.

Set it once for the whole process
If every outbound call should use the proxy, set a global dispatcher at startup:
import { ProxyAgent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new ProxyAgent(process.env.PROXY_URL));
const res = await fetch('https://example.com');
Every fetch() after that line is proxied, including calls made by libraries you did not write. That is convenient and also a risk: your calls to internal services or your own API will go out through the proxy too. For scrapers that talk to many targets and one database, the per-request dispatcher is usually the safer choice.
Authentication options in ProxyAgent
The simplest form puts the username and password in the URL. If you would rather keep them separate, Undici's ProxyAgent also accepts an options object. The documentation lists token for a preformatted proxy-authorization value, auth for base64-encoded username:password, extra headers, and TLS settings for both the proxy connection (proxyTls) and the destination (requestTls).
const dispatcher = new ProxyAgent({
uri: 'http://proxy.example.com:8080',
token: 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64'),
});
Special characters in a password (@, :, /, #) break URL parsing. Either percent-encode them with encodeURIComponent or use the options form above.
Method 2: environment variables and built-in proxy support
Recent Node.js releases can route fetch through a proxy with no code change. The Node.js Learn guide on enterprise network configuration describes it: set HTTP_PROXY, HTTPS_PROXY and optionally NO_PROXY, then enable support with NODE_USE_ENV_PROXY=1 or the --use-env-proxy flag. Fetch then uses the configured proxies unless an agent is overridden or the target matches NO_PROXY.
export HTTP_PROXY=http://USER:[email protected]:8080
export HTTPS_PROXY=http://USER:[email protected]:8080
export NO_PROXY=localhost,127.0.0.1
NODE_USE_ENV_PROXY=1 node scraper.mjs
Which Node.js versions support it
Version matters here, and it is the most common reason this method silently does nothing. The same Node.js guide documents fetch support starting in v24.0.0 and v22.21.0, and node:http / node:https support starting in v24.5.0 and v22.21.0. The HTTPS API reference records the proxyEnv option as added in those same releases. Run node --version first. On an older release, use Method 1.
Pointing Undici at the environment itself
If you want environment-driven behavior on versions without the built-in flag, Undici ships EnvHttpProxyAgent. Its documentation says it reads http_proxy, https_proxy and no_proxy, also honors the uppercase variants, and sends HTTP and HTTPS origins through their matching proxies. You can use it globally with setGlobalDispatcher or per request with fetch(url, { dispatcher }).
The no_proxy list accepts comma- or space-separated hosts, leading-dot or *. wildcards, and optional ports. A value of * bypasses the proxy for every request, which is worth remembering when a proxy "mysteriously" is not being used on a machine where someone set it.
Method 3: node-fetch with an HTTPS proxy agent
The node-fetch package predates native fetch and is still common in older codebases. It does not read proxy variables. Microsoft's Dev Proxy guide for Node.js states this directly and demonstrates the fix: create an HttpsProxyAgent and pass it as fetch's agent option. The node-fetch README documents that agent option as the hook for networking behavior beyond the Fetch spec.
npm install node-fetch https-proxy-agent
import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent(process.env.PROXY_URL);
const res = await fetch('https://api.ipify.org?format=json', { agent });
console.log(await res.json());
Why an HTTP proxy can carry HTTPS traffic
A frequent point of confusion is the proxy protocol versus the destination protocol. The https-proxy-agent README explains that HttpsProxyAgent is an http.Agent subclass that connects to an HTTP or HTTPS proxy and uses CONNECT to reach the destination. The proxy opens a TCP tunnel, and TLS is negotiated end to end with the target site. So http://proxy:8080 is a perfectly normal proxy URL for an https:// target. If you want the longer treatment of that distinction, see our HTTP vs HTTPS proxy decision rules.
When the proxy URL contains a username and password, the implementation adds a Proxy-Authorization: Basic header and upgrades the tunneled socket to TLS for secure destinations.
Choosing the agent from the environment
If you want node-fetch to behave like curl, which picks a proxy from variables, use proxy-agent. The proxy-agents documentation describes it as selecting an agent from HTTP_PROXY, HTTPS_PROXY and NO_PROXY, caching agents with an LRU cache, and mapping HTTP and HTTPS proxy protocols to http-proxy-agent and https-proxy-agent.
import { ProxyAgent } from 'proxy-agent';
const agent = new ProxyAgent();
const res = await fetch(url, { agent });
Note the name clash: this ProxyAgent from proxy-agent is an http.Agent, while the one from undici is a dispatcher. They are not interchangeable. If you use Axios rather than fetch, we cover its equivalent in our Axios proxy guide for Node.js.
Handling credentials safely
The Node.js guide confirms that credentials can sit in the proxy URL in the form http://username:[email protected]:8080. It also recommends against committing them and suggests a secret manager or programmatic configuration instead. In practice:
- Keep
PROXY_URLout of the repository. Use your platform's secret store, or a.envfile listed in.gitignoreat minimum. - Build the URL in code from separate user and password values so you can encode special characters.
- Never log the full URL. Log the host and port only.
- Give each teammate or job its own credentials where your provider allows it, so you can revoke one without breaking the rest.
A 407 Proxy Authentication Required response means the proxy rejected those credentials, not the target site. Our walkthrough on fixing 407 errors shows how to confirm that with curl before you suspect your Node code.
Debugging a proxied fetch
When a proxied request fails, work out which layer failed before changing anything. A short checklist:
- Reproduce outside Node. Run the same proxy with curl against a simple IP-echo endpoint. If curl fails, the problem is credentials, the proxy host or the network, not your fetch code. Our five curl proxy commands are a good starting set.
- Confirm the proxy is actually used. Fetch an IP-echo URL and compare it with your own address. If they match, the dispatcher or agent never applied, usually because of the wrong
ProxyAgentclass, an old Node.js version for the environment method, orNO_PROXY=*. - Read the error shape. A
fetch failedwith a nestedcausesuch asECONNREFUSEDorENOTFOUNDpoints at the proxy host or port. A407points at credentials. A TLS error such as a certificate failure points at interception or a mismatched TLS setting. - Inspect the tunnel. Undici exposes a diagnostics channel. Its documentation describes an
undici:proxy:connectedchannel that fires afterProxyAgentestablishes a tunnel, letting you inspect the proxy origin, target path, port, headers and TLS server name.
import diagnostics_channel from 'node:diagnostics_channel';
diagnostics_channel.subscribe('undici:proxy:connected', (msg) => {
console.log('tunnel up', msg);
});
For a broader method of narrowing down failures, our proxy error troubleshooting guide walks through finding the failing layer quickly.
What changes when the proxy is a mobile carrier IP
The code above is identical whether your proxy exits from a datacenter or from a mobile carrier network. What differs is how target sites treat the traffic. Datacenter ranges are easy for sites to classify, while traffic from real carrier IPs looks like ordinary phone users, which matters for price monitoring, ad verification and local search checks where blocks and throttling ruin the data.
Two practical points for a fetch-based scraper:
- Session behavior. Decide whether each job needs a rotating IP (every request looks like a new visitor) or a sticky one (a login or cart flow must keep the same address). Create one dispatcher per session so connection reuse does not mix identities. Our notes on session management for stable proxy workflows go deeper.
- Connection reuse. A
ProxyAgentkeeps tunnels open. If you expect a new IP, create a new agent or use the provider's rotation setting rather than assuming eachfetch()gets a fresh exit.
Our perspective
Most of the proxy tickets we see from Node.js teams are not proxy problems. They are mismatches between the client library and the proxy mechanism: a dispatcher given to node-fetch, an agent given to native fetch, or an environment variable set on a Node version that does not read it. The fix takes five minutes once you know which of the three models you are in.

Our advice is to standardize on native fetch with a per-request ProxyAgent for new scrapers. It keeps the proxy decision visible in the code, lets one process use different cities or session types side by side, and avoids a global switch that quietly reroutes internal traffic. Use environment variables for deployment-level defaults, and keep credentials in your secret store.
We built Masklabs around the same idea of explicit control: real US carrier IPs in 46 cities, rotating or sticky sessions, HTTP, HTTPS and SOCKS5 support, and per-team credentials you manage yourself in a dashboard, with no sales call needed to get a working endpoint. If you want a second opinion on what to compare in a provider, our list of proxy API features data teams should compare first is the checklist we would use ourselves.
— Jon
Try it with real US carrier IPs
If you are building a scraper, price monitor or ad-verification job in Node.js, you can test these snippets against Masklabs US mobile proxies on a free trial with limited data. Plans are billed monthly by data pool size: Starter at $30.00 per month, Basic at $100.00, Advanced at $500.00 and Scale at $1,000.00, with prepaid GB add-ons when you need more. See the pricing page for what each plan includes.
FAQ
Does native fetch in Node.js support proxies?
Yes, in two ways. You can pass an Undici ProxyAgent as the dispatcher option, per request or globally with setGlobalDispatcher. On Node.js v24.0.0+ or v22.21.0+ you can instead set the proxy environment variables and enable NODE_USE_ENV_PROXY=1.
Why does node-fetch ignore my HTTP_PROXY variable?
node-fetch does not read proxy environment variables on its own. You have to create an agent, usually HttpsProxyAgent from https-proxy-agent, and pass it as the agent option. For variable-driven selection, use the proxy-agent package, which reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY.
Do I need an HTTPS proxy to fetch HTTPS sites?
No. An HTTP proxy handles HTTPS targets with the CONNECT method, which opens a tunnel so TLS runs between your code and the destination. The scheme in your proxy URL describes how you talk to the proxy, not to the target.
How do I send credentials to the proxy?
Put them in the proxy URL as http://username:password@host:port, percent-encoding special characters, or use the token or auth options of Undici's ProxyAgent. Load them from a secret manager or environment rather than committing them to the repository.
How can I tell whether my requests are really going through the proxy?
Fetch an IP-echo endpoint and compare the address with your own. If it matches yours, check the Node.js version, the ProxyAgent class you imported and any NO_PROXY value. You can also subscribe to the undici:proxy:connected diagnostics channel to see the tunnel being established.
Sources
- ProxyAgent | Node.js Undici
- EnvHttpProxyAgent | Node.js Undici
- Diagnostics Channel Support | Node.js Undici
- Enterprise Network Configuration | Node.js Learn
- HTTPS | Node.js documentation
- node-fetch README
- Use Dev Proxy with Node.js applications | Microsoft Learn
- https-proxy-agent README
- https-proxy-agent source
- proxy-agent package
- Make it possible to use fetch with proxies or other agents (GitHub issue)