Dispatcher boundary runbook

Node.js Fetch Proxy Setup with Built-In Env Support and Undici

Configure Node.js fetch through an HTTP proxy with Node 24's built-in environment support or a per-request Undici ProxyAgent, then diagnose CONNECT, 407, NO_PROXY, timeout, and dispatcher-version failures from captured evidence.

Node.js fetch

HTTP client library

T-08
T-08 / Dispatcher boundaryChoose one compatible proxy control surfaceserver-rendered route map

fetch client

Node built-in | Undici package

Process-wide

--use-env-proxy

Per request

fetch + ProxyAgent, same package

Proxy evidence

CONNECT | absolute-form | 407

Keep process-wide environment routing and request-scoped Undici dispatchers separate, versioned, and observable at the proxy boundary.

By Robert SmithReviewed by James Johnson13 min runbook

Operating principle

Keep process-wide environment routing and request-scoped Undici dispatchers separate, versioned, and observable at the proxy boundary.

Choose the Node Proxy Control Surface First

There are now two sound ways to proxy fetch() in current Node.js, and they solve different deployment problems. Node 24 has built-in process-wide support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY when you explicitly enable it. The separately installed Undici package provides ProxyAgent when one request or one client needs its own dispatcher.

RequirementUseVersion boundary
One proxy policy for the whole processnode --use-env-proxy app.mjs or NODE_USE_ENV_PROXY=1Built-in proxy support arrived in Node 24.5.
Enable or replace the global policy in codehttp.setGlobalProxyFromEnv()Added in Node 24.14. Call it before requests begin.
Proxy only a selected request or clientImport both fetch and ProxyAgent from the same installed Undici packageThis guide tested Undici 8.9.0 with Node 24.14.0.

Environment variables alone did not affect built-in fetch in our isolated Node 24.14.0 run. The same request used the proxy only after the startup flag or the runtime API enabled the feature. That opt-in is the first thing to verify when a Node process appears to ignore a valid gateway.

Start with Node 24 Built-In Environment Proxy Support

For a worker whose outbound HTTP policy is process-wide, start Node with the proxy feature enabled and inject credentials through the deployment platform's secret mechanism. Do not place a live password in a repository, container definition, screenshot, or shell transcript.

# POSIX shell example; values should come from a secret manager
HTTP_PROXY='http://USER:PASSWORD@gw.databay.co:8888' \
HTTPS_PROXY='http://USER:PASSWORD@gw.databay.co:8888' \
NO_PROXY='localhost,127.0.0.1,.internal.example' \
node --use-env-proxy app.mjs
// app.mjs
const response = await fetch('https://api.ipify.org?format=json', {
  signal: AbortSignal.timeout(15_000),
});

if (!response.ok) {
  throw new Error(`Destination returned ${response.status}`);
}

console.log(await response.json());

HTTP_PROXY selects the proxy for an HTTP destination; HTTPS_PROXY selects it for an HTTPS destination. The scheme inside either value describes the client-to-proxy connection. An http:// gateway can carry an https:// destination through CONNECT, so the two schemes do not have to match.

What the Local Capture Actually Observed

We ran a credential-redacted localhost capture on 2026-07-27 with Node 24.14.0, its bundled Undici 7.21.0, and installed Undici 8.9.0. Reserved example.test was used for the failed HTTPS case, and no public proxy or destination received the lab credentials. The sanitized observation ledger preserves the result in machine-readable form.

Client pathFirst proxy messageMeasured result
Built-in fetch, environment onlyNo proxy connectionThe local destination was reached directly; environment variables were ignored without the Node opt-in.
Built-in fetch plus --use-env-proxyCONNECT 127.0.0.1:PORTNode tunneled even the HTTP destination through the local proxy.
http.setGlobalProxyFromEnv()CONNECT 127.0.0.1:PORTThe runtime API changed the global dispatcher and the request completed through the tunnel.
Undici 8.9 fetch plus ProxyAgentGET http://127.0.0.1:PORT/pathThe HTTP destination used absolute-form rather than CONNECT.
Built-in HTTPS destinationCONNECT example.test:443The lab stopped before destination TLS and fetch rejected.
Stalled proxy handshakeTCP connection, no HTTP responseAbortSignal.timeout(150) rejected with TimeoutError.

