Skip to content

http,https: add built-in proxy support in http/https.request and Agent #58980

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

joyeecheung
Copy link
Member

@joyeecheung joyeecheung commented Jul 7, 2025

This patch implements proxy support for HTTP and HTTPS clients and
agents in the http and https built-ins`. When NODE_USE_ENV_PROXY
is set to 1, the default global agent would parse the
HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy
settings from the environment variables, and proxy the requests
sent through the built-in http/https client accordingly.

To support this, http.Agent and https.Agent now accept a few new
options:

  • proxyEnv: when it's an object, the agent would read and parse
    the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy
    properties from it, and apply them based on the protocol it uses
    to send requests. This option allows custom agents to
    reuse built-in proxy support by composing options. Global agents
    set this to process.env when NODE_USE_ENV_PROXY is 1.
  • defaultPort and protocol: these allow setting of the default port
    and protocol of the agents. We also need these when configuring
    proxy settings and deciding whether a request should be proxied.

Example

Starting a Node.js process with proxy support enabled for all requests sent
through the default global agent:

NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node client.js

To create a custom agent with built-in proxy support:

const http = require('node:http');
// Creating a custom agent with custom proxy support.
const agent = new http.Agent({ proxyEnv: { HTTP_PROXY: 'http://proxy.example.com:8080' } });
http.request({
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  agent,
}, (res) => {
  // This request will be proxied through proxy.example.com:8080 using the HTTP protocol.
  console.log(`STATUS: ${res.statusCode}`);
});

Alternatively, the following also works:

const http = require('node:http');
// Use lower-cased option name.
const agent1 = new http.Agent({ proxyEnv: { http_proxy: 'http://proxy.example.com:8080' } });
// Use values inherited from the environment variables, if the process is started with
// HTTP_PROXY=http://proxy.example.com:8080 this will use the proxy server specified
// in process.env.HTTP_PROXY.
const agent2 = new http.Agent({ proxyEnv: process.env });

Implementation

Implementation-wise, this adds a ProxyConfig internal class to handle
parsing and application of proxy configurations. The configuration
is parsed during agent construction. When requests are made,
the createConnection() methods on the agents would check whether
the request should be proxied. If yes, they either connect to the
proxy server (in the case of HTTP reqeusts) or establish a tunnel
(in the case of HTTPS requests) through either a TCP socket (if the
proxy uses HTTP) or a TLS socket (if the proxy uses HTTPS).

When proxying HTTPS requests through a tunnel, the connection listener
is invoked after the tunnel is established. Tunnel establishment uses
the timeout of the request options, if there is one. Otherwise it uses
the timeout of the agent.

If an error is encountered during tunnel establishment, an
ERR_PROXY_TUNNEL would be emitted on the returned socket. If the proxy
server sends a errored status code, the error would contain an
statusCode property. If the error is caused by timeout, the error
would contain a proxyTunnelTimeout property.

This implementation honors the built-in socket pool and socket limits.
Pooled sockets are still keyed by request endpoints, they are just
connected to the proxy server instead, and the persistence of the
connection can be maintained as long as the proxy server respects
connection/proxy-connection or persist by default (HTTP/1.1)

Testing

Most of the diff of this patch are tests for various cases that a proxied client can run into. I also tested it with a production proxy server behind a firewall.

To check how transparent the sockets behave when they are going through a proxy, I also ran the existing http/https tests over a minimal testing proxy server:

NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:8000 HTTP_PROXY=http://127.0.0.1:8000 tools/test.py --timeout=20 "test/parallel/test-http*" "test/sequential/test-http*"

[03:36|% 100|+ 635|-  80]: Done

So >88% of the existing HTTP/HTTPS use cases work transparently when they go through a proxy. Among the failures most of them are caused by testing proxy server not being transparent since I didn't put a lot of thought into it, or that the tests are expecting specific things (e.g. events, errors) that have to come from a server in the same process. I think this is good enough as an initial implementation and we can continue iterating to make the proxied behavior as transparent as possible.

Some TODOs:

  • Check that it allows other popular user-land agents (for proxy or not) to operate with/without NODE_USE_ENV_PROXY
  • Check that it works transparently with popular npm packages that uses http/https.request

The first commit comes from #58950 - I split it off since it just moved the existing tests for fetch into a new client-proxy directory and made the testing proxy server a bit more versatile, not strictly tied to what this patch tries to implement.

Refs: #57872
Refs: #8381
Refs: #15620

Rewrite to ESM to use TLA.
Also add a test to make sure case precedence is honored.
Refs: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/http
  • @nodejs/net

This patch implements proxy support for HTTP and HTTPS clients and
agents in the `http` and `https` built-ins`. When NODE_USE_ENV_PROXY
is set to 1, the default global agent would parse the
HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy
settings from the environment variables, and proxy the requests
sent through the built-in http/https client accordingly.

To support this, `http.Agent` and `https.Agent` now accept a few new
options:

- `proxyEnv`: when it's an object, the agent would read and parse
  the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy
  properties from it, and apply them based on the protocol it uses
  to send requests. This option allows custom agents to
  reuse built-in proxy support by composing options. Global agents
  set this to `process.env` when NODE_USE_ENV_PROXY is 1.
- `defaultPort` and `protocol`: these allow setting of the default port
  and protocol of the agents. We also need these when configuring
  proxy settings and deciding whether a request should be proxied.

Implementation-wise, this adds a `ProxyConfig` internal class to handle
parsing and application of proxy configurations. The configuration
is parsed during agent construction. When requests are made,
the `createConnection()` methods on the agents would check whether
the request should be proxied. If yes, they either connect to the
proxy server (in the case of HTTP reqeusts) or establish a tunnel
(in the case of HTTPS requests) through either a TCP socket (if the
proxy uses HTTP) or a TLS socket (if the proxy uses HTTPS).

When proxying HTTPS requests through a tunnel, the connection listener
is invoked after the tunnel is established. Tunnel establishment uses
the timeout of the request options, if there is one. Otherwise it uses
the timeout of the agent.

If an error is encountered during tunnel establishment, an
ERR_PROXY_TUNNEL would be emitted on the returned socket. If the proxy
server sends a errored status code, the error would contain an
`statusCode` property. If the error is caused by timeout, the error
would contain a `proxyTunnelTimeout` property.

This implementation honors the built-in socket pool and socket limits.
Pooled sockets are still keyed by request endpoints, they are just
connected to the proxy server instead, and the persistence of the
connection can be maintained as long as the proxy server respects
connection/proxy-connection or persist by default (HTTP/1.1)
@joyeecheung joyeecheung force-pushed the http-agent-proxy-pr branch from 2bcc125 to fd6026e Compare July 7, 2025 10:59
Copy link

codecov bot commented Jul 7, 2025

Codecov Report

Attention: Patch coverage is 93.95425% with 37 lines in your changes missing coverage. Please review.

Project coverage is 90.10%. Comparing base (9ab9763) to head (2172897).
Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/http.js 94.27% 8 Missing and 3 partials ⚠️
lib/https.js 96.21% 8 Missing and 2 partials ⚠️
lib/_http_agent.js 91.57% 8 Missing ⚠️
lib/_http_client.js 85.71% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #58980      +/-   ##
==========================================
+ Coverage   90.06%   90.10%   +0.03%     
==========================================
  Files         640      641       +1     
  Lines      188523   189243     +720     
  Branches    36982    37119     +137     
==========================================
+ Hits       169796   170509     +713     
+ Misses      11449    11438      -11     
- Partials     7278     7296      +18     
Files with missing lines Coverage Δ
lib/internal/errors.js 97.50% <100.00%> (+<0.01%) ⬆️
lib/internal/process/pre_execution.js 90.21% <100.00%> (-0.54%) ⬇️
lib/_http_agent.js 97.28% <91.57%> (-0.89%) ⬇️
lib/_http_client.js 97.33% <85.71%> (-0.66%) ⬇️
lib/https.js 98.07% <96.21%> (-1.23%) ⬇️
lib/internal/http.js 95.66% <94.27%> (-4.34%) ⬇️

... and 41 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@joyeecheung joyeecheung force-pushed the http-agent-proxy-pr branch from 822460a to 2172897 Compare July 7, 2025 23:08
@joyeecheung joyeecheung added http Issues or PRs related to the http subsystem. semver-minor PRs that contain new features and should be released in the next minor version. notable-change PRs with changes that should be highlighted in changelogs. labels Jul 8, 2025
Copy link
Contributor

github-actions bot commented Jul 8, 2025

The notable-change PRs with changes that should be highlighted in changelogs. label has been added by @joyeecheung.

Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section.

@joyeecheung joyeecheung added https Issues or PRs related to the https subsystem. cli Issues and PRs related to the Node.js command line interface. labels Jul 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cli Issues and PRs related to the Node.js command line interface. http Issues or PRs related to the http subsystem. https Issues or PRs related to the https subsystem. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants