Skip to content

Commit e24eb6c

Browse files
committed
feat: use agent-base
This allows a single agent to serve as both the http and https agents. This PR also refactors some things based on feedback in #57.
1 parent 22ac4e5 commit e24eb6c

File tree

10 files changed

+325
-333
lines changed

10 files changed

+325
-333
lines changed

lib/agents.js

Lines changed: 157 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,203 @@
11
'use strict'
22

3-
const http = require('http')
4-
const https = require('https')
53
const net = require('net')
64
const tls = require('tls')
75
const { once } = require('events')
8-
const { createTimeout, abortRace, urlify, appendPort, cacheAgent } = require('./util')
6+
const timers = require('timers/promises')
7+
const { urlify, appendPort } = require('./util')
98
const { normalizeOptions, cacheOptions } = require('./options')
10-
const { getProxy, getProxyType, proxyCache } = require('./proxy.js')
9+
const { getProxy, getProxyAgent, proxyCache } = require('./proxy.js')
1110
const Errors = require('./errors.js')
11+
const { Agent: AgentBase } = require('agent-base')
1212

13-
const createAgent = (base, name) => {
14-
const SECURE = base === https
15-
const SOCKET_TYPE = SECURE ? tls : net
13+
module.exports = class Agent extends AgentBase {
14+
#options
15+
#timeouts
16+
#proxy
1617

17-
const agent = class extends base.Agent {
18-
#options
19-
#timeouts
20-
#proxy
21-
#socket
18+
constructor (options) {
19+
const { timeouts, proxy, noProxy, ...normalizedOptions } = normalizeOptions(options)
2220

23-
constructor (_options) {
24-
const { timeouts, proxy, noProxy, ...options } = normalizeOptions(_options)
21+
super(normalizedOptions)
2522

26-
super(options)
23+
this.#options = normalizedOptions
24+
this.#timeouts = timeouts
2725

28-
this.#options = options
29-
this.#timeouts = timeouts
30-
this.#proxy = proxy ? { proxies: getProxyType(proxy), proxy: urlify(proxy), noProxy } : null
26+
if (proxy) {
27+
this.#proxy = {
28+
proxy: urlify(proxy),
29+
noProxy,
30+
Agent: getProxyAgent(proxy),
31+
}
3132
}
33+
}
3234

33-
get proxy () {
34-
return this.#proxy ? { url: this.#proxy.proxy } : {}
35-
}
35+
get proxy () {
36+
return this.#proxy ? { url: this.#proxy.proxy } : {}
37+
}
3638

37-
#getProxy (options) {
38-
const proxy = this.#proxy
39-
? getProxy(appendPort(`${options.protocol}//${options.host}`, options.port), this.#proxy)
40-
: null
39+
#getProxy (options) {
40+
if (!this.#proxy) {
41+
return
42+
}
4143

42-
if (!proxy) {
43-
return
44-
}
44+
const proxy = getProxy(appendPort(`${options.protocol}//${options.host}`, options.port), {
45+
proxy: this.#proxy.proxy,
46+
noProxy: this.#proxy.noProxy,
47+
})
4548

46-
return cacheAgent({
47-
key: cacheOptions({
48-
...options,
49-
...this.#options,
50-
secure: SECURE,
51-
timeouts: this.#timeouts,
52-
proxy,
53-
}),
54-
cache: proxyCache,
55-
secure: SECURE,
56-
proxies: this.#proxy.proxies,
57-
}, proxy, this.#options)
49+
if (!proxy) {
50+
return
5851
}
5952

60-
#setKeepAlive (socket) {
61-
socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
62-
socket.setNoDelay(this.keepAlive)
53+
const cacheKey = cacheOptions({
54+
...options,
55+
...this.#options,
56+
timeouts: this.#timeouts,
57+
proxy,
58+
})
59+
60+
if (proxyCache.has(cacheKey)) {
61+
return proxyCache.get(cacheKey)
6362
}
6463

65-
#setIdleTimeout (socket, options) {
66-
if (this.#timeouts.idle) {
67-
socket.setTimeout(this.#timeouts.idle, () => {
68-
socket.destroy(new Errors.IdleTimeoutError(options))
69-
})
70-
}
64+
let { Agent: ProxyAgent } = this.#proxy
65+
if (Array.isArray(ProxyAgent)) {
66+
ProxyAgent = options.secureEndpoint ? ProxyAgent[1] : ProxyAgent[0]
7167
}
7268

73-
async #proxyConnect (proxy, request, options) {
74-
// socks-proxy-agent accepts a dns lookup function
75-
options.lookup ??= this.#options.lookup
69+
const proxyAgent = new ProxyAgent(proxy, this.#options)
70+
proxyCache.set(cacheKey, proxyAgent)
7671

77-
// all the proxy agents use this secureEndpoint option to determine
78-
// if the proxy should connect over tls or not. we can set it based
79-
// on if the HttpAgent or HttpsAgent is used.
80-
options.secureEndpoint = SECURE
72+
return proxyAgent
73+
}
8174

82-
const socket = await abortRace([
83-
(ac) => createTimeout(this.#timeouts.connection, ac).catch(() => {
75+
// takes an array of promises and races them against the connection timeout
76+
// which will throw the necessary error if it is hit. This will return the
77+
// result of the promise race.
78+
async #timeoutConnection ({ promises, options, timeout }, ac = new AbortController()) {
79+
if (timeout) {
80+
const connectionTimeout = timers.setTimeout(timeout, null, { signal: ac.signal })
81+
.then(() => {
8482
throw new Errors.ConnectionTimeoutError(options)
85-
}),
86-
(ac) => proxy.connect(request, options).then((s) => {
87-
this.#setKeepAlive(s)
88-
89-
const connectEvent = SECURE ? 'secureConnect' : 'connect'
90-
const connectingEvent = SECURE ? 'secureConnecting' : 'connecting'
91-
92-
if (!s[connectingEvent]) {
93-
return s
83+
}).catch((err) => {
84+
if (err.name === 'AbortError') {
85+
return
9486
}
87+
throw err
88+
})
89+
promises.push(connectionTimeout)
90+
}
9591

96-
return abortRace([
97-
() => once(s, 'error', ac).then((err) => {
98-
throw err
99-
}),
100-
() => once(s, connectEvent, ac).then(() => s),
101-
], ac)
102-
}),
103-
])
104-
105-
this.#setIdleTimeout(socket, options)
106-
107-
return socket
92+
let result
93+
try {
94+
result = await Promise.race(promises)
95+
ac.abort()
96+
} catch (err) {
97+
ac.abort()
98+
throw err
10899
}
100+
return result
101+
}
109102

110-
async connect (request, options) {
111-
const proxy = this.#getProxy(options)
112-
if (proxy) {
113-
return this.#proxyConnect(proxy, request, options)
103+
async connect (request, options) {
104+
// if the connection does not have its own lookup function
105+
// set, then use the one from our options
106+
options.lookup ??= this.#options.lookup
107+
108+
let socket
109+
let timeout = this.#timeouts.connection
110+
111+
const proxy = this.#getProxy(options)
112+
if (proxy) {
113+
// some of the proxies will wait for the socket to fully connect before
114+
// returning so we have to await this while also racing it against the
115+
// connection timeout.
116+
const start = Date.now()
117+
socket = await this.#timeoutConnection({
118+
options,
119+
timeout,
120+
promises: [proxy.connect(request, options)],
121+
})
122+
// see how much time proxy.connect took and subtract it from
123+
// the timeout
124+
if (timeout) {
125+
timeout = timeout - (Date.now() - start)
114126
}
127+
} else {
128+
socket = (options.secureEndpoint ? tls : net).connect(options)
129+
}
115130

116-
const socket = SOCKET_TYPE.connect(options)
131+
socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
132+
socket.setNoDelay(this.keepAlive)
117133

118-
this.#setKeepAlive(socket)
134+
const abortController = new AbortController()
135+
const { signal } = abortController
119136

120-
await abortRace([
121-
(s) => createTimeout(this.#timeouts.connection, s).catch(() => {
122-
throw new Errors.ConnectionTimeoutError(options)
123-
}),
124-
(s) => once(socket, 'error', s).then((err) => {
125-
throw err
126-
}),
127-
(s) => once(socket, 'connect', s),
128-
])
137+
const connectPromise = socket[options.secureEndpoint ? 'secureConnecting' : 'connecting']
138+
? once(socket, options.secureEndpoint ? 'secureConnect' : 'connect', { signal })
139+
: Promise.resolve()
129140

130-
this.#setIdleTimeout(socket, options)
141+
await this.#timeoutConnection({
142+
options,
143+
timeout,
144+
promises: [
145+
connectPromise,
146+
once(socket, 'error', { signal }).then((err) => {
147+
throw err[0]
148+
}),
149+
],
150+
}, abortController)
131151

132-
return socket
152+
if (this.#timeouts.idle) {
153+
socket.setTimeout(this.#timeouts.idle, () => {
154+
socket.destroy(new Errors.IdleTimeoutError(options))
155+
})
133156
}
134157

