Sample
undici 8.10.0: Outbound requests ignore HTTP_PROXY unless EnvHttpProxyAgent is explicitly configured, where lowercase env var spelling wins and HTTP proxy acts as fallback for HTTPS but not vice versa
Verified sample for npm undici 8.10.0: Outbound requests ignore HTTP_PROXY unless EnvHttpProxyAgent is explicitly configured, where lowercase env var…
sha256:c5f7d661b349b93b5d644271e2edd085fbcb52460bdd7105e5fec9f64148e1de
This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured.
How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people.
MIT-0
Execution evidence
The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.
- Evidence basis
- Signed contract pass
- Verification receipts
- 3
- Signing keys that built it
- 3
Declared environment
node linux x64 node node npm
Verification-run environments
| Environment | Contract | Stages | Run |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:ac1544ece1e22594 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-14 |
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
| node 22 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1node:22-alpine@sha256:c610fcdfb1d5… |
2026-09-07 |
Case
HOW- Goal
- Outbound requests ignore HTTP_PROXY unless EnvHttpProxyAgent is explicitly configured, where lowercase env var spelling wins and HTTP proxy acts as fallback for HTTPS but not vice versa
- Packages
- Symbols
-
- undici.EnvHttpProxyAgent
- undici.ProxyAgent
- undici.Agent
- undici.getGlobalDispatcher
- undici.setGlobalDispatcher
- Environment
- node
- Created
- 2026-08-14T22:45:01Z
Contract
- assert getGlobalDispatcher() defaults to an unproxied Agent that ignores proxy environment variables
- assert ProxyAgent throws without an explicit uri whereas EnvHttpProxyAgent loads from environment
- assert lowercase http_proxy takes precedence over uppercase HTTP_PROXY
- assert omitted https_proxy falls back to http_proxy but omitted http_proxy leaves HTTP direct
- assert no_proxy rules bypass proxy dispatching for matched hostnames, subdomains, and ports
- assert process.env.no_proxy is evaluated dynamically on each dispatch while proxy URIs are bound at construction
- assert setGlobalDispatcher(new EnvHttpProxyAgent()) activates environment-driven proxying
Files
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
Source
A model would expect default undici requests to automatically honour `process.env.HTTP_PROXY` or instantiate `new ProxyAgent()` with no arguments to pick up proxy settings.
The wrong version fails silently with a green build by bypassing the proxy and making direct unproxied outbound requests, or fails loudly with `InvalidArgumentError: Proxy uri is mandatory` if using `new ProxyAgent()`.
{"case":{"caseId":"case:sha256:99c5c121016a6b47e1b4d36c7080f1581322e13cd5ba731a4de0a63790c653e3","constraints":{"runtime":"node"},"contract":["assert getGlobalDispatcher() defaults to an unproxied Agent that ignores proxy environment variables","assert ProxyAgent throws without an explicit uri whereas EnvHttpProxyAgent loads from environment","assert lowercase http_proxy takes precedence over uppercase HTTP_PROXY","assert omitted https_proxy falls back to http_proxy but omitted http_proxy leaves HTTP direct","assert no_proxy rules bypass proxy dispatching for matched hostnames, subdomains, and ports","assert process.env.no_proxy is evaluated dynamically on each dispatch while proxy URIs are bound at construction","assert setGlobalDispatcher(new EnvHttpProxyAgent()) activates environment-driven proxying"],"goal":"Outbound requests ignore HTTP_PROXY unless EnvHttpProxyAgent is explicitly configured, where lowercase env var spelling wins and HTTP proxy acts as fallback for HTTPS but not vice versa","kind":"HOW","packages":["pkg:npm/undici@8.10.0"],"schemaVersion":1,"symbols":["undici.EnvHttpProxyAgent","undici.ProxyAgent","undici.Agent","undici.getGlobalDispatcher","undici.setGlobalDispatcher"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/undici@8.10.0"],"schemaVersion":1,"symbols":["undici.EnvHttpProxyAgent","undici.ProxyAgent","undici.Agent","undici.getGlobalDispatcher","undici.setGlobalDispatcher"],"verifierAdapter":"node-typescript@1"}
{
"name": "npm-undici-proxy",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "npm-undici-proxy",
"version": "1.0.0",
"dependencies": {
"undici": "8.10.0"
}
},
"node_modules/undici": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
"integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
}
}
}
}
{
"name": "npm-undici-proxy",
"version": "1.0.0",
"type": "module",
"dependencies": {
"undici": "8.10.0"
}
}
import assert from 'node:assert/strict';
import { Agent, ProxyAgent, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
// Helper to extract internal dispatcher symbols from EnvHttpProxyAgent
const sampleSymbols = Object.getOwnPropertySymbols(new EnvHttpProxyAgent());
const kNoProxyAgent = sampleSymbols.find((s) => s.description === 'no proxy agent');
const kHttpProxyAgent = sampleSymbols.find((s) => s.description === 'http proxy agent');
const kHttpsProxyAgent = sampleSymbols.find((s) => s.description === 'https proxy agent');
assert.ok(kNoProxyAgent && kHttpProxyAgent && kHttpsProxyAgent, 'Dispatcher symbols must exist');
function getProxyUri(proxyAgentInstance) {
if (!proxyAgentInstance) return null;
const syms = Object.getOwnPropertySymbols(proxyAgentInstance);
const kProxy = syms.find((s) => s.description === 'proxy agent options');
return kProxy ? proxyAgentInstance[kProxy]?.uri : null;
}
// Trap 1: Default global dispatcher does NOT honour proxy environment variables
const defaultDispatcher = getGlobalDispatcher();
assert.equal(defaultDispatcher.constructor.name, 'Agent', 'Default dispatcher must be direct Agent');
assert.notEqual(defaultDispatcher.constructor.name, 'EnvHttpProxyAgent', 'Default dispatcher is not EnvHttpProxyAgent');
// Trap 2: ProxyAgent vs EnvHttpProxyAgent constructor contract
assert.throws(() => new ProxyAgent(), { message: 'Proxy uri is mandatory' });
assert.doesNotThrow(() => new EnvHttpProxyAgent());
// Trap 3: Option precedence over environment variables
{
const agent = new EnvHttpProxyAgent({
httpProxy: 'http://opt-proxy.example:8080',
httpsProxy: 'http://opt-https-proxy.example:8443'
});
assert.equal(getProxyUri(agent[kHttpProxyAgent]), 'http://opt-proxy.example:8080/');
assert.equal(getProxyUri(agent[kHttpsProxyAgent]), 'http://opt-https-proxy.example:8443/');
}
// Trap 4: Asymmetric proxy fallback for HTTP vs HTTPS
{
// Only HTTP proxy configured -> HTTPS falls back to HTTP proxy
const httpOnly = new EnvHttpProxyAgent({ httpProxy: 'http://http-proxy.example:8080' });
assert.notEqual(httpOnly[kHttpProxyAgent], httpOnly[kNoProxyAgent]);
assert.equal(httpOnly[kHttpsProxyAgent], httpOnly[kHttpProxyAgent]);
// Only HTTPS proxy configured -> HTTP stays unproxied direct Agent
const httpsOnly = new EnvHttpProxyAgent({ httpsProxy: 'http://https-proxy.example:8443' });
assert.equal(httpsOnly[kHttpProxyAgent], httpsOnly[kNoProxyAgent]);
assert.notEqual(httpsOnly[kHttpsProxyAgent], httpsOnly[kNoProxyAgent]);
}
// Trap 5: Dispatch routing and NO_PROXY matching rules (no network)
{
const agent = new EnvHttpProxyAgent({
httpProxy: 'http://http-gw.example:8080',
httpsProxy: 'http://https-gw.example:8443',
noProxy: 'bypass.example,*.sub.example,host-with-port.example:9000'
});
let target = null;
agent[kNoProxyAgent].dispatch = () => { target = 'direct'; return true; };
agent[kHttpProxyAgent].dispatch = () => { target = 'http-proxy'; return true; };
agent[kHttpsProxyAgent].dispatch = () => { target = 'https-proxy'; return true; };
agent.dispatch({ origin: 'http://api.example.com', path: '/' }, {});
assert.equal(target, 'http-proxy', 'HTTP requests route to http proxy');
agent.dispatch({ origin: 'https://api.example.com', path: '/' }, {});
assert.equal(target, 'https-proxy', 'HTTPS requests route to https proxy');
agent.dispatch({ origin: 'http://bypass.example', path: '/' }, {});
assert.equal(target, 'direct', 'NO_PROXY exact host routes direct');
agent.dispatch({ origin: 'https://service.sub.example', path: '/' }, {});
assert.equal(target, 'direct', 'NO_PROXY wildcard domain routes direct');
agent.dispatch({ origin: 'http://host-with-port.example:9000', path: '/' }, {});
assert.equal(target, 'direct', 'NO_PROXY matching port routes direct');
agent.dispatch({ origin: 'http://host-with-port.example:8080', path: '/' }, {});
assert.equal(target, 'http-proxy', 'NO_PROXY mismatched port routes to proxy');
}
// Trap 6: Dynamic NO_PROXY evaluation vs Static HTTP_PROXY binding
{
const origHttp = process.env.http_proxy;
const origNo = process.env.no_proxy;
const origHttpUpper = process.env.HTTP_PROXY;
const origNoUpper = process.env.NO_PROXY;
try {
delete process.env.no_proxy;
delete process.env.NO_PROXY;
process.env.http_proxy = 'http://init-proxy.example:8080';
const dynAgent = new EnvHttpProxyAgent();
let target = null;
dynAgent[kNoProxyAgent].dispatch = () => { target = 'direct'; return true; };
dynAgent[kHttpProxyAgent].dispatch = () => { target = 'proxy'; return true; };
dynAgent.dispatch({ origin: 'http://dyn.example', path: '/' }, {});
assert.equal(target, 'proxy', 'Initial dispatch uses proxy');
process.env.no_proxy = 'dyn.example';
dynAgent.dispatch({ origin: 'http://dyn.example', path: '/' }, {});
assert.equal(target, 'direct', 'Mutating process.env.no_proxy updates dispatch dynamically');
process.env.http_proxy = 'http://changed-proxy.example:9999';
assert.equal(getProxyUri(dynAgent[kHttpProxyAgent]), 'http://init-proxy.example:8080/');
} finally {
if (origHttp !== undefined) process.env.http_proxy = origHttp; else delete process.env.http_proxy;
if (origNo !== undefined) process.env.no_proxy = origNo; else delete process.env.no_proxy;
if (origHttpUpper !== undefined) process.env.HTTP_PROXY = origHttpUpper; else delete process.env.HTTP_PROXY;
if (origNoUpper !== undefined) process.env.NO_PROXY = origNoUpper; else delete process.env.NO_PROXY;
}
}
// Trap 7: setGlobalDispatcher activation contract
{
const customAgent = new EnvHttpProxyAgent({ httpProxy: 'http://proxy.example:8080' });
setGlobalDispatcher(customAgent);
assert.equal(getGlobalDispatcher(), customAgent, 'Global dispatcher updated to EnvHttpProxyAgent');
}