Geonode logo
Library and script errors

How to Fix node-fetch Proxy Not Working

Three fixes for node-fetch 2 and 3, most common first, each with a short Node.js example.

Updated

TL;DR

node-fetch has no proxy setting of its own, so a call made without an agent normally leaves from your own IP and nothing warns you. Create an HttpsProxyAgent from https-proxy-agent and pass it as the agent option of each fetch call.

What node-fetch proxy problems look like

A skipped proxy throws nothing; these are the messages around it. Paths and addresses vary.

WhereWhat you see
Node.js 20.18 and older or 22.0 to 22.11, require('node-fetch') with node-fetch 3Error [ERR_REQUIRE_ESM]: require() of ES Module .../node_modules/node-fetch/src/index.js from .../app.js not supported.
Node.js 20.19+ and 22.12+, const fetch = require('node-fetch') with node-fetch 3TypeError: fetch is not a function
https-proxy-agent 7 loaded the version 5 wayTypeError: HttpsProxyAgent is not a constructor
node-fetch with an agent, nothing listening on the proxy portFetchError: request to https://example.com/ failed, reason: connect ECONNREFUSED 127.0.0.1:8080
node-fetch with https-proxy-agent, proxy refuses the login (res.status and res.statusText, nothing thrown)407 Proxy Authentication Required

Why this happens

node-fetch hands the connection to Node.js, which dials the site directly.

Under the hood each call uses Node.js's built-in client, and node-fetch passes it one connection setting, the agent. Leave that out and the global default normally connects from your machine, whatever proxy your browser or shell uses.

Diagnose your node-fetch proxy first

The first check needs only an editor; the rest show whether requests reach the proxy at all.

  • Open package.json; a node-fetch entry starting with ^3 is an ESM-only release, so CommonJS require() calls break.

  • Search the code for each fetch( call; an options object lacking agent sends that request around the proxy.

  • Note your IP on What is my IP, then request an IP echo URL from the script on that machine; a match means no proxy.

  • Print res.status and res.statusText; 407 with nothing thrown means the agent reached the proxy but the login was refused.

  • Read the address after ECONNREFUSED in the FetchError; your proxy's IP and port there mean the agent was used but nothing answered at that address.

Solutions ranked by effectiveness

Most scripts need only the first fix; the other two cover node-fetch 3 and proxies set in the environment.

  1. Most common fix

    Pass an HttpsProxyAgent as the agent

    Applies to version 2 in CommonJS projects. Build the agent once from your proxy URL and reuse it; import the class by name, since release 7 has no default export.

    // npm install node-fetch@2 https-proxy-agent@7
    const fetch = require("node-fetch");
    const { HttpsProxyAgent } = require("https-proxy-agent");
    
    const proxy = "http://USERNAME:PASSWORD@proxy.geonode.io:9000";
    const agent = new HttpsProxyAgent(proxy);
    
    fetch("https://example.com/", { agent })
      .then((res) => console.log("status", res.status))
      .catch((err) => console.log(err.name, err.message));
  2. Check next

    Load node-fetch 3 with import()

    Applies when loading throws ERR_REQUIRE_ESM or says fetch is not a function. Release 3 ships only as an ES module, so CommonJS reaches its default export through dynamic import(); downgrading to @2 also works.

    // npm install node-fetch@3 https-proxy-agent@7
    const { HttpsProxyAgent } = require("https-proxy-agent");
    
    const proxy = "http://USERNAME:PASSWORD@proxy.geonode.io:9000";
    const agent = new HttpsProxyAgent(proxy);
    
    import("node-fetch")
      .then(({ default: fetch }) => fetch("https://example.com/", { agent }))
      .then((res) => console.log("status", res.status))
      .catch((err) => console.log(err.name, err.message));
  3. For developers

    Let proxy-agent read HTTPS_PROXY and NO_PROXY

    Applies when the proxy URL should come from environment variables. proxy-agent picks an agent per URL from them; start the script with HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.geonode.io:9000.

    // npm install node-fetch@2 proxy-agent@6
    const fetch = require("node-fetch");
    const { ProxyAgent } = require("proxy-agent");
    
    const agent = new ProxyAgent();
    
    fetch("https://example.com/", { agent })
      .then((res) => console.log("status", res.status))
      .catch((err) => console.log(err.name, err.message));

Stop the node-fetch proxy problem from coming back

Habits that keep every call on the proxy through package updates, new servers and password changes.

  1. Pin node-fetch to ^2 and https-proxy-agent to ^7 in package.json; their next majors load only as ES modules.

  2. Wrap USERNAME and PASSWORD in encodeURIComponent when building the proxy URL; the agent decodes both before sending them.

  3. With proxy-agent, set HTTPS_PROXY too: https:// URLs never read HTTP_PROXY, and with no match they connect directly.

  4. Let retry logic treat a 502 response as possibly the proxy's, since https-proxy-agent passes CONNECT failures through as responses.

A new IP for every fetchBy default, every fetch opens a tunnel; Geonode's rotating ports give each a new IP.
Try residential proxies

Related errors

Learn more

FAQ

Yes, but through a helper package rather than a setting. An http.Agent subclass, for example HttpsProxyAgent, carries each call through your proxy server once you pass it in the options.

Not by itself. Calls without an agent use Node.js's default agent, which follows HTTP_PROXY and HTTPS_PROXY only on Node.js 24.5+ or 22.21+ started with NODE_USE_ENV_PROXY=1; Node.js 20 ignores them.

For node-fetch, https-proxy-agent alone is enough: it opens a CONNECT tunnel for https:// and http:// URLs. http-proxy-agent sends plain HTTP requests to the proxy and fits http:// URLs only.

You can on Node.js 18 or later, but the proxy moves from agent to the dispatcher option that built-in fetch reads. Also check code that pipes res.body: node-fetch returns a Node.js Readable stream, not a web ReadableStream.

Yes, through socks-proxy-agent@8, its last CommonJS major: pass new SocksProxyAgent('socks5h://USERNAME:PASSWORD@host:port') as the agent. With socks5h:// the proxy resolves host names; socks5:// looks them up on your machine first.

Yes, and create it only once. Passing keepAlive: true also lets it reuse open tunnels, so later requests to the same site ride an existing tunnel and keep its exit IP.

One sticky IP per agent

On sticky ports, a session ID in each agent's username keeps its IP until expiry.