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.
Files changed (38) hide show
  1. package/docs/docs/api/Client.md +2 -0
  2. package/docs/docs/api/Dispatcher.md +2 -2
  3. package/docs/docs/best-practices/migrating-from-v7-to-v8.md +231 -0
  4. package/index.js +2 -2
  5. package/lib/core/util.js +1 -5
  6. package/lib/dispatcher/agent.js +1 -1
  7. package/lib/dispatcher/balanced-pool.js +0 -3
  8. package/lib/dispatcher/client.js +3 -2
  9. package/lib/dispatcher/dispatcher-base.js +22 -0
  10. package/lib/dispatcher/dispatcher1-wrapper.js +6 -0
  11. package/lib/dispatcher/h2c-client.js +1 -1
  12. package/lib/dispatcher/pool.js +1 -4
  13. package/lib/dispatcher/proxy-agent.js +4 -4
  14. package/lib/dispatcher/round-robin-pool.js +0 -3
  15. package/lib/dispatcher/socks5-proxy-agent.js +68 -62
  16. package/lib/handler/redirect-handler.js +1 -51
  17. package/lib/interceptor/decompress.js +1 -2
  18. package/lib/interceptor/dns.js +1 -1
  19. package/lib/util/cache.js +7 -6
  20. package/lib/util/runtime-features.js +3 -34
  21. package/lib/web/cache/cache.js +6 -8
  22. package/lib/web/fetch/body.js +1 -2
  23. package/lib/web/fetch/index.js +17 -9
  24. package/lib/web/fetch/util.js +4 -2
  25. package/lib/web/webidl/index.js +2 -4
  26. package/lib/web/websocket/permessage-deflate.js +13 -31
  27. package/lib/web/websocket/receiver.js +62 -22
  28. package/lib/web/websocket/stream/websocketstream.js +5 -6
  29. package/lib/web/websocket/websocket.js +6 -1
  30. package/package.json +3 -3
  31. package/types/agent.d.ts +0 -2
  32. package/types/client.d.ts +18 -12
  33. package/types/dispatcher.d.ts +0 -2
  34. package/types/h2c-client.d.ts +6 -6
  35. package/types/pool.d.ts +0 -2
  36. package/types/round-robin-pool.d.ts +0 -2
  37. package/types/webidl.d.ts +0 -1
  38. package/lib/util/promise.js +0 -28
@@ -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: 262144.
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 request's body exceeds this value then the connection will be closed. Default: `1048576`.
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
- ...opts,
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.prototype.hasOwnProperty.call(obj, Symbol.iterator)
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,
@@ -35,7 +35,7 @@ class Agent extends DispatcherBase {
35
35
  throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
36
36
  }
37
37
 
38
- super()
38
+ super(options)
39
39
 
40
40
  if (connect && typeof connect !== 'function') {
41
41
  connect = { ...connect }
@@ -57,9 +57,6 @@ class BalancedPool extends PoolBase {
57
57
  super()
58
58
 
59
59
  this[kOptions] = { ...util.deepClone(opts) }
60
- this[kOptions].interceptors = opts.interceptors
61
- ? { ...opts.interceptors }
62
- : undefined
63
60
  this[kIndex] = -1
64
61
  this[kCurrentWeight] = 0
65
62
 
@@ -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 { connect, maxConcurrentStreams, pipelining, ...opts } =
18
+ const { maxConcurrentStreams, pipelining, ...opts } =
19
19
  clientOpts ?? {}
20
20
  let defaultMaxConcurrentStreams = 100
21
21
  let defaultPipelining = 100
@@ -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 socket = await new Promise((resolve, reject) => {
83
- const onConnect = () => {
84
- socket.removeListener('error', onError)
85
- resolve(socket)
86
- }
82
+ const socketReady = Promise.withResolvers()
87
83
 
88
- const onError = (err) => {
89
- socket.removeListener('connect', onConnect)
90
- reject(err)
91
- }
84
+ const onSocketConnect = () => {
85
+ socket.removeListener('error', onSocketError)
86
+ socketReady.resolve(socket)
87
+ }
92
88
 
93
- const socket = net.connect({
94
- host: proxyHost,
95
- port: proxyPort
96
- })
89
+ const onSocketError = (err) => {
90
+ socket.removeListener('connect', onSocketConnect)
91
+ socketReady.reject(err)
92
+ }
97
93
 
98
- socket.once('connect', onConnect)
99
- socket.once('error', onError)
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
- await new Promise((resolve, reject) => {
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
- const onError = (err) => {
127
- clearTimeout(timeout)
128
- socks5Client.removeListener('authenticated', onAuthenticated)
129
- reject(err)
130
- }
119
+ const authenticationTimeout = setTimeout(() => {
120
+ authenticationReady.reject(new Error('SOCKS5 authentication timeout'))
121
+ }, 5000)
131
122
 
132
- // Check if already authenticated (for NO_AUTH method)
133
- if (socks5Client.state === 'authenticated') {
134
- clearTimeout(timeout)
135
- resolve()
136
- } else {
137
- socks5Client.once('authenticated', onAuthenticated)
138
- socks5Client.once('error', onError)
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
- await new Promise((resolve, reject) => {
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
- const onError = (err) => {
159
- clearTimeout(timeout)
160
- socks5Client.removeListener('connected', onConnected)
161
- reject(err)
162
- }
152
+ const connectionTimeout = setTimeout(() => {
153
+ connectionReady.reject(new Error('SOCKS5 connection timeout'))
154
+ }, 5000)
163
155
 
164
- socks5Client.once('connected', onConnected)
165
- socks5Client.once('error', onError)
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
- await new Promise((resolve, reject) => {
210
- finalSocket.once('secureConnect', resolve)
211
- finalSocket.once('error', reject)
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)