15% off your first purchase with code WELCOME15, up to 25 GB.

See pricing

Integration · HTTP clients

How to use a proxy with Node.js fetch

Node's built-in fetch has no proxy option. It runs on undici, and undici's ProxyAgent passed as dispatcher is the documented way to send it through an authenticated proxy: fetch(url, { dispatcher: new ProxyAgent('http://user:pass@host:port') }). Recent Node versions can also read HTTPS_PROXY when NODE_USE_ENV_PROXY=1 is set.

Reviewed 30 Oct 2026

your proxy, as a URL
http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000

The short answer

  • ProxyAgent takes the credentials from the proxy URL and sends them as Basic proxy authentication.
  • One agent per identity: an agent pools its connections, so reuse it for a sticky session and close() it when done.
  • A rejected tunnel surfaces as TypeError: fetch failed; the reason, "Proxy response (407) !== 200", sits in the error's cause chain.
  • undici's ProxyAgent also accepts socks5:// URLs.

Setup

Set up Node.js fetch with a proxy, step by step

  1. 01

    Install undici

    Importing fetch and ProxyAgent from the same package keeps them on the same undici version. undici 8 requires Node 22.19 or later.

    shell
    npm i undici
  2. 02

    Fetch through the gateway and check the exit

    The agent is the dispatcher for this request. The echo service shows the exit the target sees.

    check-exit.mjs
    import { fetch, ProxyAgent } from 'undici';const dispatcher = new ProxyAgent('http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000');const res = await fetch('https://api.ipify.org?format=json', {  dispatcher,  signal: AbortSignal.timeout(30_000),});console.log(await res.json());   // { ip: '…' }await dispatcher.close();
  3. 03

    One agent per sticky session

    Create an agent with a fresh _session- handle per task and reuse it for every request in that task. Closing it afterwards releases its pooled connections.

    sticky.mjs
    import { fetch, ProxyAgent } from 'undici';import { randomBytes } from 'node:crypto';async function withSession(task, minutes = 10) {  const handle = randomBytes(4).toString('hex');  const dispatcher = new ProxyAgent(    `http://USERNAME_pool-arcfast_country-de_session-${handle}_sesstime-${minutes}:PASSWORD@proxy.arcproxies.io:8000`,  );  try {    return await task((url) => fetch(url, { dispatcher, signal: AbortSignal.timeout(30_000) }));  } finally {    await dispatcher.close();  }}const statuses = await withSession(async (get) => {  const out = [];  for (let page = 1; page <= 3; page++) out.push((await get(`https://example.com/search?page=${page}`)).status);  return out;});console.log(statuses);
  4. 04

    Geo-target with a different agent

    The country and city are part of the username, so each market is its own agent. Cities come from the published list, lowercased without spaces.

    geo.mjs
    import { fetch, ProxyAgent } from 'undici';const agents = {  'fr-lyon':    new ProxyAgent('http://USERNAME_pool-arcfast_country-fr_city-lyon:PASSWORD@proxy.arcproxies.io:8000'),  'ca-toronto': new ProxyAgent('http://USERNAME_pool-arcfast_country-ca_city-toronto:PASSWORD@proxy.arcproxies.io:8000'),  'de':         new ProxyAgent('http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000'),};for (const [market, dispatcher] of Object.entries(agents)) {  const res = await fetch('https://example.com/pricing', { dispatcher });  console.log(market, res.status);}await Promise.all(Object.values(agents).map((a) => a.close()));
  5. 05

    Read the cause chain on failures

    fetch rejects with a generic TypeError: fetch failed. Walk err.cause to find the proxy's answer: a 407 means stop, other failures and timeouts are worth a retry on a new handle.

    fetch.mjs
    import { fetch, ProxyAgent } from 'undici';import { randomBytes } from 'node:crypto';import { setTimeout as sleep } from 'node:timers/promises';const causes = (err) => { const out = []; for (let e = err; e; e = e.cause) out.push(String(e.message)); return out.join(' | '); };async function fetchWithRetry(url, attempts = 4) {  for (let attempt = 0; attempt < attempts; attempt++) {    const dispatcher = new ProxyAgent(      `http://USERNAME_pool-arcfast_country-de_session-${randomBytes(4).toString('hex')}_sesstime-5:PASSWORD@proxy.arcproxies.io:8000`,    );    try {      const res = await fetch(url, { dispatcher, signal: AbortSignal.timeout(30_000) });      if (res.status !== 429 && res.status < 500) return await res.text();    } catch (err) {      const why = causes(err);      if (why.includes('(407)')) throw new Error('proxy rejected the credentials');      console.warn(`attempt ${attempt + 1}: ${why}`);    } finally {      await dispatcher.close();    }    await sleep(1000 * 2 ** attempt);  }  throw new Error(`gave up on ${url}`);}
  6. 06

    Or: proxy from the environment, no code changes

    On Node 24 and 22.21 or later, NODE_USE_ENV_PROXY=1 makes Node read HTTP_PROXY, HTTPS_PROXY and NO_PROXY at startup. It is marked as active development, and it gives you one proxy for the whole process, so per-task sessions still need ProxyAgent.

    shell
    NODE_USE_ENV_PROXY=1 \HTTPS_PROXY="http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000" \node app.mjs

