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
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.
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.
| Requirement | Use | Version boundary |
|---|---|---|
| One proxy policy for the whole process | node --use-env-proxy app.mjs or NODE_USE_ENV_PROXY=1 | Built-in proxy support arrived in Node 24.5. |
| Enable or replace the global policy in code | http.setGlobalProxyFromEnv() | Added in Node 24.14. Call it before requests begin. |
| Proxy only a selected request or client | Import both fetch and ProxyAgent from the same installed Undici package | This 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 path | First proxy message | Measured result |
|---|---|---|
Built-in fetch, environment only | No proxy connection | The local destination was reached directly; environment variables were ignored without the Node opt-in. |
Built-in fetch plus --use-env-proxy | CONNECT 127.0.0.1:PORT | Node tunneled even the HTTP destination through the local proxy. |
http.setGlobalProxyFromEnv() | CONNECT 127.0.0.1:PORT | The runtime API changed the global dispatcher and the request completed through the tunnel. |
Undici 8.9 fetch plus ProxyAgent | GET http://127.0.0.1:PORT/path | The HTTP destination used absolute-form rather than CONNECT. |
| Built-in HTTPS destination | CONNECT example.test:443 | The lab stopped before destination TLS and fetch rejected. |
| Stalled proxy handshake | TCP connection, no HTTP response | AbortSignal.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,
});
}| Evidence | Layer | Next safe check |
|---|---|---|
No proxy connection and no NO_PROXY match | Proxy opt-in or process configuration | Confirm Node version, startup flag, and variable presence. |
| TCP refusal before an HTTP response | Proxy host, port, firewall, or listener | Test one connection from the same runtime and network. |
407 and Proxy-Authenticate | Proxy authentication | Verify the credential grammar and account state; do not retry with changing identities. |
| CONNECT rejected | Proxy tunnel policy | Preserve the gateway response and confirm the destination is permitted. |
TimeoutError | The configured total time budget expired | Identify whether the delay was before proxy response, during TLS, or after destination headers. |
| Destination 401, 403, or 429 | Destination authentication, authorization, or rate control | Stop, 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
- Record
process.versionandprocess.versions.undici; if using the package, record its separate version too. - Choose one control surface: process-wide startup flag, runtime global policy, or same-package per-request Undici dispatcher.
- Inspect the names and routing meaning of proxy environment variables without logging their values.
- Set a total timeout and make one request with no redirect or retry loop.
- Confirm whether the proxy saw CONNECT, an absolute-form request, a 407, or no connection.
- Confirm whether
NO_PROXYintentionally selected the direct path. - Inspect the nested cause, then correlate it with gateway and destination logs.
- Keep TLS verification enabled and use scoped trust configuration where a private authority is legitimate.
- Stop on unclear permission, authentication boundaries, destination restrictions, quota signals, or account enforcement.
- 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.
Questions specific to Node.js fetch
Does Node.js fetch use HTTP_PROXY automatically?
Should I use Node's built-in proxy support or Undici ProxyAgent?
Why does fetch only say TypeError: fetch failed?
Can I pass a ProxyAgent from any Undici version to built-in fetch?
How should proxy credentials with @ or : be encoded?
Does NO_PROXY explain why the gateway saw no request?
Same gateway, different control surface
Python Requests
HTTP client library
Session -> HTTPAdapter policy -> proxy hop -> approved destination
Choose the exit pool after the control path works. Use residential for reputation-sensitive targets or datacenter for throughput.
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.