undici 8.0.2 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Client.md +2 -0
- package/docs/docs/api/Dispatcher.md +2 -2
- package/docs/docs/best-practices/migrating-from-v7-to-v8.md +231 -0
- package/index.js +2 -2
- package/lib/core/util.js +1 -5
- package/lib/dispatcher/agent.js +1 -1
- package/lib/dispatcher/balanced-pool.js +0 -3
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +22 -0
- package/lib/dispatcher/dispatcher1-wrapper.js +6 -0
- package/lib/dispatcher/h2c-client.js +1 -1
- package/lib/dispatcher/pool.js +1 -4
- package/lib/dispatcher/proxy-agent.js +4 -4
- package/lib/dispatcher/round-robin-pool.js +0 -3
- package/lib/dispatcher/socks5-proxy-agent.js +68 -62
- package/lib/handler/redirect-handler.js +1 -51
- package/lib/interceptor/decompress.js +1 -2
- package/lib/interceptor/dns.js +1 -1
- package/lib/util/cache.js +7 -6
- package/lib/util/runtime-features.js +3 -34
- package/lib/web/cache/cache.js +6 -8
- package/lib/web/fetch/body.js +1 -2
- package/lib/web/fetch/index.js +17 -9
- package/lib/web/fetch/util.js +4 -2
- package/lib/web/webidl/index.js +2 -4
- package/lib/web/websocket/permessage-deflate.js +13 -31
- package/lib/web/websocket/receiver.js +62 -22
- package/lib/web/websocket/stream/websocketstream.js +5 -6
- package/lib/web/websocket/websocket.js +6 -1
- package/package.json +3 -3
- package/types/agent.d.ts +0 -2
- package/types/client.d.ts +18 -12
- package/types/dispatcher.d.ts +0 -2
- package/types/h2c-client.d.ts +6 -6
- package/types/pool.d.ts +0 -2
- package/types/round-robin-pool.d.ts +0 -2
- package/types/webidl.d.ts +0 -1
- package/lib/util/promise.js +0 -28
package/docs/docs/api/Client.md
CHANGED
|
@@ -24,6 +24,8 @@ Returns: `Client`
|
|
|
24
24
|
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
|
25
25
|
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
|
26
26
|
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
27
|
+
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
28
|
+
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
|
27
29
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
|
28
30
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
29
31
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
@@ -533,7 +533,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
|
533
533
|
|
|
534
534
|
`body` contains the following additional extensions:
|
|
535
535
|
|
|
536
|
-
- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default:
|
|
536
|
+
- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 131072.
|
|
537
537
|
|
|
538
538
|
Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
|
|
539
539
|
|
|
@@ -1031,7 +1031,7 @@ const client = new Client("http://service.example").compose(
|
|
|
1031
1031
|
The `dump` interceptor enables you to dump the response body from a request upon a given limit.
|
|
1032
1032
|
|
|
1033
1033
|
**Options**
|
|
1034
|
-
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the
|
|
1034
|
+
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the response's body exceeds this value then the connection will be closed. Default: `1048576`.
|
|
1035
1035
|
|
|
1036
1036
|
> The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis.
|
|
1037
1037
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Migrating from Undici 7 to 8
|
|
2
|
+
|
|
3
|
+
This guide covers the changes you are most likely to hit when upgrading an
|
|
4
|
+
application or library from Undici v7 to v8.
|
|
5
|
+
|
|
6
|
+
## Before you upgrade
|
|
7
|
+
|
|
8
|
+
- Make sure your runtime is Node.js `>= 22.19.0`.
|
|
9
|
+
- If you have custom dispatchers, interceptors, or handlers, review the
|
|
10
|
+
handler API changes before updating.
|
|
11
|
+
- If you rely on HTTP/1.1-only behavior, plan to set `allowH2: false`
|
|
12
|
+
explicitly.
|
|
13
|
+
|
|
14
|
+
## 1. Update your Node.js version
|
|
15
|
+
|
|
16
|
+
Undici v8 requires Node.js `>= 22.19.0`.
|
|
17
|
+
|
|
18
|
+
If you are still on Node.js 20 or an older Node.js 22 release, upgrade Node.js
|
|
19
|
+
first:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node -v
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If that command prints a version lower than `v22.19.0`, upgrade Node.js before
|
|
26
|
+
installing Undici v8.
|
|
27
|
+
|
|
28
|
+
## 2. Migrate custom dispatcher handlers to the v2 API
|
|
29
|
+
|
|
30
|
+
Undici v8 uses the newer dispatcher handler API consistently.
|
|
31
|
+
|
|
32
|
+
If you implemented custom dispatchers, interceptors, or wrappers around
|
|
33
|
+
`dispatch()`, update legacy callbacks such as `onConnect`, `onHeaders`, and
|
|
34
|
+
`onComplete` to the newer callback names.
|
|
35
|
+
|
|
36
|
+
### Old handler callbacks vs. v8 callbacks
|
|
37
|
+
|
|
38
|
+
| Undici 7 style | Undici 8 style |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `onConnect(abort, context)` | `onRequestStart(controller, context)` |
|
|
41
|
+
| `onHeaders(statusCode, rawHeaders, resume, statusText)` | `onResponseStart(controller, statusCode, headers, statusText)` |
|
|
42
|
+
| `onData(chunk)` | `onResponseData(controller, chunk)` |
|
|
43
|
+
| `onComplete(trailers)` | `onResponseEnd(controller, trailers)` |
|
|
44
|
+
| `onError(err)` | `onResponseError(controller, err)` |
|
|
45
|
+
| `onUpgrade(statusCode, rawHeaders, socket)` | `onRequestUpgrade(controller, statusCode, headers, socket)` |
|
|
46
|
+
|
|
47
|
+
### Example
|
|
48
|
+
|
|
49
|
+
Before:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
client.dispatch(options, {
|
|
53
|
+
onConnect (abort) {
|
|
54
|
+
this.abort = abort
|
|
55
|
+
},
|
|
56
|
+
onHeaders (statusCode, headers, resume) {
|
|
57
|
+
this.resume = resume
|
|
58
|
+
return true
|
|
59
|
+
},
|
|
60
|
+
onData (chunk) {
|
|
61
|
+
chunks.push(chunk)
|
|
62
|
+
return true
|
|
63
|
+
},
|
|
64
|
+
onComplete (trailers) {
|
|
65
|
+
console.log(trailers)
|
|
66
|
+
},
|
|
67
|
+
onError (err) {
|
|
68
|
+
console.error(err)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
After:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
client.dispatch(options, {
|
|
77
|
+
onRequestStart (controller) {
|
|
78
|
+
this.controller = controller
|
|
79
|
+
},
|
|
80
|
+
onResponseStart (controller, statusCode, headers, statusText) {
|
|
81
|
+
console.log(statusCode, statusText, headers)
|
|
82
|
+
},
|
|
83
|
+
onResponseData (controller, chunk) {
|
|
84
|
+
chunks.push(chunk)
|
|
85
|
+
},
|
|
86
|
+
onResponseEnd (controller, trailers) {
|
|
87
|
+
console.log(trailers)
|
|
88
|
+
},
|
|
89
|
+
onResponseError (controller, err) {
|
|
90
|
+
console.error(err)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Pause, resume, and abort now go through the controller
|
|
96
|
+
|
|
97
|
+
In Undici v7, legacy handlers could return `false` or keep references to
|
|
98
|
+
`abort()` and `resume()` callbacks. In Undici v8, use the controller instead:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
onRequestStart (controller) {
|
|
102
|
+
this.controller = controller
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onResponseData (controller, chunk) {
|
|
106
|
+
controller.pause()
|
|
107
|
+
setImmediate(() => controller.resume())
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onResponseError (controller, err) {
|
|
111
|
+
controller.abort(err)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Raw headers and trailers moved to the controller
|
|
116
|
+
|
|
117
|
+
If you need the raw header arrays, read them from the controller:
|
|
118
|
+
|
|
119
|
+
- `controller.rawHeaders`
|
|
120
|
+
- `controller.rawTrailers`
|
|
121
|
+
|
|
122
|
+
## 3. Update `onBodySent()` handlers
|
|
123
|
+
|
|
124
|
+
If you implemented `onBodySent()`, note that its signature changed.
|
|
125
|
+
|
|
126
|
+
Before, handlers received counters:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
onBodySent (chunkSize, totalBytesSent) {}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In Undici v8, handlers receive the actual chunk:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
onBodySent (chunk) {}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If you need a notification that the whole body has been sent, use
|
|
139
|
+
`onRequestSent()`:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
onRequestSent () {
|
|
143
|
+
console.log('request body fully sent')
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 4. If you need HTTP/1.1 only, disable HTTP/2 explicitly
|
|
148
|
+
|
|
149
|
+
Undici v8 enables HTTP/2 by default when a TLS server negotiates it via ALPN.
|
|
150
|
+
|
|
151
|
+
If your application depends on HTTP/1.1-specific behavior, set `allowH2: false`
|
|
152
|
+
explicitly.
|
|
153
|
+
|
|
154
|
+
Before:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const client = new Client('https://example.com')
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
After, to keep HTTP/1.1 only:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const client = new Client('https://example.com', {
|
|
164
|
+
allowH2: false
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The same applies when you configure an `Agent`:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const agent = new Agent({
|
|
172
|
+
allowH2: false
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 5. Use real `Blob` and `File` instances
|
|
177
|
+
|
|
178
|
+
Undici v8 no longer accepts fake Blob-like values that only imitate `Blob` or
|
|
179
|
+
`File` via properties such as `Symbol.toStringTag`.
|
|
180
|
+
|
|
181
|
+
If you were passing custom objects that looked like `Blob`s, replace them with
|
|
182
|
+
actual `Blob` or `File` instances:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const body = new Blob(['hello'])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 6. Avoid depending on the internal global dispatcher symbol
|
|
189
|
+
|
|
190
|
+
`setGlobalDispatcher()` and `getGlobalDispatcher()` remain the public APIs and
|
|
191
|
+
should continue to be used.
|
|
192
|
+
|
|
193
|
+
Internally, Undici v8 stores its dispatcher under
|
|
194
|
+
`Symbol.for('undici.globalDispatcher.2')` and mirrors a v1-compatible wrapper
|
|
195
|
+
for legacy consumers such as Node.js built-in `fetch`.
|
|
196
|
+
|
|
197
|
+
If your code was reading or writing `Symbol.for('undici.globalDispatcher.1')`
|
|
198
|
+
directly, migrate to the public APIs instead:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { setGlobalDispatcher, getGlobalDispatcher, Agent } from 'undici'
|
|
202
|
+
|
|
203
|
+
setGlobalDispatcher(new Agent())
|
|
204
|
+
const dispatcher = getGlobalDispatcher()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
If you must expose a dispatcher to legacy v1 handler consumers, wrap it with
|
|
208
|
+
`Dispatcher1Wrapper`:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { Agent, Dispatcher1Wrapper } from 'undici'
|
|
212
|
+
|
|
213
|
+
const legacyCompatibleDispatcher = new Dispatcher1Wrapper(new Agent())
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 7. Verify the upgrade
|
|
217
|
+
|
|
218
|
+
After moving to Undici v8, it is worth checking these paths in your test suite:
|
|
219
|
+
|
|
220
|
+
- requests that use a custom `dispatcher`
|
|
221
|
+
- `setGlobalDispatcher()` behavior
|
|
222
|
+
- any custom interceptor or retry handler
|
|
223
|
+
- uploads that use `Blob`, `File`, or `FormData`
|
|
224
|
+
- integrations that depend on HTTP/1.1-only behavior
|
|
225
|
+
|
|
226
|
+
## Related documentation
|
|
227
|
+
|
|
228
|
+
- [Dispatcher](/docs/api/Dispatcher.md)
|
|
229
|
+
- [Client](/docs/api/Client.md)
|
|
230
|
+
- [Global Installation](/docs/api/GlobalInstallation.md)
|
|
231
|
+
- [Undici Module vs. Node.js Built-in Fetch](/docs/best-practices/undici-vs-builtin-fetch.md)
|
package/index.js
CHANGED
|
@@ -105,14 +105,14 @@ function makeDispatcher (fn) {
|
|
|
105
105
|
url = util.parseURL(url)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const { agent, dispatcher = getGlobalDispatcher() } = opts
|
|
108
|
+
const { agent, dispatcher = getGlobalDispatcher(), ...restOpts } = opts
|
|
109
109
|
|
|
110
110
|
if (agent) {
|
|
111
111
|
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return fn.call(dispatcher, {
|
|
115
|
-
...
|
|
115
|
+
...restOpts,
|
|
116
116
|
origin: url.origin,
|
|
117
117
|
path: url.search ? `${url.pathname}${url.search}` : url.pathname,
|
|
118
118
|
method: opts.method || (opts.body ? 'PUT' : 'GET')
|
package/lib/core/util.js
CHANGED
|
@@ -12,8 +12,6 @@ const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
|
|
12
12
|
const { headerNameLowerCasedRecord } = require('./constants')
|
|
13
13
|
const { tree } = require('./tree')
|
|
14
14
|
|
|
15
|
-
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v))
|
|
16
|
-
|
|
17
15
|
class BodyAsyncIterable {
|
|
18
16
|
constructor (body) {
|
|
19
17
|
this[kBody] = body
|
|
@@ -323,7 +321,7 @@ function isIterable (obj) {
|
|
|
323
321
|
*/
|
|
324
322
|
function hasSafeIterator (obj) {
|
|
325
323
|
const prototype = Object.getPrototypeOf(obj)
|
|
326
|
-
const ownIterator = Object.
|
|
324
|
+
const ownIterator = Object.hasOwn(obj, Symbol.iterator)
|
|
327
325
|
return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function')
|
|
328
326
|
}
|
|
329
327
|
|
|
@@ -989,8 +987,6 @@ module.exports = {
|
|
|
989
987
|
normalizedMethodRecords,
|
|
990
988
|
isValidPort,
|
|
991
989
|
isHttpOrHttpsPrefixed,
|
|
992
|
-
nodeMajor,
|
|
993
|
-
nodeMinor,
|
|
994
990
|
safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']),
|
|
995
991
|
wrapRequestBody,
|
|
996
992
|
setupConnectTimeout,
|
package/lib/dispatcher/agent.js
CHANGED
package/lib/dispatcher/client.js
CHANGED
|
@@ -114,7 +114,8 @@ class Client extends DispatcherBase {
|
|
|
114
114
|
useH2c,
|
|
115
115
|
initialWindowSize,
|
|
116
116
|
connectionWindowSize,
|
|
117
|
-
pingInterval
|
|
117
|
+
pingInterval,
|
|
118
|
+
webSocket
|
|
118
119
|
} = {}) {
|
|
119
120
|
if (keepAlive !== undefined) {
|
|
120
121
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -222,7 +223,7 @@ class Client extends DispatcherBase {
|
|
|
222
223
|
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
super()
|
|
226
|
+
super({ webSocket })
|
|
226
227
|
|
|
227
228
|
if (typeof connect !== 'function') {
|
|
228
229
|
connect = buildConnector({
|
|
@@ -10,6 +10,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
|
|
|
10
10
|
|
|
11
11
|
const kOnDestroyed = Symbol('onDestroyed')
|
|
12
12
|
const kOnClosed = Symbol('onClosed')
|
|
13
|
+
const kWebSocketOptions = Symbol('webSocketOptions')
|
|
13
14
|
|
|
14
15
|
class DispatcherBase extends Dispatcher {
|
|
15
16
|
/** @type {boolean} */
|
|
@@ -24,6 +25,23 @@ class DispatcherBase extends Dispatcher {
|
|
|
24
25
|
/** @type {Array<Function>|null} */
|
|
25
26
|
[kOnClosed] = null
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('../../types/dispatcher').DispatcherOptions} [opts]
|
|
30
|
+
*/
|
|
31
|
+
constructor (opts) {
|
|
32
|
+
super()
|
|
33
|
+
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @returns {import('../../types/dispatcher').WebSocketOptions}
|
|
38
|
+
*/
|
|
39
|
+
get webSocketOptions () {
|
|
40
|
+
return {
|
|
41
|
+
maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
/** @returns {boolean} */
|
|
28
46
|
get destroyed () {
|
|
29
47
|
return this[kDestroyed]
|
|
@@ -138,6 +156,10 @@ class DispatcherBase extends Dispatcher {
|
|
|
138
156
|
throw new InvalidArgumentError('opts must be an object.')
|
|
139
157
|
}
|
|
140
158
|
|
|
159
|
+
if (opts.dispatcher) {
|
|
160
|
+
throw new InvalidArgumentError('opts.dispatcher is not supported by instance methods. Pass opts.dispatcher to the top-level undici functions or call the dispatcher instance method directly.')
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
if (this[kDestroyed] || this[kOnDestroyed]) {
|
|
142
164
|
throw new ClientDestroyedError()
|
|
143
165
|
}
|
|
@@ -86,6 +86,12 @@ class Dispatcher1Wrapper extends Dispatcher {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
dispatch (opts, handler) {
|
|
89
|
+
// Legacy (v1) consumers do not support HTTP/2, so force HTTP/1.1.
|
|
90
|
+
// See https://github.com/nodejs/undici/issues/4989
|
|
91
|
+
if (opts.allowH2 !== false) {
|
|
92
|
+
opts = { ...opts, allowH2: false }
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
return this.#dispatcher.dispatch(opts, Dispatcher1Wrapper.wrapHandler(handler))
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -15,7 +15,7 @@ class H2CClient extends Client {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { maxConcurrentStreams, pipelining, ...opts } =
|
|
19
19
|
clientOpts ?? {}
|
|
20
20
|
let defaultMaxConcurrentStreams = 100
|
|
21
21
|
let defaultPipelining = 100
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -63,14 +63,11 @@ class Pool extends PoolBase {
|
|
|
63
63
|
})
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
super()
|
|
66
|
+
super(options)
|
|
67
67
|
|
|
68
68
|
this[kConnections] = connections || null
|
|
69
69
|
this[kUrl] = util.parseOrigin(origin)
|
|
70
70
|
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath }
|
|
71
|
-
this[kOptions].interceptors = options.interceptors
|
|
72
|
-
? { ...options.interceptors }
|
|
73
|
-
: undefined
|
|
74
71
|
this[kFactory] = factory
|
|
75
72
|
|
|
76
73
|
this.on('connect', (origin, targets) => {
|
|
@@ -104,7 +104,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
104
104
|
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const { proxyTunnel = true } = opts
|
|
107
|
+
const { proxyTunnel = true, connectTimeout } = opts
|
|
108
108
|
|
|
109
109
|
super()
|
|
110
110
|
|
|
@@ -128,9 +128,9 @@ class ProxyAgent extends DispatcherBase {
|
|
|
128
128
|
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const connect = buildConnector({ ...opts.proxyTls })
|
|
132
|
-
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
|
|
133
|
-
this[kConnectEndpointHTTP1] = buildConnector({ ...opts.requestTls, allowH2: false })
|
|
131
|
+
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls })
|
|
132
|
+
this[kConnectEndpoint] = buildConnector({ timeout: connectTimeout, ...opts.requestTls })
|
|
133
|
+
this[kConnectEndpointHTTP1] = buildConnector({ timeout: connectTimeout, ...opts.requestTls, allowH2: false })
|
|
134
134
|
|
|
135
135
|
const agentFactory = opts.factory || defaultAgentFactory
|
|
136
136
|
const factory = (origin, options) => {
|
|
@@ -69,9 +69,6 @@ class RoundRobinPool extends PoolBase {
|
|
|
69
69
|
this[kConnections] = connections || null
|
|
70
70
|
this[kUrl] = util.parseOrigin(origin)
|
|
71
71
|
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath }
|
|
72
|
-
this[kOptions].interceptors = options.interceptors
|
|
73
|
-
? { ...options.interceptors }
|
|
74
|
-
: undefined
|
|
75
72
|
this[kFactory] = factory
|
|
76
73
|
this[kIndex] = -1
|
|
77
74
|
|
|
@@ -79,26 +79,28 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
79
79
|
debug('creating SOCKS5 connection to', proxyHost, proxyPort)
|
|
80
80
|
|
|
81
81
|
// Connect to the SOCKS5 proxy
|
|
82
|
-
const
|
|
83
|
-
const onConnect = () => {
|
|
84
|
-
socket.removeListener('error', onError)
|
|
85
|
-
resolve(socket)
|
|
86
|
-
}
|
|
82
|
+
const socketReady = Promise.withResolvers()
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const onSocketConnect = () => {
|
|
85
|
+
socket.removeListener('error', onSocketError)
|
|
86
|
+
socketReady.resolve(socket)
|
|
87
|
+
}
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
const onSocketError = (err) => {
|
|
90
|
+
socket.removeListener('connect', onSocketConnect)
|
|
91
|
+
socketReady.reject(err)
|
|
92
|
+
}
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
const socket = net.connect({
|
|
95
|
+
host: proxyHost,
|
|
96
|
+
port: proxyPort
|
|
100
97
|
})
|
|
101
98
|
|
|
99
|
+
socket.once('connect', onSocketConnect)
|
|
100
|
+
socket.once('error', onSocketError)
|
|
101
|
+
|
|
102
|
+
await socketReady.promise
|
|
103
|
+
|
|
102
104
|
// Create SOCKS5 client
|
|
103
105
|
const socks5Client = new Socks5Client(socket, this[kProxyAuth])
|
|
104
106
|
|
|
@@ -112,58 +114,62 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
112
114
|
await socks5Client.handshake()
|
|
113
115
|
|
|
114
116
|
// Wait for authentication (if required)
|
|
115
|
-
|
|
116
|
-
const timeout = setTimeout(() => {
|
|
117
|
-
reject(new Error('SOCKS5 authentication timeout'))
|
|
118
|
-
}, 5000)
|
|
119
|
-
|
|
120
|
-
const onAuthenticated = () => {
|
|
121
|
-
clearTimeout(timeout)
|
|
122
|
-
socks5Client.removeListener('error', onError)
|
|
123
|
-
resolve()
|
|
124
|
-
}
|
|
117
|
+
const authenticationReady = Promise.withResolvers()
|
|
125
118
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
reject(err)
|
|
130
|
-
}
|
|
119
|
+
const authenticationTimeout = setTimeout(() => {
|
|
120
|
+
authenticationReady.reject(new Error('SOCKS5 authentication timeout'))
|
|
121
|
+
}, 5000)
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
123
|
+
const onAuthenticated = () => {
|
|
124
|
+
clearTimeout(authenticationTimeout)
|
|
125
|
+
socks5Client.removeListener('error', onAuthenticationError)
|
|
126
|
+
authenticationReady.resolve()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const onAuthenticationError = (err) => {
|
|
130
|
+
clearTimeout(authenticationTimeout)
|
|
131
|
+
socks5Client.removeListener('authenticated', onAuthenticated)
|
|
132
|
+
authenticationReady.reject(err)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if already authenticated (for NO_AUTH method)
|
|
136
|
+
if (socks5Client.state === 'authenticated') {
|
|
137
|
+
clearTimeout(authenticationTimeout)
|
|
138
|
+
authenticationReady.resolve()
|
|
139
|
+
} else {
|
|
140
|
+
socks5Client.once('authenticated', onAuthenticated)
|
|
141
|
+
socks5Client.once('error', onAuthenticationError)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await authenticationReady.promise
|
|
141
145
|
|
|
142
146
|
// Send CONNECT command
|
|
143
147
|
await socks5Client.connect(targetHost, targetPort)
|
|
144
148
|
|
|
145
149
|
// Wait for connection
|
|
146
|
-
|
|
147
|
-
const timeout = setTimeout(() => {
|
|
148
|
-
reject(new Error('SOCKS5 connection timeout'))
|
|
149
|
-
}, 5000)
|
|
150
|
-
|
|
151
|
-
const onConnected = (info) => {
|
|
152
|
-
debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info)
|
|
153
|
-
clearTimeout(timeout)
|
|
154
|
-
socks5Client.removeListener('error', onError)
|
|
155
|
-
resolve()
|
|
156
|
-
}
|
|
150
|
+
const connectionReady = Promise.withResolvers()
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
reject(err)
|
|
162
|
-
}
|
|
152
|
+
const connectionTimeout = setTimeout(() => {
|
|
153
|
+
connectionReady.reject(new Error('SOCKS5 connection timeout'))
|
|
154
|
+
}, 5000)
|
|
163
155
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
const onConnected = (info) => {
|
|
157
|
+
debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info)
|
|
158
|
+
clearTimeout(connectionTimeout)
|
|
159
|
+
socks5Client.removeListener('error', onConnectionError)
|
|
160
|
+
connectionReady.resolve()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const onConnectionError = (err) => {
|
|
164
|
+
clearTimeout(connectionTimeout)
|
|
165
|
+
socks5Client.removeListener('connected', onConnected)
|
|
166
|
+
connectionReady.reject(err)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
socks5Client.once('connected', onConnected)
|
|
170
|
+
socks5Client.once('error', onConnectionError)
|
|
171
|
+
|
|
172
|
+
await connectionReady.promise
|
|
167
173
|
|
|
168
174
|
return socket
|
|
169
175
|
}
|
|
@@ -206,10 +212,10 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
206
212
|
...connectOpts.tls || {}
|
|
207
213
|
})
|
|
208
214
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
const tlsReady = Promise.withResolvers()
|
|
216
|
+
finalSocket.once('secureConnect', tlsReady.resolve)
|
|
217
|
+
finalSocket.once('error', tlsReady.reject)
|
|
218
|
+
await tlsReady.promise
|
|
213
219
|
}
|
|
214
220
|
|
|
215
221
|
callback(null, finalSocket)
|