Skip to content

Commit

Permalink
Add keepAlive: true support (#147)
Browse files Browse the repository at this point in the history
Enables `keepAlive: true` option for all proxy agent classes.
When set, the `Proxy-Connection: Keep-Alive` HTTP header
will be sent to HTTP proxies.
  • Loading branch information
TooTallNate committed May 3, 2023
1 parent a03e785 commit 4333067
Show file tree
Hide file tree
Showing 15 changed files with 529 additions and 312 deletions.
10 changes: 10 additions & 0 deletions .changeset/tidy-deers-glow.md
@@ -0,0 +1,10 @@
---
'https-proxy-agent': minor
'socks-proxy-agent': minor
'http-proxy-agent': minor
'pac-proxy-agent': minor
'proxy-agent': minor
'agent-base': minor
---

Add support for core `keepAlive: true`
14 changes: 9 additions & 5 deletions packages/agent-base/src/index.ts
Expand Up @@ -33,8 +33,12 @@ export abstract class Agent extends http.Agent {
_protocol?: string;
_currentSocket?: Duplex;

constructor() {
super();
// Set by `http.Agent` - missing from `@types/node`
options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;
keepAlive!: boolean;

constructor(opts?: http.AgentOptions) {
super(opts);
this._defaultPort = undefined;
this._protocol = undefined;
}
Expand All @@ -57,8 +61,8 @@ export abstract class Agent extends http.Agent {
.then(() => this.connect(req, o))
.then((socket) => {
if (socket instanceof http.Agent) {
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
return socket.createSocket(req, o, cb);
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
return socket.addRequest(req, o);
}
this._currentSocket = socket;
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
Expand All @@ -77,7 +81,7 @@ export abstract class Agent extends http.Agent {
if (typeof this._defaultPort === 'number') {
return this._defaultPort;
}
const port = isSecureEndpoint() ? 443 : 80;
const port = this.protocol === 'https:' ? 443 : 80;
return port;
}

Expand Down
177 changes: 135 additions & 42 deletions packages/agent-base/test/test.ts
@@ -1,39 +1,20 @@
import assert from 'assert';
import * as fs from 'fs';
import * as net from 'net';
import * as tls from 'tls';
import * as url from 'url';
import * as http from 'http';
import * as https from 'https';
import assert from 'assert';
import listen from 'async-listen';
import { Agent, AgentConnectOpts } from '../src';
import { once } from 'events';
import { listen } from 'async-listen';
import { Agent, AgentConnectOpts, req, json } from '../src';

const sleep = (n: number) => new Promise((r) => setTimeout(r, n));

const req = (opts: https.RequestOptions): Promise<http.IncomingMessage> =>
new Promise((resolve, reject) => {
(opts.protocol === 'https:' ? https : http)
.request(opts, resolve)
.once('error', reject)
.end();
});

const sslOptions = {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`),
};

function json(res: http.IncomingMessage): Promise<Record<string, string>> {
return new Promise((resolve) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (b) => {
data += b;
});
res.on('end', () => resolve(JSON.parse(data)));
});
}

describe('Agent (TypeScript)', () => {
describe('subclass', () => {
it('should be extendable (direct return)', () => {
Expand Down Expand Up @@ -91,8 +72,9 @@ describe('Agent (TypeScript)', () => {
const { port } = addr;

try {
const info = url.parse(`http://127.0.0.1:${port}/foo`);
const res = await req({ agent, ...info });
const res = await req(`http://127.0.0.1:${port}/foo`, {
agent,
});
assert.equal('bar', res.headers['x-foo']);
assert.equal('/foo', res.headers['x-url']);
assert(gotReq);
Expand Down Expand Up @@ -130,8 +112,9 @@ describe('Agent (TypeScript)', () => {
agent.defaultPort = port;

try {
const info = url.parse(`http://127.0.0.1:${port}/foo`);
const res = await req({ agent, ...info });
const res = await req(`http://127.0.0.1:${port}/foo`, {
agent,
});
const body = await json(res);
assert.equal(body.host, '127.0.0.1');
} finally {
Expand Down Expand Up @@ -172,8 +155,9 @@ describe('Agent (TypeScript)', () => {
const { port } = addr;

try {
const info = url.parse(`http://127.0.0.1:${port}/foo`);
const res = await req({ agent, ...info });
const res = await req(`http://127.0.0.1:${port}/foo`, {
agent,
});
assert.equal('bar', res.headers['x-foo']);
assert.equal('/foo', res.headers['x-url']);
assert(gotReq);
Expand All @@ -197,8 +181,7 @@ describe('Agent (TypeScript)', () => {
const agent = new MyAgent();

try {
const info = url.parse('http://127.0.0.1/throws');
await req({ agent, ...info });
await req('http://127.0.0.1/throws', { agent });
} catch (err: unknown) {
gotError = true;
assert.equal((err as Error).message, 'bad');
Expand All @@ -223,8 +206,7 @@ describe('Agent (TypeScript)', () => {
const agent = new MyAgent();

try {
const info = url.parse('http://127.0.0.1/throws');
await req({ agent, ...info });
await req('http://127.0.0.1/throws', { agent });
} catch (err: unknown) {
gotError = true;
assert.equal((err as Error).message, 'bad');
Expand All @@ -233,6 +215,72 @@ describe('Agent (TypeScript)', () => {
assert(gotError);
assert(gotCallback);
});

it('should support `keepAlive: true`', async () => {
let reqCount1 = 0;
let reqCount2 = 0;
let connectCount = 0;

class MyAgent extends Agent {
async connect(
_req: http.ClientRequest,
opts: AgentConnectOpts
) {
connectCount++;
assert(opts.secureEndpoint === false);
return net.connect(opts);
}
}
const agent = new MyAgent({ keepAlive: true });

const server1 = http.createServer((req, res) => {
expect(req.headers.connection).toEqual('keep-alive');
reqCount1++;
res.end();
});
const addr1 = (await listen(server1)) as URL;

const server2 = http.createServer((req, res) => {
expect(req.headers.connection).toEqual('keep-alive');
reqCount2++;
res.end();
});
const addr2 = (await listen(server2)) as URL;

try {
const res = await req(new URL('/foo', addr1), { agent });
expect(reqCount1).toEqual(1);
expect(connectCount).toEqual(1);
expect(res.headers.connection).toEqual('keep-alive');
res.resume();
const s1 = res.socket;

await once(s1, 'free');

const res2 = await req(new URL('/another', addr1), { agent });
expect(reqCount1).toEqual(2);
expect(connectCount).toEqual(1);
expect(res2.headers.connection).toEqual('keep-alive');
assert(res2.socket === s1);

res2.resume();
await once(res2.socket, 'free');

// This is a different host, so a new socket should be used
const res3 = await req(new URL('/another', addr2), { agent });
expect(reqCount2).toEqual(1);
expect(connectCount).toEqual(2);
expect(res3.headers.connection).toEqual('keep-alive');
assert(res3.socket !== s1);

res3.resume();
await once(res3.socket, 'free');
} finally {
agent.destroy();
server1.close();
server2.close();
}
});
});

describe('"https" module', () => {
Expand Down Expand Up @@ -268,11 +316,9 @@ describe('Agent (TypeScript)', () => {
const { port } = addr;

try {
const info = url.parse(`https://127.0.0.1:${port}/foo`);
const res = await req({
const res = await req(`https://127.0.0.1:${port}/foo`, {
agent,
rejectUnauthorized: false,
...info,
});
assert.equal('bar', res.headers['x-foo']);
assert.equal('/foo', res.headers['x-url']);
Expand Down Expand Up @@ -328,11 +374,9 @@ describe('Agent (TypeScript)', () => {
const { port } = addr;

try {
const info = url.parse(`https://127.0.0.1:${port}/foo`);
const res = await req({
const res = await req(`https://127.0.0.1:${port}/foo`, {
agent: agent1,
rejectUnauthorized: false,
...info,
});
assert.equal('bar', res.headers['x-foo']);
assert.equal('/foo', res.headers['x-url']);
Expand Down Expand Up @@ -376,11 +420,9 @@ describe('Agent (TypeScript)', () => {
agent.defaultPort = port;

try {
const info = url.parse(`https://127.0.0.1:${port}/foo`);
const res = await req({
const res = await req(`https://127.0.0.1:${port}/foo`, {
agent,
rejectUnauthorized: false,
...info,
});
const body = await json(res);
assert.equal(body.host, '127.0.0.1');
Expand All @@ -389,5 +431,56 @@ describe('Agent (TypeScript)', () => {
server.close();
}
});

it('should support `keepAlive: true`', async () => {
let gotReq = false;
let connectCount = 0;

class MyAgent extends Agent {
async connect(
_req: http.ClientRequest,
opts: AgentConnectOpts
) {
connectCount++;
assert(opts.secureEndpoint === true);
return tls.connect(opts);
}
}
const agent = new MyAgent({ keepAlive: true });

const server = https.createServer(sslOptions, (req, res) => {
gotReq = true;
res.end();
});
const addr = (await listen(server)) as URL;

try {
const res = await req(new URL('/foo', addr), {
agent,
rejectUnauthorized: false,
});
assert(gotReq);
expect(connectCount).toEqual(1);
expect(res.headers.connection).toEqual('keep-alive');
res.resume();
const s1 = res.socket;

await once(s1, 'free');

const res2 = await req(new URL('/another', addr), {
agent,
rejectUnauthorized: false,
});
expect(connectCount).toEqual(1);
expect(res2.headers.connection).toEqual('keep-alive');
assert(res2.socket === s1);

res2.resume();
await once(res2.socket, 'free');
} finally {
agent.destroy();
server.close();
}
});
});
});
13 changes: 11 additions & 2 deletions packages/http-proxy-agent/src/index.ts
Expand Up @@ -15,12 +15,14 @@ type ConnectOptsMap = {
https: Omit<tls.ConnectionOptions, 'host' | 'port'>;
};

export type HttpProxyAgentOptions<T> = {
type ConnectOpts<T> = {
[P in keyof ConnectOptsMap]: Protocol<T> extends P
? ConnectOptsMap[P]
: never;
}[keyof ConnectOptsMap];

export type HttpProxyAgentOptions<T> = ConnectOpts<T> & http.AgentOptions;

interface HttpProxyAgentClientRequest extends http.ClientRequest {
outputData?: {
data: string;
Expand Down Expand Up @@ -48,7 +50,7 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
}

constructor(proxy: Uri | URL, opts?: HttpProxyAgentOptions<Uri>) {
super();
super(opts);
this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy;
debug('Creating new HttpProxyAgent instance: %o', this.proxy.href);

Expand Down Expand Up @@ -99,6 +101,13 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
);
}

if (!req.hasHeader('proxy-connection')) {
req.setHeader(
'Proxy-Connection',
this.keepAlive ? 'Keep-Alive' : 'close'
);
}

// Create a socket connection to the proxy server.
let socket: net.Socket;
if (this.secureProxy) {
Expand Down

1 comment on commit 4333067

@vercel
Copy link

@vercel vercel bot commented on 4333067 May 3, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

proxy-agents – ./

proxy-agents.vercel.app
proxy-agents-git-main-tootallnate.vercel.app
proxy-agents-tootallnate.vercel.app

Please sign in to comment.