Do not turn this table into a universal performance claim. It identifies control flow for the exact versions above. Re-run the harness after changing Node or Undici because proxy support is still marked active development in the Node 24 documentation.

Enable the Global Proxy at Runtime When Startup Flags Are Impractical

Node 24.14 added http.setGlobalProxyFromEnv(). It accepts the environment by default or a scoped object, returns a restore function, and changes the global HTTP, HTTPS, and Undici dispatcher configuration. Invoke it during process initialization, not while concurrent requests are in flight.

import http from 'node:http';

const restoreProxy = http.setGlobalProxyFromEnv({
  http_proxy: process.env.DATABAY_HTTP_PROXY,
  https_proxy: process.env.DATABAY_HTTPS_PROXY,
  no_proxy: 'localhost,127.0.0.1,.internal.example',
});

try {
  const response = await fetch('https://service.example/health', {
    signal: AbortSignal.timeout(10_000),
  });
  console.log(response.status);
} finally {
  restoreProxy();
}

The restore function is useful in a short-lived diagnostic or test. A long-running service should normally configure the policy once at startup. Calling the API later replaces previously configured global agents and dispatchers, which can alter unrelated libraries that share those globals.

Use One Undici Version for a Per-Request Proxy

For request-scoped routing, install Undici and import the client and dispatcher from that same package. Pairing the two avoids a subtle version boundary between Node's bundled implementation and the independently released package.

import { fetch, ProxyAgent } from 'undici';

const proxyUrl = new URL('http://gw.databay.co:8888');
proxyUrl.username = process.env.DATABAY_PROXY_USER;
proxyUrl.password = process.env.DATABAY_PROXY_PASS;

const dispatcher = new ProxyAgent(proxyUrl);
try {
  const response = await fetch('https://api.ipify.org?format=json', {
    dispatcher,
    signal: AbortSignal.timeout(15_000),
  });
  if (!response.ok) throw new Error(`Destination returned ${response.status}`);
  console.log(await response.json());
} finally {
  await dispatcher.close();
}

Our successful per-request case imported both fetch and ProxyAgent from Undici 8.9.0. In a separate negative control, passing an Undici 8.9.0 ProxyAgent to Node 24.14.0's built-in fetch, which reports bundled Undici 7.21.0, failed before any proxy connection with UND_ERR_INVALID_ARG and invalid onRequestStart method. Node documents that a custom dispatcher must be compatible; do not assume two different Undici majors are interchangeable. Record both process.version and process.versions.undici when debugging.

Encode Credentials Without Logging the Proxy URL

A proxy URL is structured data. A username or password containing @, :, /, #, or other reserved characters must be URL-encoded before it becomes part of the URI. Assigning URL.username and URL.password is safer than concatenating strings because the URL object performs the encoding.

The localhost authentication proxy received one redacted Basic credential after the test assigned a 20-character password containing reserved characters. The ledger stores only the scheme, username, and password length; it does not preserve the secret or encoded URL. Undici's ProxyAgent documentation likewise states that credentials embedded in its URI are URL-decoded before Basic encoding.

An environment variable is a delivery mechanism, not a complete secret-management policy. Restrict who can inspect the process environment, disable debug logs that print configuration objects, redact nested error causes before shipping logs, and rotate a credential after accidental disclosure. Never send Proxy-Authorization to the destination as an ordinary request header; let the proxy implementation attach it to the proxy hop.

Audit NO_PROXY Before Blaming the Gateway

Node 24 recognizes exact hosts, domain suffixes, wildcard domains, exact IP addresses, IP ranges, and host-plus-port entries in NO_PROXY. A matching entry intentionally sends the request directly, so a correct gateway may record nothing.

