Short answers
How do I set a proxy for Node.js fetch?
Install undici, create a ProxyAgent and pass it as the dispatcher option to Undici’s fetch. This makes the route explicit for the request without changing the configuration of unrelated HTTP clients.
Where do the username and password go?
Put the gateway address in the agent’s uri and the Basic authentication value in its token. Keep proxy authentication out of the destination request headers and keep both credentials out of source control.
Does Node support HTTP_PROXY and HTTPS_PROXY?
Current Node releases offer opt-in environment proxy support, but older versions and different clients behave differently. Check your runtime and its startup configuration. An explicit dispatcher avoids depending on a process-wide setting for this example.
Why does fetch only say “fetch failed”?
The top-level error can wrap a connection, authentication, TLS or timeout failure. Inspect its cause locally, then compare the same configuration in a small curl check. Do not assume every fetch failure means the destination returned an HTTP error.
If your project uses Axios, follow the Axios proxy setup guide. Its HTTP agent configuration differs from Undici’s dispatcher.
Use a matching client and dispatcher
Use a supported Node.js runtime compatible with the Undici release you install. Node’s bundled implementation and the separately installed package are not necessarily the same version. This recipe imports both fetch and ProxyAgent from the same package so their interfaces are aligned.
node --version
npm install undici
npm ls undiciThe Undici project documentation publishes its runtime support information. Keep your lockfile with the application so a deployment uses the version you tested rather than silently resolving a different one.
- Open the connection builder, choose HTTP and select your pool, country and rotation.
- Provide
PROXY_USERas the full generated username andPROXY_PASSWORDas the proxy password through runtime secret configuration. - Keep the gateway address separate from credentials. The optional
PROXY_SERVERvariable below contains just an HTTP scheme, host and port. - Use a small echo request before a real authorised job. The curl test is a useful independent check of the same settings.
Which fetch does your project use?
Check the import at the top of the file before choosing a proxy option. Node’s built-in fetch, Undici’s exported fetch and the separate node-fetch package can share the same function name while expecting different connection options.
| Client | How to recognise it | Connection option |
|---|---|---|
| Built-in Node.js fetch | The runtime-provided global fetch, unless your application replaces it. | An Undici-compatible dispatcher; check compatibility with the Node version you deploy. |
| Undici package | fetch imported from undici, as in the example below. | dispatcher, with ProxyAgent from the same installed package. |
| node-fetch package | fetch imported from node-fetch. | agent, using an HTTP/HTTPS agent appropriate to the proxy and destination. Undici’s ProxyAgent is a different interface. |
The Node fetch reference documents custom dispatchers for the built-in implementation. The separate node-fetch agent reference documents its agent option. If your file imports node-fetch, do not paste the dispatcher option into that call and assume it changed the route. This guide’s runnable recipe uses Undici; it is not a tested node-fetch integration.
Send one request through the HTTP gateway
Save the following file as proxy-check.mjs and run node proxy-check.mjs. The request uses an explicit dispatcher, a deadline and a response check. It prints only the status and your own observed exit address.
import { ProxyAgent, fetch } from 'undici';
function required(name) {
const value = process.env[name];
if (!value) throw new Error('Missing ' + name);
return value;
}
async function main() {
const username = required('PROXY_USER');
const password = required('PROXY_PASSWORD');
const dispatcher = new ProxyAgent({
uri: process.env.PROXY_SERVER ?? 'http://gw.portproof.org:7000',
token: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'),
});
const signal = AbortSignal.timeout(30_000);
try {
const response = await fetch('https://api.portproof.org/v1/echo-ip', {
dispatcher,
signal,
});
const text = await response.text();
if (!response.ok) throw new Error('HTTP status ' + response.status);
const payload = JSON.parse(text);
if (typeof payload.ip !== 'string') {
throw new Error('Echo did not return an IP address');
}
console.log('HTTP', response.status, 'exit', payload.ip);
} finally {
if (signal.aborted) await dispatcher.destroy();
else await dispatcher.close();
}
}
main().catch((error) => {
console.error('Proxy check failed:', error.cause?.code ?? error.name);
process.exitCode = 1;
});The agent configuration uses the current ProxyAgent authentication interface. token is the complete Basic header value. Base64 is an encoding, not encryption; treat that token as a password and never log it.
An HTTPS destination can use this HTTP gateway through a CONNECT tunnel. The proxy URI stays http:// while the destination remains https://. Do not change the gateway scheme just because the destination is secure, and keep certificate verification enabled.
Do not mix agent and dispatcher options
A recipe written for node:http, node:https or another request library may use an agent option. Undici’s request configuration uses dispatcher. Similar names do not make the objects interchangeable: configure the API you actually import.
Likewise, do not add Proxy-Authorization to the headers of the destination fetch. The proxy agent handles gateway authentication. An application’s own Authorization header is a separate credential with a different recipient.
The example is deliberately scoped to HTTP proxying. SOCKS support depends on the installed client version and the agent you choose. Keep the matching protocol and port together and consult that version’s documentation; HTTP or SOCKS5 explains the choice.
Bound the request and finish the body
The Node AbortSignal reference documents AbortSignal.timeout. Here it limits the operation while fetch and body reading are in progress. Thirty seconds is an example budget for a small check, not a network guarantee. A larger job also needs limits on queued work and overall runtime.
The example reads the body before interpreting the HTTP status. That releases the response resources for both success and an ordinary error response. If your application deliberately ignores a body, cancel it instead. Undici warns that unconsumed response bodies can exhaust connection resources; relying on garbage collection can make a worker stall.
For a long-running service, create one dispatcher per stable proxy configuration and reuse it. Close it during shutdown after the work assigned to it has finished. Creating a fresh dispatcher for every request discards pooling and complicates lifecycle management.
Why can cleanup outlast a fetch timeout?
AbortSignal.timeout() limits the fetch and body read; it does not put a deadline on every later await. Graceful dispatcher.close() waits for the dispatcher’s work to finish. The one-request example above instead calls destroy() when its signal has aborted. See the Undici dispatcher lifecycle.
We checked this with Node.js 22.22.0 and installed Undici 7.29.1 on 30 September 2026. A local authenticated CONNECT proxy deliberately withheld its reply for 1,500 ms while the request used a 250 ms timeout. These are recorded results from a local simulation; they do not measure the Portproof gateway.
| Cleanup after the timeout | Fetch rejected after | Cleanup finished after |
|---|---|---|
| Graceful close() | 254 ms | 1,516 ms |
| Conditional destroy() after signal abort | 255 ms | 259 ms |
The same correction kept graceful cleanup for a completed response. A slow response body also aborted after headers arrived. Scheduling affects these timings, so they are not a precise deadline guarantee. Use the CONNECT failure checklist when the tunnel is rejected before a destination response exists.
destroy() aborts other work using that dispatcher. This pattern is for the dispatcher owned by this one-request check. A service sharing one dispatcher across jobs needs its own shutdown policy; one job’s timeout must not cancel unrelated work.Choose explicit or environment configuration
Newer Node releases offer --use-env-proxy and NODE_USE_ENV_PROXY=1. Availability and details depend on the runtime; check the Node command-line reference for the version deployed to your worker.
Within an application that imports Undici, EnvHttpProxyAgent is another documented choice when deployment-wide HTTP_PROXY, HTTPS_PROXY and NO_PROXY settings are intentional. Its environment proxy documentation describes precedence and host exclusions. Those exclusions matter: an excluded destination may be reached directly even though another request uses a proxy.
Do not combine process-level routing, a global dispatcher and a per-request dispatcher without a clear reason. For this initial check, use the explicit recipe alone. For a centrally managed environment, follow that network policy and verify the deployed process rather than assuming your local shell’s configuration carried over.
Connection reuse changes rotation tests
Two calls to fetch can reuse an existing connection. A rotation mode that acts on new connections therefore does not imply a different address on every JavaScript call. Test the connection behaviour you need, rather than treating equal echo responses as automatic proof of failure.
For a related sequence of requests, choose a sticky session in the builder and keep its generated username with that job. A sticky session keeps the same device while available; the device’s address can still change. The session guide explains what continuity can and cannot mean for a workflow.
Troubleshoot Node.js proxy errors
| What you observe | What to inspect |
|---|---|
| Module not found or an unsupported engine warning | Install a compatible Undici version in the actual application environment and deploy its lockfile. |
| fetch failed with a connection error cause | Gateway hostname, port, outbound connectivity and whether the worker has the same settings as the passing curl check. |
| Proxy authentication failure | The complete username and current proxy password; keep Basic authentication in the agent token. |
| TimeoutError or an abort-related failure | The request budget, current gateway availability and destination responsiveness. |
| A successful fetch with status 403 or 429 | Inspect which server answered and the destination’s access or rate policy. Fetch does not treat every HTTP error status as a rejected promise. |
| A TLS verification error | The destination hostname, certificate chain and runtime trust configuration. |
| A worker stalls after many requests | Bodies that were neither consumed nor cancelled, excessive concurrency, and dispatchers that were never closed. |
Keep detailed error inspection local and redact credentials before sharing a log. Use 407 troubleshooting and CONNECT tunnel failures to separate authentication from transport. Never rotate endlessly in response to a destination’s refusal.
After the echo succeeds, make one authorised application request and verify its content. Add bounded retries only for operations safe to repeat, respect rate limits, and measure traffic before increasing concurrency. See how traffic is counted, check per-GB prices or look up a connection field in the setup reference.
What is not allowed
Use proxy requests for authorised QA, lawful monitoring and research within the destination’s terms and rate limits. A route through a different network does not change permission to access a resource. See the acceptable-use policy.
Read next
Python Requests proxy setup
A complete Python check with HTTP and SOCKS5, predictable settings and a practical failure checklist.
Read the guide
HTTPS_PROXY not working? Check your client
Check inherited variables, exclusions and client support when curl works but an application does not.
Read the guide