Skip to content
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

Fix keepAlive for http-proxy-agent #169

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-parrots-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'http-proxy-agent': patch
---

Fixed the keepAlive option for HttpProxyAgent
2 changes: 2 additions & 0 deletions packages/agent-base/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ describe('Agent (TypeScript)', () => {
assert(gotReq);
expect(connectCount).toEqual(1);
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;

Expand All @@ -473,6 +474,7 @@ describe('Agent (TypeScript)', () => {
});
expect(connectCount).toEqual(1);
expect(res2.headers.connection).toEqual('keep-alive');
expect(res2.statusCode).toEqual(200);
assert(res2.socket === s1);

res2.resume();
Expand Down
36 changes: 29 additions & 7 deletions packages/http-proxy-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface HttpProxyAgentClientRequest extends http.ClientRequest {
}[];
_header?: string | null;
_implicitHeader(): void;
_updatedPropsForProxyAgent?: boolean;
}

function isHTTPS(protocol?: string | null): boolean {
Expand Down Expand Up @@ -71,15 +72,12 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
};
}

async connect(
setRequestProps(
req: HttpProxyAgentClientRequest,
opts: AgentConnectOpts
): Promise<net.Socket> {
const { proxy } = this;

const protocol = opts.secureEndpoint ? 'https:' : 'http:';
): void {
const hostname = req.getHeader('host') || 'localhost';
const base = `${protocol}//${hostname}`;
const base = `http://${hostname}`;
Comment on lines -82 to +80
Copy link
Contributor Author

@jportner jportner May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed redundant to have const protocol = opts.secureEndpoint ? 'https:' : 'http:'; and use that to generate the URL, when HttpProxyAgent should only be used for HTTP URLs. In any case, the secureEndpoint option is always undefined Agent.addRequest() (except for when ProxyAgent is used).

I simplified it here by hard-coding the protocol, I think that's OK.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to do this, then the function should throw an error when opts.secureEndpoint === true. However, I think it is valid to do HTTPS requests (though less ideal, especially over an external network). I believe squid works when passing an https URL for example.

const url = new URL(req.path, base);
if (opts.port !== 80) {
url.port = String(opts.port);
Expand All @@ -89,6 +87,8 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
// to the absolute path of the URL that will be requested.
req.path = String(url);

const { proxy } = this;

// Inject the `Proxy-Authorization` header if necessary.
req._header = null;
if (proxy.username || proxy.password) {
Expand All @@ -108,6 +108,26 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
);
}

req._updatedPropsForProxyAgent = true;
}

addRequest(req: HttpProxyAgentClientRequest, opts: AgentConnectOpts): void {
this.setRequestProps(req, opts);
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
super.addRequest(req, opts);
}

async connect(
req: HttpProxyAgentClientRequest,
opts: AgentConnectOpts
): Promise<net.Socket> {
if (!req._updatedPropsForProxyAgent) {
// Maintain backwards compatibility with ProxyAgent and
// PacProxyAgent, which both rely on HttpProxyAgent to set request
// props inside the connect() function.
this.setRequestProps(req, opts);
}
Comment on lines +114 to +129
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to work in a fully backwards-compatible way.
ProxyAgent and PacProxyAgent still function and all tests pass.


// Create a socket connection to the proxy server.
let socket: net.Socket;
if (this.secureProxy) {
Expand All @@ -124,7 +144,9 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
let first: string;
let endOfHeaders: number;
debug('Regenerating stored HTTP header string for request');
req._implicitHeader();
if (!req._header) {
req._implicitHeader();
}
Comment on lines -127 to +149
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add this conditional because now that we are setting the headers before addRequest(), it causes req._header to be non-null here, which will cause req._implicitHeader() to throw an exception.

Alternatively, I could do just allow connect() to rewrite the request URL and set headers, even if it's already happened in addRequest():

diff --git a/packages/http-proxy-agent/src/index.ts b/packages/http-proxy-agent/src/index.ts
index 16d5b51..807e4c5 100644
--- a/packages/http-proxy-agent/src/index.ts
+++ b/packages/http-proxy-agent/src/index.ts
@@ -29,7 +29,6 @@ interface HttpProxyAgentClientRequest extends http.ClientRequest {
 	}[];
 	_header?: string | null;
 	_implicitHeader(): void;
-	_updatedPropsForProxyAgent?: boolean;
 }
 
 function isHTTPS(protocol?: string | null): boolean {
@@ -104,8 +103,6 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
 				this.keepAlive ? 'Keep-Alive' : 'close'
 			);
 		}
-
-		req._updatedPropsForProxyAgent = true;
 	}
 
 	addRequest(req: HttpProxyAgentClientRequest, opts: AgentConnectOpts): void {
@@ -118,12 +115,10 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
 		req: HttpProxyAgentClientRequest,
 		opts: AgentConnectOpts
 	): Promise<net.Socket> {
-		if (!req._updatedPropsForProxyAgent) {
-			// Maintain backwards compatibility with ProxyAgent and
-			// PacProxyAgent, which both rely on HttpProxyAgent to set request
-			// props inside the connect() function.
-			this.setRequestProps(req, opts);
-		}
+		// Maintain backwards compatibility with ProxyAgent and
+		// PacProxyAgent, which both rely on HttpProxyAgent to set request
+		// props inside the connect() function.
+		this.setRequestProps(req, opts);
 
 		// Create a socket connection to the proxy server.
 		let socket: net.Socket;
@@ -141,9 +136,7 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
 		let first: string;
 		let endOfHeaders: number;
 		debug('Regenerating stored HTTP header string for request');
-		if (!req._header) {
-			req._implicitHeader();
-		}
+		req._implicitHeader();
 		if (req.outputData && req.outputData.length > 0) {
 			// Node >= 12
 			debug(

What do you think?

if (req.outputData && req.outputData.length > 0) {
// Node >= 12
debug(
Expand Down
28 changes: 28 additions & 0 deletions packages/http-proxy-agent/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import assert from 'assert';
import { once } from 'events';
import { createProxy, ProxyServer } from 'proxy';
import { listen } from 'async-listen';
import { json, req } from 'agent-base';
Expand Down Expand Up @@ -187,5 +188,32 @@ describe('HttpProxyAgent', () => {
const body = await json(res);
expect(body.host).toEqual(httpServerUrl.hostname);
});

it('should work with `keepAlive: true`', async () => {
httpServer.on('request', (req, res) => {
res.end(JSON.stringify(req.headers));
});

const agent = new HttpProxyAgent(proxyUrl, { keepAlive: true });

try {
const res = await req(httpServerUrl, { agent });
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;
await once(s1, 'free');

const res2 = await req(httpServerUrl, { agent });
expect(res2.headers.connection).toEqual('keep-alive');
expect(res2.statusCode).toEqual(200);
res2.resume();
const s2 = res2.socket;
assert(s1 === s2);
await once(s2, 'free');
} finally {
agent.destroy();
}
});
});
});
4 changes: 4 additions & 0 deletions packages/https-proxy-agent/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,14 @@ describe('HttpsProxyAgent', () => {
try {
const res = await req(serverUrl, { agent });
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;
await once(s1, 'free');

const res2 = await req(serverUrl, { agent });
expect(res2.headers.connection).toEqual('keep-alive');
expect(res2.statusCode).toEqual(200);
res2.resume();
const s2 = res2.socket;
assert(s1 === s2);
Expand Down Expand Up @@ -332,6 +334,7 @@ describe('HttpsProxyAgent', () => {
rejectUnauthorized: false,
});
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;
await once(s1, 'free');
Expand All @@ -341,6 +344,7 @@ describe('HttpsProxyAgent', () => {
rejectUnauthorized: false,
});
expect(res2.headers.connection).toEqual('keep-alive');
expect(res2.statusCode).toEqual(200);
res2.resume();
const s2 = res2.socket;
assert(s1 === s2);
Expand Down
80 changes: 80 additions & 0 deletions packages/pac-proxy-agent/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { listen } from 'async-listen';
import { ProxyServer, createProxy } from 'proxy';
import { req, json } from 'agent-base';
import { PacProxyAgent } from '../src';
import { once } from 'events';

const sslOptions = {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
Expand Down Expand Up @@ -244,6 +245,44 @@ describe('PacProxyAgent', () => {
assert.equal(httpServerUrl.host, data.host);
assert(gotReq);
}, 10000); // This test is slow on Windows :/

it('should work with `keepAlive: true`', async () => {
httpServer.on('request', function (req, res) {
res.end(JSON.stringify(req.headers));
});

function FindProxyForURL() {
return 'PROXY localhost:PORT;';
}

const uri = `data:,${encodeURIComponent(
FindProxyForURL.toString().replace('PORT', proxyServerUrl.port)
)}`;
const agent = new PacProxyAgent(uri, { keepAlive: true });

try {
const res = await req(new URL('/test', httpServerUrl), {
agent,
});
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;
await once(s1, 'free');

const res2 = await req(new URL('/test', httpServerUrl), {
agent,
});
expect(res2.headers.connection).toEqual('keep-alive');
// expect(res2.statusCode).toEqual(200); // TODO: this assertion will cause this test to fail; uncomment in a follow-on PR to fix keepAlive for PacProxyAgent+HTTP
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the new tests and test assertions intact.
I simply commented out the assertions that are currently failing.

res2.resume();
const s2 = res2.socket;
assert(s1 === s2);
await once(s2, 'free');
} finally {
agent.destroy();
}
});
});

describe('"https" module', () => {
Expand Down Expand Up @@ -358,5 +397,46 @@ describe('PacProxyAgent', () => {
assert.equal(proxyCount, 4);
assert(gotReq);
}, 10000); // This test is slow on Windows :/

it('should work with `keepAlive: true`', async () => {
httpsServer.on('request', function (req, res) {
res.end(JSON.stringify(req.headers));
});

function FindProxyForURL() {
return 'PROXY localhost:PORT;';
}

const uri = `data:,${encodeURIComponent(
FindProxyForURL.toString().replace('PORT', proxyServerUrl.port)
)}`;
const agent = new PacProxyAgent(uri, {
keepAlive: true,
rejectUnauthorized: false,
});

try {
const res = await req(new URL('/test', httpsServerUrl), {
agent,
});
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
res.resume();
const s1 = res.socket;
await once(s1, 'free');

const res2 = await req(new URL('/test', httpsServerUrl), {
agent,
});
expect(res2.headers.connection).toEqual('keep-alive');
expect(res2.statusCode).toEqual(200);
res2.resume();
const s2 = res2.socket;
assert(s1 === s2);
await once(s2, 'free');
} finally {
agent.destroy();
}
});
});
});
2 changes: 2 additions & 0 deletions packages/proxy-agent/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ describe('ProxyAgent', () => {
});
res.resume();
expect(res.headers.connection).toEqual('keep-alive');
expect(res.statusCode).toEqual(200);
const s1 = res.socket;
await once(s1, 'free');

Expand All @@ -166,6 +167,7 @@ describe('ProxyAgent', () => {
});
res2.resume();
expect(res2.headers.connection).toEqual('keep-alive');
// expect(res2.statusCode).toEqual(200); // TODO: this assertion will cause this test to fail; uncomment in a follow-on PR to fix keepAlive for ProxyAgent+HTTP
const s2 = res2.socket;
assert(s1 === s2);

Expand Down