How undici's ProxyAgent talks to the gateway

  • HTTPS targets go through a CONNECT tunnel; TLS runs between Node and the site.
  • Credentials come from the URL's userinfo, URL-decoded, unless you pass token (a ready-made Proxy-Authorization value). The older auth option is deprecated in favour of token.
  • SOCKS5: a socks5:// URI makes ProxyAgent delegate to undici's Socks5ProxyAgent, against the same gateway port with the same credentials.
  • Plain HTTP targets are forwarded in absolute form by default; proxyTunnel: true tunnels them too.

Rotating vs sticky with fetch

A rotating username lets each new connection take a new exit, but an agent keeps connections alive, so requests through one agent to one host can share an IP. That is why the examples tie one agent to one _session- handle: the agent's connection pool and the exit IP then mean the same thing. Rotating vs sticky sessions explains when to use which.

Using the node-fetch package instead

The node-fetch npm package predates built-in fetch and does not use undici. It takes an agent option, so the proxy comes from https-proxy-agent: fetch(url, { agent: new HttpsProxyAgent('http://user:pass@host:port') }). On current Node, the built-in fetch with ProxyAgent needs one dependency fewer. Axios is the other common choice.

The problem

Common pitfalls

Passing an agent instead of a dispatcher

Built-in fetch ignores agent. It takes dispatcher, and only undici dispatchers.

Creating an agent per request in a hot loop

Each agent opens its own connections. Reuse an agent for a session; create a new one only when you want a new identity, and close() the old one.

Logging err.message only

"fetch failed" says nothing. The proxy's status and undici's error code live in err.cause.

No timeout

Pass signal: AbortSignal.timeout(ms); otherwise a stalled exit holds the request open for undici's much longer defaults.

FAQ

Questions

Something not covered? Write to contact@arcproxies.io.

How do I use a proxy with fetch in Node.js?

Install undici and pass a ProxyAgent as the dispatcher: fetch(url, { dispatcher: new ProxyAgent('http://user:pass@host:port') }). Node's built-in fetch has no proxy option of its own.

Does Node.js fetch respect HTTPS_PROXY?

Only when enabled: set NODE_USE_ENV_PROXY=1 or start Node with --use-env-proxy (Node 24 and 22.21 or later). The feature is marked as active development in the Node docs.

Why does fetch say fetch failed when the proxy rejects me?

Built-in fetch wraps network errors in TypeError: fetch failed. The underlying reason, for example "Proxy response (407) !== 200 when HTTP Tunneling", is in err.cause.

Can undici use a SOCKS5 proxy?

Yes. new ProxyAgent('socks5://user:pass@host:port') delegates to undici's Socks5ProxyAgent. It is documented in undici's ProxyAgent reference.

Sources & review

Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.

  1. [1]undici: ProxyAgent
  2. [2]Node.js: HTTP: Built-in proxy support
  3. [3]Node.js: CLI: NODE_USE_ENV_PROXY=1

Integrations

Other http clients

Python requestsUse an authenticated proxy with Python requests: the proxies argument, sticky sessions per task, geo-targeting, SOCKS5 and retries that skip 407 errors.HTTPXConfigure an authenticated proxy in HTTPX 0.28: the proxy= argument that replaced proxies=, mounts per domain, async clients, SOCKS5 and error handling.aiohttpUse an authenticated proxy with aiohttp: proxy= per request or per session, why proxy_auth is deprecated, SOCKS5 through aiohttp-socks, and 407 handling.AxiosUse an authenticated proxy with Axios in Node.js: the proxy option on 1.16.1+, https-proxy-agent for older versions, sticky sessions, SOCKS5 and 407s.curlSend curl through an authenticated proxy with -x and -U, check the exit IP, test sticky sessions and countries, use socks5h, and read 407 failures.GoRoute Go's net/http through an authenticated proxy: Transport.Proxy with url.UserPassword, a sticky session per request context, SOCKS5 and 407 checks.RubyUse an authenticated proxy in Ruby: Net::HTTP proxy arguments, Faraday proxy options, sticky sessions per task, geo-targeting and 407 handling.PHP GuzzleRoute PHP Guzzle and plain cURL through an authenticated proxy: the proxy option, sticky sessions, geo-targeting, retries that stop on a 407.Java HttpClientUse an authenticated proxy with java.net.http.HttpClient: ProxySelector, Authenticator, the Basic-over-CONNECT setting, sticky sessions and 407s.C# HttpClientUse an authenticated proxy in .NET: WebProxy and NetworkCredential on SocketsHttpHandler, a client per sticky session, SOCKS5 and 407 handling.Rust reqwestRoute Rust reqwest through an authenticated proxy: Proxy::all with basic_auth, a Client per sticky session, socks5h, timeouts and 407 errors.wgetRun GNU Wget through an authenticated proxy: https_proxy, --proxy-user, a .wgetrc for credentials, sticky sessions for mirrors and exit codes.PostmanSend Postman requests through an authenticated proxy: the custom proxy settings, Proxy auth, checking the exit IP, sessions, countries and Newman.DockerGive Docker containers and builds an authenticated proxy: HTTP_PROXY, HTTPS_PROXY and NO_PROXY, build args, Compose, and why the daemon proxy differs.

Get started

Get credentials and paste them in.

Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.