135-
addRequest (request, options) {
136-
const proxy = this.#getProxy(options)
137-
// it would be better to call proxy.addRequest here but this causes the
138-
// http-proxy-agent to call its super.addRequest which causes the request
139-
// to be added to the agent twice. since we only support 3 agents
140-
// currently (see the required agents in proxy.js) we have manually
141-
// checked that the only public methods we need to call are called in the
142-
// next block. this could change in the future and presumably we would get
143-
// failing tests until we have properly called the necessary methods on
144-
// each of our proxy agents
145-
if (proxy?.setRequestProps) {
146-
proxy.setRequestProps(request, options)
147-
}
148-
149-
request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
150-
151-
const responseTimeout = createTimeout(this.#timeouts.response)
152-
if (responseTimeout) {
153-
request.once('finish', () => {
154-
responseTimeout.start(() => {
155-
request.destroy(new Errors.ResponseTimeoutError(request, this.proxy?.url))
156-
})
157-
})
158-
request.once('response', () => {
159-
responseTimeout.clear()
160-
})
161-
}
162-
163-
const transferTimeout = createTimeout(this.#timeouts.transfer)
164-
if (transferTimeout) {
165-
request.once('response', (res) => {
166-
transferTimeout.start(() => {
167-
res.destroy(new Errors.TransferTimeoutError(request, this.proxy?.url))
168-
})
169-
res.once('close', () => {
170-
transferTimeout.clear()
171-
})
172-
})
173-
}
158+
return socket
159+
}
174160

175-
return super.addRequest(request, options)
161+
addRequest (request, options) {
162+
const proxy = this.#getProxy(options)
163+
// it would be better to call proxy.addRequest here but this causes the
164+
// http-proxy-agent to call its super.addRequest which causes the request
165+
// to be added to the agent twice. since we only support 3 agents
166+
// currently (see the required agents in proxy.js) we have manually
167+
// checked that the only public methods we need to call are called in the
168+
// next block. this could change in the future and presumably we would get
169+
// failing tests until we have properly called the necessary methods on
170+
// each of our proxy agents
171+
if (proxy?.setRequestProps) {
172+
proxy.setRequestProps(request, options)
176173
}
177174

178-
createSocket (req, options, cb) {
179-
return Promise.resolve()
180-
.then(() => this.connect(req, options))
181-
.then((socket) => {
182-
this.#socket = socket
183-
return super.createSocket(req, options, cb)
184-
}, cb)
175+
request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
176+
177+
if (this.#timeouts.response) {
178+
let responseTimeout
179+
request.once('finish', () => {
180+
setTimeout(() => {
181+
request.destroy(new Errors.ResponseTimeoutError(request, this.proxy?.url))
182+
}, this.#timeouts.response)
183+
})
184+
request.once('response', () => {
185+
clearTimeout(responseTimeout)
186+
})
185187
}
186188

187-
createConnection () {
188-
return this.#socket
189+
if (this.#timeouts.transfer) {
190+
let transferTimeout
191+
request.once('response', (res) => {
192+
setTimeout(() => {
193+
res.destroy(new Errors.TransferTimeoutError(request, this.proxy?.url))
194+
}, this.#timeouts.transfer)
195+
res.once('close', () => {
196+
clearTimeout(transferTimeout)
197+
})
198+
})
189199
}
190-
}
191200

192-
Object.defineProperty(agent, 'name', { value: name })
193-
return agent
194-
}
195-
196-
module.exports = {
197-
HttpAgent: createAgent(http, 'HttpAgent'),
198-
HttpsAgent: createAgent(https, 'HttpsAgent'),
201+
return super.addRequest(request, options)
202+
}
199203
}

0 commit comments

Comments
 (0)