From df5244263866d13a0b99f75c7d522f53ea62251a Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 30 Mar 2025 14:45:18 +0200 Subject: [PATCH 1/3] feat(#4086): keep alive on CONNECT --- lib/dispatcher/client-h1.js | 2 +- test/proxy-agent.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 37fe94e2c4f..96a92aad85d 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -1101,7 +1101,7 @@ function writeH1 (client, request) { socket[kReset] = true } - if (upgrade || method === 'CONNECT') { + if (upgrade) { // On CONNECT or upgrade, block pipeline from dispatching further // requests on this connection. diff --git a/test/proxy-agent.js b/test/proxy-agent.js index a412b4adebf..c7a08873953 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -520,7 +520,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async const expectedProxyHeaders = { host: `localhost:${server.address().port}`, - connection: 'close' + connection: 'keep-alive' } proxy.on('connect', (req, res) => { From 3250144f0363cd5712ec835efdaec888e8b3c632 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 21 Apr 2025 12:54:18 +0200 Subject: [PATCH 2/3] Revert "feat(#4086): keep alive on CONNECT" This reverts commit df5244263866d13a0b99f75c7d522f53ea62251a. --- lib/dispatcher/client-h1.js | 2 +- test/proxy-agent.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 96a92aad85d..37fe94e2c4f 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -1101,7 +1101,7 @@ function writeH1 (client, request) { socket[kReset] = true } - if (upgrade) { + if (upgrade || method === 'CONNECT') { // On CONNECT or upgrade, block pipeline from dispatching further // requests on this connection. diff --git a/test/proxy-agent.js b/test/proxy-agent.js index c7a08873953..a412b4adebf 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -520,7 +520,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async const expectedProxyHeaders = { host: `localhost:${server.address().port}`, - connection: 'keep-alive' + connection: 'close' } proxy.on('connect', (req, res) => { From 24685ad281b5719f19dd1cb47dd756dff3ef0cc0 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 21 Apr 2025 13:11:44 +0200 Subject: [PATCH 3/3] feat: add proxy-connection header --- docs/docs/api/ProxyAgent.md | 2 ++ lib/dispatcher/proxy-agent.js | 3 ++- test/proxy-agent.js | 41 +++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/docs/api/ProxyAgent.md b/docs/docs/api/ProxyAgent.md index 932716ae795..49a0a643946 100644 --- a/docs/docs/api/ProxyAgent.md +++ b/docs/docs/api/ProxyAgent.md @@ -17,6 +17,8 @@ Returns: `ProxyAgent` Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions) > It ommits `AgentOptions#connect`. +> **Note:** When `AgentOptions#connections` is set, and different from `0`, the non-standard [`proxy-connection` header](https://udger.com/resources/http-request-headers-detail?header=Proxy-Connection) will be set to `keep-alive` in the request. + * **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string. If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org). For detailed information on the parsing process and potential validation errors, please refer to the ["Writing" section](https://url.spec.whatwg.org/#writing) of the WHATWG URL Specification. diff --git a/lib/dispatcher/proxy-agent.js b/lib/dispatcher/proxy-agent.js index c5b4d51babb..48a13f71267 100644 --- a/lib/dispatcher/proxy-agent.js +++ b/lib/dispatcher/proxy-agent.js @@ -75,7 +75,8 @@ class ProxyAgent extends DispatcherBase { signal: opts.signal, headers: { ...this[kProxyHeaders], - host: opts.host + host: opts.host, + ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {}) }, servername: this[kProxyTls]?.servername || proxyHostname }) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index a412b4adebf..4a294d79cc4 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -119,6 +119,46 @@ test('should accept string, URL and object as options', (t) => { t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' })) }) +test('use proxy-agent to connect through proxy (keep alive)', async (t) => { + t = tspl(t, { plan: 6 }) + const server = await buildServer() + const proxy = await buildProxy() + delete proxy.authenticate + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + const proxyAgent = new ProxyAgent({ + uri: proxyUrl + }) + const parsedOrigin = new URL(serverUrl) + + proxy.on('connect', (msg) => { + t.strictEqual(msg.headers['proxy-connection'], 'keep-alive') + }) + + server.on('request', (req, res) => { + t.strictEqual(req.url, '/') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) + }) + + const { + statusCode, + headers, + body + } = await request(serverUrl, { dispatcher: proxyAgent }) + const json = await body.json() + + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') + + server.close() + proxy.close() + proxyAgent.close() +}) + test('use proxy-agent to connect through proxy', async (t) => { t = tspl(t, { plan: 6 }) const server = await buildServer() @@ -519,6 +559,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async } const expectedProxyHeaders = { + 'proxy-connection': 'keep-alive', host: `localhost:${server.address().port}`, connection: 'close' }