NO_PROXY='localhost,127.0.0.1,.internal.example,api.example:8443'

Our capture set NO_PROXY to the exact local destination host and port. The destination received one request and the proxy received zero connections. That is the evidence pattern for an intentional direct route. A value of * excludes every destination from proxying.

On case-sensitive systems, Node gives lowercase http_proxy, https_proxy, and no_proxy precedence over their uppercase forms. Windows environment names are case-insensitive, so the lab could not represent two distinct values that differed only by case. Inventory variables by name, but do not print credential-bearing values into CI logs.

Diagnose fetch Failed Through the Nested Cause

Node and Undici often reject a network failure with the outer message TypeError: fetch failed. That string is a wrapper, not a diagnosis. Preserve a small, redacted set of fields from error.cause and pair it with gateway evidence.

try {
  await fetch(url, { signal: AbortSignal.timeout(10_000) });
} catch (error) {
  console.error({
    name: error.name,
    message: error.message,
    causeCode: error.cause?.code,
    causeMessage: error.cause?.message,
  });
}
EvidenceLayerNext safe check
No proxy connection and no NO_PROXY matchProxy opt-in or process configurationConfirm Node version, startup flag, and variable presence.
TCP refusal before an HTTP responseProxy host, port, firewall, or listenerTest one connection from the same runtime and network.
407 and Proxy-AuthenticateProxy authenticationVerify the credential grammar and account state; do not retry with changing identities.
CONNECT rejectedProxy tunnel policyPreserve the gateway response and confirm the destination is permitted.
TimeoutErrorThe configured total time budget expiredIdentify whether the delay was before proxy response, during TLS, or after destination headers.
Destination 401, 403, or 429Destination authentication, authorization, or rate controlStop, honor the response, and use the approved API or access path.

In the tested built-in client, both the local 407 and the deliberately refused CONNECT surfaced as fetch failed with a generic nested cancellation message. That makes proxy-side status logging important; the JavaScript exception alone did not distinguish those two cases.

Bound Time and Retry Only Safe Operations

fetch does not provide a universal production timeout or application retry policy. Use an AbortSignal to bound the complete operation and let the user job determine the budget. The 150 ms value in the localhost lab exists only to make a stalled handshake finish quickly; it is not a production recommendation.

const response = await fetch(url, {
  signal: AbortSignal.timeout(15_000),
});

If the application adds retries, limit them to authorized, idempotent requests whose failure semantics are known. Preserve one overall deadline across all attempts, keep the same intended session when continuity matters, and add jitter to transient backoff. Do not automatically retry 401, 403, 407, CAPTCHA, account enforcement, or an unclear 429. Those responses require permission, credential, quota, or workflow decisions rather than a different exit.

Do not disable TLS verification to make a proxy error disappear. For a controlled private proxy that uses a private certificate authority, configure a scoped trust path appropriate to the deployment and verify which TLS hop failed. NODE_TLS_REJECT_UNAUTHORIZED=0 disables certificate verification broadly and Node's documentation strongly discourages it.

Add Databay Routing Controls After the Base Request Works

First establish one request with the base gateway, base zone credential, a bounded timeout, and a harmless destination you control. Then add one documented country or session field at a time so any failure has one new variable.

// Values come from a secret manager; shown separately for clarity.
DATABAY_PROXY_USER='USER-zone-residential-countryCode-us-sessionId-node-qa-01'
DATABAY_PROXY_PASS='REDACTED'
DATABAY_HTTP_PROXY='http://gw.databay.co:8888'

A country field requests an exit from that pool; it does not reproduce language, account history, device, consent, or every other localization signal. A session field requests temporary route continuity; it is not a device identity. Verify the observed exit once with a controlled endpoint, then run the authorized workflow without changing route identity in response to a destination restriction.

Use datacenter proxies when an approved job prioritizes throughput and stable infrastructure, or residential proxies when it genuinely needs a consumer-network route or country sample. The HTTP versus SOCKS5 guide covers protocol and DNS decisions; Node's built-in environment support described here documents HTTP and HTTPS proxy URLs.

Run This Preflight Before Shipping

  1. Record process.version and process.versions.undici; if using the package, record its separate version too.
  2. Choose one control surface: process-wide startup flag, runtime global policy, or same-package per-request Undici dispatcher.
  3. Inspect the names and routing meaning of proxy environment variables without logging their values.
  4. Set a total timeout and make one request with no redirect or retry loop.
  5. Confirm whether the proxy saw CONNECT, an absolute-form request, a 407, or no connection.
  6. Confirm whether NO_PROXY intentionally selected the direct path.
  7. Inspect the nested cause, then correlate it with gateway and destination logs.
  8. Keep TLS verification enabled and use scoped trust configuration where a private authority is legitimate.
  9. Stop on unclear permission, authentication boundaries, destination restrictions, quota signals, or account enforcement.
  10. Close a request-scoped dispatcher during orderly shutdown so pooled sockets do not outlive the worker.

Use the browser-only proxy format converter to validate an endpoint's syntax without transmitting credentials, and use the proxy checker evidence contract when you need a point-in-time remote reachability observation. Neither tool can authorize a destination or prove future availability.

Troubleshooting desk

Questions specific to Node.js fetch

Does Node.js fetch use HTTP_PROXY automatically?
Not merely because the variable exists. In supported Node releases you must opt in at startup with --use-env-proxy or NODE_USE_ENV_PROXY=1, or enable the global policy at runtime with http.setGlobalProxyFromEnv().
Should I use Node's built-in proxy support or Undici ProxyAgent?
Use the built-in environment path when one policy should govern the process. Use a ProxyAgent with fetch imported from the same Undici package when a selected request or client needs an explicit dispatcher.
Why does fetch only say TypeError: fetch failed?
That is the outer network-error wrapper. Inspect a redacted error.cause and correlate it with proxy evidence such as no connection, 407, CONNECT rejection, TLS failure, or timeout.
Can I pass a ProxyAgent from any Undici version to built-in fetch?
Do not assume so. Node requires a compatible dispatcher. In the documented test, an Undici 8.9 ProxyAgent passed to Node 24.14 built-in fetch, which bundled Undici 7.21, failed before the proxy connection. Import fetch and ProxyAgent from the same installed package for the per-request pattern.
How should proxy credentials with @ or : be encoded?
Build a URL object and assign its username and password properties so reserved characters are encoded. Inject the raw values from a secret manager and never print the resulting credential-bearing URL.
Does NO_PROXY explain why the gateway saw no request?
It can. A matching exact host, suffix, wildcard, IP, range, or host-and-port entry selects a direct route. The local capture confirmed that an exact host-and-port match reached the destination while the proxy recorded no connection.
Adjacent runbooks

Same gateway, different control surface

  • Axios

    HTTP client library

    Axios request -> route selector -> absolute-form or CONNECT -> hop evidence

  • cURL

    Command-line client

    curl -> proxy auth -> CONNECT or SOCKS -> approved destination

  • Python Requests

    HTTP client library

    Session -> HTTPAdapter policy -> proxy hop -> approved destination

  • Incogniton

    Multi-profile browser

    Profile editor -> Databay gateway -> pinned residential exit

  • Playwright

    Browser automation

    Browser process -> isolated contexts -> independent exit sessions

  • Puppeteer

    Browser automation

    --proxy-server -> page.authenticate() -> page.goto()

  • Scrapy

    Crawler framework

    Request queue -> downloader middleware -> rotating exit pool

  • Selenium

    Browser automation

    WebDriver -> auth decision -> Databay gateway

Choose the exit pool after the control path works. Use residential for reputation-sensitive targets or datacenter for throughput.

Residential →Datacenter →

Ship Node.js fetch to production

Create an account, drop in your gateway credentials, and route your first Node.js fetch request in minutes.

Pricing, order minimums, and traffic validity vary by product.