From 34a41728c8f1fd05fd25520fd7af1110ca27f6ae Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 3 Jul 2025 20:11:13 +0200 Subject: [PATCH 1/5] test: move http proxy tests to test/client-proxy 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 --- test/client-proxy/client-proxy.status | 7 ++ test/client-proxy/test-http-proxy-fetch.mjs | 76 ++++++++++++++++++ test/client-proxy/test-https-proxy-fetch.mjs | 83 ++++++++++++++++++++ test/client-proxy/testcfg.py | 6 ++ test/common/proxy-server.js | 78 ++++++++++++++++-- test/parallel/test-http-proxy-fetch.js | 62 --------------- test/parallel/test-https-proxy-fetch.js | 67 ---------------- 7 files changed, 243 insertions(+), 136 deletions(-) create mode 100644 test/client-proxy/client-proxy.status create mode 100644 test/client-proxy/test-http-proxy-fetch.mjs create mode 100644 test/client-proxy/test-https-proxy-fetch.mjs create mode 100644 test/client-proxy/testcfg.py delete mode 100644 test/parallel/test-http-proxy-fetch.js delete mode 100644 test/parallel/test-https-proxy-fetch.js diff --git a/test/client-proxy/client-proxy.status b/test/client-proxy/client-proxy.status new file mode 100644 index 00000000000000..33cdae9612b2af --- /dev/null +++ b/test/client-proxy/client-proxy.status @@ -0,0 +1,7 @@ +prefix client-proxy + +# To mark a test as flaky, list the test name in the appropriate section +# below, without ".js", followed by ": PASS,FLAKY". Example: +# sample-test : PASS,FLAKY + +[true] # This section applies to all platforms diff --git a/test/client-proxy/test-http-proxy-fetch.mjs b/test/client-proxy/test-http-proxy-fetch.mjs new file mode 100644 index 00000000000000..bfbd4e911816fb --- /dev/null +++ b/test/client-proxy/test-http-proxy-fetch.mjs @@ -0,0 +1,76 @@ +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { once } from 'events'; +import http from 'node:http'; +import { createProxyServer, checkProxiedFetch } from '../common/proxy-server.js'; + +// Start a server to process the final request. +const server = http.createServer(common.mustCall((req, res) => { + res.end('Hello world'); +}, 3)); +server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); +server.listen(0); +await once(server, 'listening'); + +// Start a minimal proxy server. +const { proxy, logs } = createProxyServer(); +proxy.listen(0); +await once(proxy, 'listening'); + +const serverHost = `localhost:${server.address().port}`; + +// FIXME(undici:4083): undici currently always tunnels the request over +// CONNECT if proxyTunnel is not explicitly set to false, but what we +// need is for it to be automatically false for HTTP requests to be +// consistent with curl. +const expectedLogs = [{ + method: 'CONNECT', + url: serverHost, + headers: { + 'connection': 'close', + 'host': serverHost, + 'proxy-connection': 'keep-alive', + }, +}]; + +// Check upper-cased HTTPS_PROXY environment variable. +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `http://${serverHost}/test`, + HTTP_PROXY: `http://localhost:${proxy.address().port}`, +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +// Check lower-cased https_proxy environment variable. +logs.splice(0, logs.length); +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `http://${serverHost}/test`, + http_proxy: `http://localhost:${proxy.address().port}`, +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +const proxy2 = http.createServer(); +proxy2.on('connect', common.mustNotCall()); +proxy2.listen(0); +await once(proxy2, 'listening'); + +// Check lower-cased http_proxy environment variable takes precedence. +logs.splice(0, logs.length); +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `http://${serverHost}/test`, + http_proxy: `http://localhost:${proxy.address().port}`, + HTTP_PROXY: `http://localhost:${proxy2.address().port}`, +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +proxy.close(); +proxy2.close(); +server.close(); diff --git a/test/client-proxy/test-https-proxy-fetch.mjs b/test/client-proxy/test-https-proxy-fetch.mjs new file mode 100644 index 00000000000000..8a3d7f07284e9a --- /dev/null +++ b/test/client-proxy/test-https-proxy-fetch.mjs @@ -0,0 +1,83 @@ +import * as common from '../common/index.mjs'; +import fixtures from '../common/fixtures.js'; +import assert from 'node:assert'; +import https from 'node:https'; +import http from 'node:http'; +import { once } from 'events'; +import { createProxyServer, checkProxiedFetch } from '../common/proxy-server.js'; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Start a server to process the final request. +const server = https.createServer({ + cert: fixtures.readKey('agent8-cert.pem'), + key: fixtures.readKey('agent8-key.pem'), +}, common.mustCall((req, res) => { + res.end('Hello world'); +}, 3)); +server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); +server.listen(0); +await once(server, 'listening'); + +// Start a minimal proxy server. +const { proxy, logs } = createProxyServer(); +proxy.listen(0); +await once(proxy, 'listening'); + +const serverHost = `localhost:${server.address().port}`; + +const expectedLogs = [{ + method: 'CONNECT', + url: serverHost, + headers: { + 'connection': 'close', + 'host': serverHost, + 'proxy-connection': 'keep-alive', + }, +}]; + +// Check upper-cased HTTPS_PROXY environment variable. +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `https://${serverHost}/test`, + HTTPS_PROXY: `http://localhost:${proxy.address().port}`, + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +// Check lower-cased https_proxy environment variable. +logs.splice(0, logs.length); +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `https://${serverHost}/test`, + https_proxy: `http://localhost:${proxy.address().port}`, + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +const proxy2 = http.createServer(); +proxy2.on('connect', common.mustNotCall()); +proxy2.listen(0); +await once(proxy2, 'listening'); + +// Check lower-cased https_proxy environment variable takes precedence. +logs.splice(0, logs.length); +await checkProxiedFetch({ + NODE_USE_ENV_PROXY: 1, + FETCH_URL: `https://${serverHost}/test`, + https_proxy: `http://localhost:${proxy.address().port}`, + HTTPS_PROXY: `http://localhost:${proxy2.address().port}`, + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), +}, { + stdout: 'Hello world', +}); +assert.deepStrictEqual(logs, expectedLogs); + +proxy.close(); +proxy2.close(); +server.close(); diff --git a/test/client-proxy/testcfg.py b/test/client-proxy/testcfg.py new file mode 100644 index 00000000000000..055549e7a04c5e --- /dev/null +++ b/test/client-proxy/testcfg.py @@ -0,0 +1,6 @@ +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import testpy + +def GetConfiguration(context, root): + return testpy.ParallelTestConfiguration(context, root, 'client-proxy') diff --git a/test/common/proxy-server.js b/test/common/proxy-server.js index d2da6813b5a971..5777d7e2519f3e 100644 --- a/test/common/proxy-server.js +++ b/test/common/proxy-server.js @@ -14,19 +14,32 @@ function logRequest(logs, req) { // This creates a minimal proxy server that logs the requests it gets // to an array before performing proxying. -exports.createProxyServer = function() { +exports.createProxyServer = function(options = {}) { const logs = []; - const proxy = http.createServer(); + let proxy; + if (options.https) { + const common = require('../common'); + if (!common.hasCrypto) { + common.skip('missing crypto'); + } + proxy = require('https').createServer({ + cert: require('./fixtures').readKey('agent9-cert.pem'), + key: require('./fixtures').readKey('agent9-key.pem'), + }); + } else { + proxy = http.createServer(); + } proxy.on('request', (req, res) => { logRequest(logs, req); const [hostname, port] = req.headers.host.split(':'); const targetPort = port || 80; + const url = new URL(req.url); const options = { hostname: hostname, port: targetPort, - path: req.url, + path: url.pathname + url.search, // Convert back to relative URL. method: req.method, headers: req.headers, }; @@ -38,8 +51,16 @@ exports.createProxyServer = function() { proxyReq.on('error', (err) => { logs.push({ error: err, source: 'proxy request' }); - res.writeHead(500); - res.end('Proxy error: ' + err.message); + if (!res.headersSent) { + res.writeHead(500); + } + if (!res.writableEnded) { + res.end(`Proxy error ${err.code}: ${err.message}`); + } + }); + + res.on('error', (err) => { + logs.push({ error: err, source: 'proxy response' }); }); req.pipe(proxyReq, { end: true }); @@ -49,6 +70,11 @@ exports.createProxyServer = function() { logRequest(logs, req); const [hostname, port] = req.url.split(':'); + + res.on('error', (err) => { + logs.push({ error: err, source: 'proxy response' }); + }); + const proxyReq = net.connect(port, hostname, () => { res.write( 'HTTP/1.1 200 Connection Established\r\n' + @@ -74,8 +100,46 @@ exports.createProxyServer = function() { return { proxy, logs }; }; -exports.checkProxiedRequest = async function(envExtension, expectation) { - const { spawnPromisified } = require('./'); +function spawnPromisified(...args) { + const { spawn } = require('child_process'); + let stderr = ''; + let stdout = ''; + + const child = spawn(...args); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (data) => { + console.error('[STDERR]', data); + stderr += data; + }); + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { + console.log('[STDOUT]', data); + stdout += data; + }); + + return new Promise((resolve, reject) => { + child.on('close', (code, signal) => { + console.log('[CLOSE]', code, signal); + resolve({ + code, + signal, + stderr, + stdout, + }); + }); + child.on('error', (code, signal) => { + console.log('[ERROR]', code, signal); + reject({ + code, + signal, + stderr, + stdout, + }); + }); + }); +} + +exports.checkProxiedFetch = async function(envExtension, expectation) { const fixtures = require('./fixtures'); const { code, signal, stdout, stderr } = await spawnPromisified( process.execPath, diff --git a/test/parallel/test-http-proxy-fetch.js b/test/parallel/test-http-proxy-fetch.js deleted file mode 100644 index 7e2f7c2eca5ee9..00000000000000 --- a/test/parallel/test-http-proxy-fetch.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const common = require('../common'); -const assert = require('assert'); -const { once } = require('events'); -const http = require('http'); -const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server'); - -(async () => { - // Start a server to process the final request. - const server = http.createServer(common.mustCall((req, res) => { - res.end('Hello world'); - }, 2)); - server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); - server.listen(0); - await once(server, 'listening'); - - // Start a minimal proxy server. - const { proxy, logs } = createProxyServer(); - proxy.listen(0); - await once(proxy, 'listening'); - - const serverHost = `localhost:${server.address().port}`; - - // FIXME(undici:4083): undici currently always tunnels the request over - // CONNECT if proxyTunnel is not explicitly set to false, but what we - // need is for it to be automatically false for HTTP requests to be - // consistent with curl. - const expectedLogs = [{ - method: 'CONNECT', - url: serverHost, - headers: { - 'connection': 'close', - 'host': serverHost, - 'proxy-connection': 'keep-alive' - } - }]; - - // Check upper-cased HTTPS_PROXY environment variable. - await checkProxiedRequest({ - NODE_USE_ENV_PROXY: 1, - FETCH_URL: `http://${serverHost}/test`, - HTTP_PROXY: `http://localhost:${proxy.address().port}`, - }, { - stdout: 'Hello world', - }); - assert.deepStrictEqual(logs, expectedLogs); - - // Check lower-cased https_proxy environment variable. - logs.splice(0, logs.length); - await checkProxiedRequest({ - NODE_USE_ENV_PROXY: 1, - FETCH_URL: `http://${serverHost}/test`, - http_proxy: `http://localhost:${proxy.address().port}`, - }, { - stdout: 'Hello world', - }); - assert.deepStrictEqual(logs, expectedLogs); - - proxy.close(); - server.close(); -})().then(common.mustCall()); diff --git a/test/parallel/test-https-proxy-fetch.js b/test/parallel/test-https-proxy-fetch.js deleted file mode 100644 index b4dab4e3eebb6b..00000000000000 --- a/test/parallel/test-https-proxy-fetch.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const fixtures = require('../common/fixtures'); -const assert = require('assert'); -const https = require('https'); -const { once } = require('events'); -const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server'); - -(async () => { - // Start a server to process the final request. - const server = https.createServer({ - cert: fixtures.readKey('agent8-cert.pem'), - key: fixtures.readKey('agent8-key.pem'), - }, common.mustCall((req, res) => { - res.end('Hello world'); - }, 2)); - server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); - server.listen(0); - await once(server, 'listening'); - - // Start a minimal proxy server. - const { proxy, logs } = createProxyServer(); - proxy.listen(0); - await once(proxy, 'listening'); - - const serverHost = `localhost:${server.address().port}`; - - const expectedLogs = [{ - method: 'CONNECT', - url: serverHost, - headers: { - 'connection': 'close', - 'host': serverHost, - 'proxy-connection': 'keep-alive' - } - }]; - - // Check upper-cased HTTPS_PROXY environment variable. - await checkProxiedRequest({ - NODE_USE_ENV_PROXY: 1, - FETCH_URL: `https://${serverHost}/test`, - HTTPS_PROXY: `http://localhost:${proxy.address().port}`, - NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), - }, { - stdout: 'Hello world', - }); - assert.deepStrictEqual(logs, expectedLogs); - - // Check lower-cased https_proxy environment variable. - logs.splice(0, logs.length); - await checkProxiedRequest({ - NODE_USE_ENV_PROXY: 1, - FETCH_URL: `https://${serverHost}/test`, - https_proxy: `http://localhost:${proxy.address().port}`, - NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), - }, { - stdout: 'Hello world', - }); - assert.deepStrictEqual(logs, expectedLogs); - - proxy.close(); - server.close(); -})().then(common.mustCall()); From fd6026eeb9abb3d920f7003ab348cdb0f84f2a74 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 2 Jul 2025 01:17:07 +0200 Subject: [PATCH 2/5] http,https: add built-in proxy support in http/https.request and Agent 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) --- doc/api/errors.md | 6 + doc/api/http.md | 114 ++++++++ doc/api/https.md | 8 + lib/_http_agent.js | 110 ++++++- lib/_http_client.js | 56 ++++ lib/https.js | 269 ++++++++++++++++-- lib/internal/errors.js | 1 + lib/internal/http.js | 173 +++++++++++ lib/internal/process/pre_execution.js | 5 +- ...-http-proxy-request-connection-refused.mjs | 39 +++ .../test-http-proxy-request-https-proxy.mjs | 54 ++++ .../test-http-proxy-request-invalid-url.mjs | 30 ++ .../test-http-proxy-request-max-sockets.mjs | 123 ++++++++ ...t-http-proxy-request-no-proxy-asterisk.mjs | 42 +++ ...est-http-proxy-request-no-proxy-domain.mjs | 81 ++++++ .../test-http-proxy-request-no-proxy-ip.mjs | 59 ++++ ...p-proxy-request-no-proxy-port-specific.mjs | 80 ++++++ .../test-http-proxy-request-no-proxy.mjs | 60 ++++ ...t-http-proxy-request-proxy-failure-500.mjs | 55 ++++ ...tp-proxy-request-proxy-failure-hang-up.mjs | 39 +++ ...t-http-proxy-request-socket-keep-alive.mjs | 102 +++++++ test/client-proxy/test-http-proxy-request.mjs | 92 ++++++ ...http-request-proxy-post-server-failure.mjs | 60 ++++ .../test-http-request-proxy-post.mjs | 62 ++++ .../test-https-proxy-request-auth-failure.mjs | 57 ++++ ...https-proxy-request-connection-refused.mjs | 45 +++ ...est-https-proxy-request-empty-response.mjs | 48 ++++ ...-https-proxy-request-handshake-failure.mjs | 54 ++++ .../test-https-proxy-request-https-proxy.mjs | 56 ++++ ...https-proxy-request-incomplete-headers.mjs | 50 ++++ .../test-https-proxy-request-invalid-url.mjs | 37 +++ ...https-proxy-request-malformed-response.mjs | 49 ++++ .../test-https-proxy-request-max-sockets.mjs | 114 ++++++++ .../test-https-proxy-request-no-proxy.mjs | 51 ++++ ...-https-proxy-request-proxy-failure-404.mjs | 49 ++++ ...-https-proxy-request-proxy-failure-500.mjs | 50 ++++ ...-https-proxy-request-proxy-failure-502.mjs | 48 ++++ ...ps-proxy-request-proxy-failure-hang-up.mjs | 48 ++++ ...s-proxy-request-server-failure-hang-up.mjs | 58 ++++ ...-https-proxy-request-socket-keep-alive.mjs | 95 +++++++ ...tps-proxy-request-tunnel-timeout-agent.mjs | 51 ++++ ...est-https-proxy-request-tunnel-timeout.mjs | 51 ++++ .../client-proxy/test-https-proxy-request.mjs | 103 +++++++ .../test-https-request-proxy-post.mjs | 71 +++++ test/common/proxy-server.js | 29 +- test/fixtures/post-resource-and-log.js | 50 ++++ test/fixtures/request-and-log.js | 55 ++++ 47 files changed, 3003 insertions(+), 36 deletions(-) create mode 100644 test/client-proxy/test-http-proxy-request-connection-refused.mjs create mode 100644 test/client-proxy/test-http-proxy-request-https-proxy.mjs create mode 100644 test/client-proxy/test-http-proxy-request-invalid-url.mjs create mode 100644 test/client-proxy/test-http-proxy-request-max-sockets.mjs create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy-asterisk.mjs create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy-domain.mjs create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy-ip.mjs create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy-port-specific.mjs create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy.mjs create mode 100644 test/client-proxy/test-http-proxy-request-proxy-failure-500.mjs create mode 100644 test/client-proxy/test-http-proxy-request-proxy-failure-hang-up.mjs create mode 100644 test/client-proxy/test-http-proxy-request-socket-keep-alive.mjs create mode 100644 test/client-proxy/test-http-proxy-request.mjs create mode 100644 test/client-proxy/test-http-request-proxy-post-server-failure.mjs create mode 100644 test/client-proxy/test-http-request-proxy-post.mjs create mode 100644 test/client-proxy/test-https-proxy-request-auth-failure.mjs create mode 100644 test/client-proxy/test-https-proxy-request-connection-refused.mjs create mode 100644 test/client-proxy/test-https-proxy-request-empty-response.mjs create mode 100644 test/client-proxy/test-https-proxy-request-handshake-failure.mjs create mode 100644 test/client-proxy/test-https-proxy-request-https-proxy.mjs create mode 100644 test/client-proxy/test-https-proxy-request-incomplete-headers.mjs create mode 100644 test/client-proxy/test-https-proxy-request-invalid-url.mjs create mode 100644 test/client-proxy/test-https-proxy-request-malformed-response.mjs create mode 100644 test/client-proxy/test-https-proxy-request-max-sockets.mjs create mode 100644 test/client-proxy/test-https-proxy-request-no-proxy.mjs create mode 100644 test/client-proxy/test-https-proxy-request-proxy-failure-404.mjs create mode 100644 test/client-proxy/test-https-proxy-request-proxy-failure-500.mjs create mode 100644 test/client-proxy/test-https-proxy-request-proxy-failure-502.mjs create mode 100644 test/client-proxy/test-https-proxy-request-proxy-failure-hang-up.mjs create mode 100644 test/client-proxy/test-https-proxy-request-server-failure-hang-up.mjs create mode 100644 test/client-proxy/test-https-proxy-request-socket-keep-alive.mjs create mode 100644 test/client-proxy/test-https-proxy-request-tunnel-timeout-agent.mjs create mode 100644 test/client-proxy/test-https-proxy-request-tunnel-timeout.mjs create mode 100644 test/client-proxy/test-https-proxy-request.mjs create mode 100644 test/client-proxy/test-https-request-proxy-post.mjs create mode 100644 test/fixtures/post-resource-and-log.js create mode 100644 test/fixtures/request-and-log.js diff --git a/doc/api/errors.md b/doc/api/errors.md index b06abbf8d96a2a..27e5e074ea4c87 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2489,6 +2489,12 @@ Accessing `Object.prototype.__proto__` has been forbidden using [`Object.setPrototypeOf`][] should be used to get and set the prototype of an object. + + +### `ERR_PROXY_TUNNEL` + +Failed to establish proxy tunnel when `NODE_USE_ENV_PROXY` is enabled. + ### `ERR_QUIC_APPLICATION_ERROR` diff --git a/doc/api/http.md b/doc/api/http.md index d104032e0f848c..81ebcd594ea8c8 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -116,6 +116,14 @@ http.get({ + +> Stability: 1.1 - Active development + +When Node.js creates the global agent, it checks the `NODE_USE_ENV_PROXY` +environment variable. If it is set to `1`, the global agent will be constructed +with `proxyEnv: process.env`, enabling proxy support based on the environment variables. + +Custom agents can also be created with proxy support by passing a +`proxyEnv` option when constructing the agent. The value can be `process.env` +if they just want to inherit the configuration from the environment variables, +or an object with specific setting overriding the environment. + +The following properties of the `proxyEnv` are checked to configure proxy +support. + +* `HTTP_PROXY` or `http_proxy`: Proxy server URL for HTTP requests. If both are set, + `http_proxy` takes precedence. +* `HTTPS_PROXY` or `https_proxy`: Proxy server URL for HTTPS requests. If both are set, + `https_proxy` takes precedence. +* `NO_PROXY` or `no_proxy`: Comma-separated list of hosts to bypass the proxy. If both are set, + `no_proxy` takes precedence. + +If the request is made to a Unix domain socket, the proxy settings will be ignored. + +### Proxy URL Format + +Proxy URLs can use either HTTP or HTTPS protocols: + +* HTTP proxy: `http://proxy.example.com:8080` +* HTTPS proxy: `https://proxy.example.com:8080` +* Proxy with authentication: `http://username:password@proxy.example.com:8080` + +### `NO_PROXY` Format + +The `NO_PROXY` environment variable supports several formats: + +* `*` - Bypass proxy for all hosts +* `example.com` - Exact host name match +* `.example.com` - Domain suffix match (matches `sub.example.com`) +* `*.example.com` - Wildcard domain match +* `192.168.1.100` - Exact IP address match +* `192.168.1.1-192.168.1.100` - IP address range +* `example.com:8080` - Hostname with specific port + +Multiple entries should be separated by commas. + +### Example + +Starting a Node.js process with proxy support enabled for all requests sent +through the default global agent: + +```console +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: + +```cjs +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: + +```cjs +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 }); +``` + +[Built-in Proxy Support]: #built-in-proxy-support [RFC 8187]: https://www.rfc-editor.org/rfc/rfc8187.txt [`'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`]: errors.md#err_http_content_length_mismatch [`'checkContinue'`]: #event-checkcontinue diff --git a/doc/api/https.md b/doc/api/https.md index f7e52ec99ab42a..06ce65867ad970 100644 --- a/doc/api/https.md +++ b/doc/api/https.md @@ -65,6 +65,14 @@ An [`Agent`][] object for HTTPS similar to [`http.Agent`][]. See