Guide

How to Use Proxies With Node fetch (undici ProxyAgent)

Native fetch in Node has no proxy option. How to route it through a proxy with undici's ProxyAgent and setGlobalDispatcher, per-request dispatchers, auth, SOCKS, and verifying the exit.

HProxy Team··4 min read
HProxy.Guide

Skip the dead lists.

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump.

Open the free proxy list

Node 18 added a global fetch and quietly took away the easy way to proxy it. The fetch you now get for free comes from undici, it has no proxy option, and unlike most tools it does not read HTTP_PROXY from the environment, so the first proxied request goes out on your real IP and nothing warns you. This guide covers proxies with native Node fetch end to end: undici's ProxyAgent, setting it globally versus per request, authentication, why SOCKS needs a different dispatcher, rotating a pool, and verifying the exit IP.

We run a proxy network and a live proxy checker, so the Node fetch proxy question we field most is simply "where does the proxy go", because there is no option for it on the call. The answer is a dispatcher, shown below. Every example runs on Node 18 or newer with undici installed (npm install undici). Where one needs a live proxy, pull a fresh one from our free proxy API, which returns recently checked endpoints with no key.

Why native fetch ignores your proxy

The fetch global is undici under the hood, and undici routes requests through an object it calls a Dispatcher. The default dispatcher opens direct connections and knows nothing about proxies or environment variables. There is no fetch(url, { proxy }), so setting one there does nothing. To proxy fetch, you replace or override that dispatcher with a ProxyAgent.

How to make native fetch use a proxy
  1. fetch(url)

    no proxy option exists

  2. Default dispatcher

    connects direct, ignores env

  3. ProxyAgent

    from undici

  4. setGlobalDispatcher or dispatcher:

    now fetch proxies

Source: Native fetch proxies through an undici dispatcher, not an option

Set it globally

The simplest approach: build a ProxyAgent and install it as the global dispatcher. Every fetch after that uses it.

import { ProxyAgent, setGlobalDispatcher } from "undici";

setGlobalDispatcher(new ProxyAgent("http://203.0.113.7:8080"));

const res = await fetch("https://httpbin.org/ip");
console.log(await res.json());

This is right when the whole process should go through one proxy. It is a poor fit for rotation, because it changes the exit for everything at once.

Set it per request

Native fetch in Node accepts a dispatcher option (an undici extension), so you can proxy a single call without touching the global default:

import { ProxyAgent } from "undici";

const agent = new ProxyAgent("http://203.0.113.7:8080");

const res = await fetch("https://example.com", { dispatcher: agent });

The per-call dispatcher is the primitive rotation is built on: different agent, different exit, no effect on other requests.

Authentication

Put the credentials in the proxy URL, user:pass@host:port:

const agent = new ProxyAgent("http://user:pass@203.0.113.7:8080");

Or pass a token, which undici sends as the Proxy-Authorization header, handy when the password is awkward in a URL:

const token = "Basic " + Buffer.from("myuser:mypass").toString("base64");
const agent = new ProxyAgent({ uri: "http://203.0.113.7:8080", token });

A wrong or missing login returns HTTP 407 Proxy Authentication Required, which confirms the proxy is alive and only the credentials are off.

SOCKS needs a different dispatcher

ProxyAgent handles HTTP and HTTPS proxies, tunnelling HTTPS through CONNECT. It does not speak SOCKS. For a SOCKS proxy you need a SOCKS-capable dispatcher, built with the socks package and a custom undici connector, or you step across to a client like axios with socks-proxy-agent, which we cover in the axios guide. For most scraping an HTTP proxy is all you need and avoids the extra plumbing.

Rotating a pool

Because the dispatcher is per request, rotation is choosing which ProxyAgent to attach and retrying on another when one fails:

import { ProxyAgent } from "undici";

const POOL = [
  "http://user:pass@203.0.113.7:8080",
  "http://user:pass@203.0.113.24:3128",
  "http://user:pass@198.51.100.14:8080",
];
const agents = POOL.map((p) => new ProxyAgent(p));

async function getRotating(url, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const agent = agents[Math.floor(Math.random() * agents.length)];
    try {
      const res = await fetch(url, {
        dispatcher: agent,
        signal: AbortSignal.timeout(15000),   // a dead exit fails in 15s
      });
      if (res.ok) return await res.text();
    } catch {
      // dead or blocked exit, try the next
    }
  }
  throw new Error(`all ${tries} proxies failed for ${url}`);
}

Reusing the ProxyAgent objects keeps undici's connection pool per exit, and AbortSignal.timeout stops a silent proxy from hanging the call. Fill POOL from our free proxy API, which returns a plain-text list you can split at startup.

Verify the exit IP

Never assume the dispatcher took effect. Read an IP echo through the proxied call and compare it to your own address:

const real = await (await fetch("https://httpbin.org/ip")).json();
const via = await (await fetch("https://httpbin.org/ip", {
  dispatcher: new ProxyAgent("http://203.0.113.7:8080"),
})).json();

console.log("real:", real.origin, "proxy:", via.origin);
if (real.origin === via.origin) throw new Error("proxy is not changing your IP");

If the two match, no dispatcher was attached to that call, or setGlobalDispatcher was never run. For a fuller read of the exit, our proxy checker reports the exit IP, country, latency and anonymity grade in one paste.

Where to go from here

Native fetch is a fine client once you know the proxy lives on a dispatcher, not an option, and that nothing reads your environment for you. After that the discipline is the same as any proxied client: verify each exit, bound your timeouts, and rotate a pool that is alive.

The Node.js proxy guide covers the older http/https module paths, the axios guide is the route to SOCKS and a more feature-rich client, and proxies for web scraping covers choosing the right proxy type. When a hand-rolled pool of dispatchers becomes a chore, a rotating gateway that returns a fresh residential IP per request at $0.44/GB lets one dispatcher keep a single endpoint while the exit changes underneath it.

Frequently asked questions

Does Node's built-in fetch support a proxy?
Not directly. The global fetch added in Node 18 comes from undici and has no proxy option, and it does not read HTTP_PROXY from the environment either. To proxy it you attach an undici Dispatcher: create a ProxyAgent and set it globally with setGlobalDispatcher, or pass it per call as the dispatcher option, which native fetch accepts in Node.
How do I set a proxy for fetch in Node?
Install undici, then either set it globally, import { ProxyAgent, setGlobalDispatcher } from 'undici'; setGlobalDispatcher(new ProxyAgent('http://user:pass@host:port')), after which every fetch uses it, or pass it on one call, fetch(url, { dispatcher: new ProxyAgent(...) }). The per-call dispatcher is how you vary the exit without affecting other requests.
How do I authenticate a proxy with undici ProxyAgent?
Put the credentials in the proxy URL as user:pass@host:port, or pass a token, new ProxyAgent({ uri: 'http://host:port', token: 'Basic ' + Buffer.from('user:pass').toString('base64') }). The token becomes the Proxy-Authorization header. A wrong login returns HTTP 407 Proxy Authentication Required, which confirms the proxy is reachable.
Does undici ProxyAgent support SOCKS5?
No. undici's ProxyAgent is for HTTP and HTTPS proxies (it uses a CONNECT tunnel for HTTPS targets). For SOCKS you need a SOCKS-capable dispatcher, built with the socks package and a custom undici connector, or you fall back to a client like axios with socks-proxy-agent. For most jobs an HTTP proxy avoids the question.
How do I rotate proxies with Node fetch?
Create one ProxyAgent per proxy and pass a different one as the dispatcher on each request, retrying on another when a call fails. Because the dispatcher is per call, rotation is just choosing which agent to attach, and on any pool some exits are always down so the retry-on-a-different-exit loop is not optional.

Get proxies that are alive right now

Our free list re-checks every exit every few minutes and shows a last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup