Node.js proxy guide: fetch, undici, axios and SOCKS5 with authentication
Node's fetch ignores proxies unless you ask. How to route fetch, undici, axios and the http module through an authenticated proxy, with NODE_USE_ENV_PROXY, SOCKS5, special characters, rotation, sticky sessions and the error behind each mistake, tested on Node 26.

The quickest way to send a Node.js request through an authenticated proxy is undici's ProxyAgent, passed to fetch as the dispatcher:
import { fetch, ProxyAgent } from "undici";
const proxy = new ProxyAgent("http://USERNAME:[email protected]:8080");
const res = await fetch("https://api.ipify.org?format=json", { dispatcher: proxy });
console.log(await res.json());Install it with npm install undici. The rest of this guide covers the other ways Node can use a proxy, and the details that bite: why plain fetch ignores your proxy, the built-in environment variable support and its quirks, axios, SOCKS5, special characters in passwords, rotation, sticky sessions and the errors you get when something is wrong. Everything was tested on Node.js 26.1 with undici 8.12, axios 1.20, https-proxy-agent 9.1 and socks-proxy-agent 10.1.
Plain fetch ignores your proxy
Node's built-in fetch does not read proxy settings unless you tell it to. With HTTPS_PROXY set in the environment and nothing else, our request went straight to the website from the machine's own IP, with no error.
Passing a proxy option does not help either:
const res = await fetch("https://api.ipify.org", { proxy: "http://USERNAME:[email protected]:8080" });That option belongs to Bun, where it works. Node ignores unknown options, so the request again goes out directly and silently. Whatever method you choose below, check the IP a test site reports before you trust it.
undici ProxyAgent, per request or global
undici is the HTTP client that powers Node's own fetch, published as a separate package with extra features, including ProxyAgent. You can pass it per request, as in the first example, or install it as the global dispatcher so that every fetch in the process uses it, the built-in one included:
import { ProxyAgent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new ProxyAgent("http://USERNAME:[email protected]:8080"));
const res = await fetch("https://api.ipify.org?format=json");
console.log(await res.json());The proxy URL starts with http:// even though the website is HTTPS. For HTTPS targets, ProxyAgent asks the proxy to open a tunnel with CONNECT and runs TLS through it, end to end between you and the site.
Built-in proxy support from environment variables
Recent Node versions can read the standard proxy variables themselves, but only when you opt in, with the NODE_USE_ENV_PROXY=1 environment variable or the --use-env-proxy flag:
export HTTPS_PROXY="http://USERNAME:[email protected]:8080"
NODE_USE_ENV_PROXY=1 node app.mjsNode's documentation lists NODE_USE_ENV_PROXY from v24.0.0 and v22.21.0, and the flag, along with support in http.request and https.request, from v24.5.0 and v22.21.0. In our test both switches worked for fetch and for https.get.
Two behaviours surprised us, and both come from fetch and the http module following different rules:
- Only HTTP_PROXY set:
fetchused it for an HTTPS site;https.getwent direct. - NO_PROXY=ipify.org:
fetchskipped the proxy forapi.ipify.org;https.getstill used it.
Neither skips the proxy for local addresses on its own. A request to 127.0.0.1 went through the proxy until 127.0.0.1 itself was in NO_PROXY; localhost in the list did not cover it, and neither did 127.0.0.0/8 for fetch. The proxy environment variables guide compares these rules across curl, Python, Node and Go.
undici's EnvHttpProxyAgent reads the same variables without any flag, as a dispatcher you pass per request or set globally.
Passwords with special characters
The username and password go into the proxy URL, so characters such as @, :, / and # break it. With a password of p@ss:w/rd#1 pasted raw, new ProxyAgent threw TypeError: Invalid URL. Encode the credentials, and keep them out of the code while you are at it:
import { fetch, ProxyAgent } from "undici";
const user = encodeURIComponent(process.env.PROXY_USER);
const pass = encodeURIComponent(process.env.PROXY_PASS);
const proxy = new ProxyAgent(`http://${user}:${pass}@geo.gw.nyxon.sh:8080`);
const res = await fetch("https://api.ipify.org?format=json", { dispatcher: proxy });
console.log(await res.json());undici decodes them again before building the Proxy-Authorization header.
axios
axios has its own proxy option, and it reads HTTPS_PROXY from the environment by default, with no flag:
import axios from "axios";
const res = await axios.get("https://api.ipify.org?format=json", {
proxy: {
protocol: "http",
host: "geo.gw.nyxon.sh",
port: 8080,
auth: { username: "USERNAME", password: "PASSWORD" },
},
});
console.log(res.data);The auth object takes the password as is, special characters included. Set proxy: false to make axios ignore the environment variables.
Older advice says the axios proxy option mishandles HTTPS sites and that you need an agent package instead. That is out of date if you keep axios current: with axios 1.20, an HTTPS request through this config opened a CONNECT tunnel like any other client. The axios changelog for 1.16.1 lists the fix: "Fixed an issue where HTTPS request data could be transmitted in cleartext to an HTTP proxy under certain configurations." If you are on an older version, upgrade.
If you would rather use an agent, for example to share it with other libraries, https-proxy-agent still works. Pass it as httpsAgent and turn the built-in proxy off:
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://USERNAME:[email protected]:8080");
const res = await axios.get("https://api.ipify.org?format=json", { httpsAgent: agent, proxy: false });
console.log(res.data);SOCKS5
undici has a Socks5ProxyAgent, marked experimental, so Node prints an ExperimentalWarning the first time you create one:
import { fetch, Socks5ProxyAgent } from "undici";
const socks = new Socks5ProxyAgent("socks5://USERNAME:[email protected]:1080");
const res = await fetch("https://api.ipify.org?format=json", { dispatcher: socks });
console.log(await res.json());It always sends the hostname to the proxy, so DNS is resolved on the proxy side, and it rejects socks5h:// URLs with InvalidArgumentError: Proxy URL must use socks5:// or socks:// protocol.
For axios and the http module, use socks-proxy-agent, and there the scheme matters. With socks5h://, the proxy resolved the hostname; with socks5://, Node looked the name up locally and sent only the IP, which leaks your DNS lookups to your own resolver:
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://USERNAME:[email protected]:1080");
const res = await axios.get("https://api.ipify.org?format=json", {
httpAgent: agent,
httpsAgent: agent,
});
console.log(res.data);The SOCKS5 vs HTTP guide explains when SOCKS5 is worth it over the HTTP port.
Rotation: one agent, one tunnel
On a rotating username, every new connection to the proxy gets a new IP. But an agent keeps its connections open and reuses them. In our test, five requests to the same site through one ProxyAgent used a single tunnel, and so did five requests sent in parallel with Promise.all. All of them would leave from the same IP.
To get a new IP per request, give each request its own agent and close it afterwards:
import { fetch, ProxyAgent } from "undici";
async function viaNewIp(url) {
const agent = new ProxyAgent("http://USERNAME:[email protected]:8080");
try {
const res = await fetch(url, { dispatcher: agent });
return await res.text();
} finally {
await agent.close();
}
}
for (let i = 0; i < 3; i++) console.log(await viaNewIp("https://api.ipify.org"));With that function, five requests opened five tunnels.
Sticky sessions and countries
NyxonProxies reads targeting and session options from the username. To keep one IP for a while, add a session ID and a duration in minutes (1 to 120); to pick a country, add -country- and its code:
import { randomBytes } from "node:crypto";
import { fetch, ProxyAgent } from "undici";
function stickyAgent(country, minutes = 10) {
const sid = randomBytes(4).toString("hex");
const user = `USERNAME-country-${country}-session-${sid}-time-${minutes}`;
return new ProxyAgent(`http://${user}:[email protected]:8080`);
}
const agent = stickyAgent("us");
for (let i = 0; i < 3; i++) {
const res = await fetch("https://api.ipify.org", { dispatcher: agent });
console.log(await res.text());
}
await agent.close();Residential sessions last up to 120 minutes. The Generate page builds the username for you, and the sessions guide explains each option.
Errors and how to read them
fetch reports every network failure with the same message, TypeError: fetch failed. The real reason is in err.cause, so log both:
try {
await fetch("https://api.ipify.org", { dispatcher: proxy });
} catch (err) {
console.error(err.message, err.cause);
}What we saw for each mistake:
- Wrong username or password: the cause is a DOMException, "Request was cancelled." The 407 itself is not shown.
- Wrong host or port: the cause has the code
ECONNREFUSED. - https:// in front of the HTTP port: the cause has the code
ERR_SSL_WRONG_VERSION_NUMBER, "wrong version number": the client starts TLS on a port that speaks plain HTTP. - Special characters not encoded:
TypeError: Invalid URL, thrown by theProxyAgentconstructor. - axios with a wrong password: "Request failed with status code 407", with
error.response.statusset to 407.
Because a wrong password looks like a cancelled request in fetch, test new credentials with curl or axios first, where the 407 is visible. The proxy error codes guide covers what each status means.
FAQ
Does Node's fetch support proxies? Not by default. Pass an undici ProxyAgent as the dispatcher, set one globally, or start Node with NODE_USE_ENV_PROXY=1 and HTTPS_PROXY set.
How do I use a SOCKS5 proxy with fetch? With undici's Socks5ProxyAgent, which is still experimental, or by switching to axios with socks-proxy-agent and a socks5h:// URL.
Why do all my requests come from the same IP? The agent reuses its tunnel to the site. Use a new agent per request for a new IP each time.
Does axios work with HTTPS through an HTTP proxy? Yes. Current axios opens a CONNECT tunnel for HTTPS sites; the fix for older versions is listed under 1.16.1.
Try it
Create a NyxonProxies account, link Discord under Freebies for 250MB of free Residential Budget traffic, and copy your proxy username and password from the Generate page into any snippet above. Rates for every proxy type are on the pricing page. Using Python instead? See the Python Requests guide.