Integration4 min read

Use a proxy with Node.js fetch

Route Node.js native fetch through an Undici ProxyAgent with explicit authentication, an abort deadline and response cleanup.

On this page

The starting point

Use a dispatcher for one fetch call so the proxy is explicit and other network clients in your process keep their own configuration.

Choose a compatible runtime and dispatcher

This server-side example targets Node.js 24 and Undici 7. The agent must come from an undici major that the runtime's built-in fetch accepts: on Node 26, undici 5 and 6 agents fail with invalid onError method, and on Node 22 or 24, undici 8 agents fail with invalid onRequestStart method (how to fix both). In a separate example project, run npm install undici@7. Node's built-in fetch accepts an Undici-compatible dispatcher. This is different from browser fetch, where page JavaScript cannot select an arbitrary system proxy.

If npm install undici@7 fails behind your gateway, npm proxy settings checks registry metadata and the package archive separately. npm's installation settings do not configure this runtime fetch dispatcher.

Use a provider-documented HTTP(S) gateway in PROXY_URL, for example the deliberately nonworking https://proxy.example.invalid:8443. Inject PROXY_USERNAME and PROXY_PASSWORD through your deployment's secret manager. Do not put gateway credentials in frontend code. If the same gateway works from the curl baseline but not from Node, the difference is in the dispatcher configuration, not the proxy.

Keep proxy authentication on the dispatcher

Save as proxy-check.mjs and run node proxy-check.mjs. The Basic token belongs to the proxy agent, not to the destination's Authorization header. The request has a 30-second abort deadline and does not follow redirects automatically. A 407 at this step means the proxy rejected those credentials; fix proxy error 407 lists what to check in order.

proxy-check.mjs · Node.js 24 + Undici 7
import { ProxyAgent } from 'undici';

function required(name) {
  const value = process.env[name];
  if (!value) throw new Error('Missing environment variable: ' + name);
  return value;
}

const gateway = new URL(required('PROXY_URL'));
if (!['http:', 'https:'].includes(gateway.protocol) || gateway.username || gateway.password) {
  throw new Error('Use a credential-free HTTP(S) proxy URL');
}
const credentials = required('PROXY_USERNAME') + ':' + required('PROXY_PASSWORD');
const dispatcher = new ProxyAgent({
  uri: gateway.href,
  token: 'Basic ' + Buffer.from(credentials).toString('base64'),
});

try {
  const response = await fetch('https://example.com/', {
    dispatcher,
    signal: AbortSignal.timeout(30_000),
    redirect: 'manual',
  });
  await response.body?.cancel();
  if (!response.ok) throw new Error('Destination HTTP ' + response.status);
  console.log({ status: response.status });
} finally {
  await dispatcher.close();
}

Move from a diagnostic to a worker

This script deliberately cancels the response body because the diagnostic needs only a status. A worker that uses the result should consume the body within its budget. Always consume or cancel it before moving on, and close the dispatcher when the worker shuts down.

For repeated jobs, reuse a dispatcher with bounded concurrency rather than creating one per request. Keep each job's deadline separate. Do not install a process-wide dispatcher unless changing every affected client's routing is intentional. If you rely on HTTP_PROXY and NO_PROXY instead of an explicit dispatcher, proxy environment variables covers how they are read and where NO_PROXY changes the route.

When a request does fail, print the whole err.cause chain before changing anything: Fix Node.js TypeError: fetch failed behind a proxy decodes each cause string, from an ignored HTTPS_PROXY and undici version skew to the CONNECT status the error hides.

If you work in an MCP-compatible coding agent, Proxy Toolkit MCP can suggest causes and next checks from a sanitized error. Keep passwords, private URLs and full logs out of the prompt; the toolkit does not route your application's requests.

  • An HTTP error is a response; fetch does not reject merely because its status is 4xx or 5xx.
  • A rejected CONNECT, such as a 407 or 403, rejects with TypeError: fetch failed before any Response exists; the real reason is in err.cause, one or two levels down.
  • Capture sanitized error categories; do not log the Basic token or full credential-bearing URLs.

From reading to doing

Before you ship

  • Use the documented Node and Undici major versions.
  • Pass the dispatcher explicitly.
  • Consume or cancel response bodies and close the agent.

Sources & further reading

Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.