undici 8.0.3 → 8.2.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/lib/api/api-connect.js +1 -1
- package/lib/api/api-pipeline.js +2 -2
- package/lib/api/api-request.js +2 -2
- package/lib/api/api-stream.js +1 -1
- package/lib/api/api-upgrade.js +8 -2
- package/lib/api/readable.js +3 -2
- package/lib/cache/memory-cache-store.js +1 -1
- package/lib/cache/sqlite-cache-store.js +6 -4
- package/lib/core/connect.js +16 -0
- package/lib/core/constants.js +1 -24
- package/lib/core/errors.js +2 -2
- package/lib/core/request.js +17 -2
- package/lib/core/socks5-client.js +24 -9
- package/lib/core/socks5-utils.js +32 -23
- package/lib/core/util.js +28 -3
- package/lib/dispatcher/agent.js +38 -40
- package/lib/dispatcher/balanced-pool.js +21 -23
- package/lib/dispatcher/client-h1.js +34 -16
- package/lib/dispatcher/client-h2.js +400 -147
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +18 -0
- package/lib/dispatcher/h2c-client.js +4 -4
- package/lib/dispatcher/pool-base.js +6 -6
- package/lib/dispatcher/pool.js +8 -3
- package/lib/dispatcher/proxy-agent.js +2 -0
- package/lib/dispatcher/round-robin-pool.js +5 -6
- package/lib/dispatcher/socks5-proxy-agent.js +23 -14
- package/lib/handler/cache-handler.js +1 -1
- package/lib/handler/redirect-handler.js +4 -0
- package/lib/interceptor/redirect.js +3 -3
- package/lib/llhttp/llhttp-wasm.js +1 -1
- package/lib/llhttp/llhttp_simd-wasm.js +1 -1
- package/lib/mock/mock-agent.js +8 -8
- package/lib/mock/mock-call-history.js +15 -15
- package/lib/util/cache.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/fetch/formdata-parser.js +17 -6
- package/lib/web/fetch/index.js +38 -28
- package/lib/web/webidl/index.js +5 -5
- package/lib/web/websocket/frame.js +1 -7
- package/lib/web/websocket/permessage-deflate.js +13 -31
- package/lib/web/websocket/receiver.js +62 -22
- package/lib/web/websocket/stream/websocketstream.js +6 -5
- package/lib/web/websocket/websocket.js +6 -1
- package/package.json +1 -1
- package/types/client.d.ts +11 -0
- package/types/dispatcher.d.ts +4 -4
- package/types/header.d.ts +5 -0
- package/types/interceptors.d.ts +1 -1
- package/types/proxy-agent.d.ts +2 -2
- package/types/socks5-proxy-agent.d.ts +2 -2
- package/lib/llhttp/.gitkeep +0 -0
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]
|
|
@@ -17,15 +17,15 @@ class H2CClient extends Client {
|
|
|
17
17
|
|
|
18
18
|
const { maxConcurrentStreams, pipelining, ...opts } =
|
|
19
19
|
clientOpts ?? {}
|
|
20
|
-
|
|
20
|
+
const defaultMaxConcurrentStreams = maxConcurrentStreams ?? 100
|
|
21
21
|
let defaultPipelining = 100
|
|
22
22
|
|
|
23
23
|
if (
|
|
24
24
|
maxConcurrentStreams != null &&
|
|
25
|
-
Number.isInteger(maxConcurrentStreams)
|
|
26
|
-
maxConcurrentStreams
|
|
25
|
+
(!Number.isInteger(maxConcurrentStreams) ||
|
|
26
|
+
maxConcurrentStreams < 1)
|
|
27
27
|
) {
|
|
28
|
-
|
|
28
|
+
throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (pipelining != null && Number.isInteger(pipelining) && pipelining > 0) {
|
|
@@ -189,12 +189,12 @@ class PoolBase extends DispatcherBase {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
[kRemoveClient] (client) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
})
|
|
192
|
+
const idx = this[kClients].indexOf(client)
|
|
193
|
+
if (idx !== -1) {
|
|
194
|
+
this[kClients].splice(idx, 1)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
client.close(() => {})
|
|
198
198
|
|
|
199
199
|
this[kNeedDrain] = this[kClients].some(dispatcher => (
|
|
200
200
|
!dispatcher[kNeedDrain] &&
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -36,6 +36,7 @@ class Pool extends PoolBase {
|
|
|
36
36
|
autoSelectFamily,
|
|
37
37
|
autoSelectFamilyAttemptTimeout,
|
|
38
38
|
allowH2,
|
|
39
|
+
useH2c,
|
|
39
40
|
clientTtl,
|
|
40
41
|
...options
|
|
41
42
|
} = {}) {
|
|
@@ -56,6 +57,7 @@ class Pool extends PoolBase {
|
|
|
56
57
|
...tls,
|
|
57
58
|
maxCachedSessions,
|
|
58
59
|
allowH2,
|
|
60
|
+
useH2c,
|
|
59
61
|
socketPath,
|
|
60
62
|
timeout: connectTimeout,
|
|
61
63
|
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
@@ -63,11 +65,11 @@ class Pool extends PoolBase {
|
|
|
63
65
|
})
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
super()
|
|
68
|
+
super(options)
|
|
67
69
|
|
|
68
70
|
this[kConnections] = connections || null
|
|
69
71
|
this[kUrl] = util.parseOrigin(origin)
|
|
70
|
-
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath }
|
|
72
|
+
this[kOptions] = { ...util.deepClone(options), connect, allowH2, useH2c, clientTtl, socketPath }
|
|
71
73
|
this[kFactory] = factory
|
|
72
74
|
|
|
73
75
|
this.on('connect', (origin, targets) => {
|
|
@@ -95,10 +97,13 @@ class Pool extends PoolBase {
|
|
|
95
97
|
|
|
96
98
|
[kGetDispatcher] () {
|
|
97
99
|
const clientTtlOption = this[kOptions].clientTtl
|
|
98
|
-
for (
|
|
100
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
101
|
+
const client = this[kClients][i]
|
|
102
|
+
|
|
99
103
|
// check ttl of client and if it's stale, remove it from the pool
|
|
100
104
|
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
|
|
101
105
|
this[kRemoveClient](client)
|
|
106
|
+
i--
|
|
102
107
|
} else if (!client[kNeedDrain]) {
|
|
103
108
|
return client
|
|
104
109
|
}
|
|
@@ -126,6 +126,8 @@ class ProxyAgent extends DispatcherBase {
|
|
|
126
126
|
this[kProxyHeaders]['proxy-authorization'] = opts.token
|
|
127
127
|
} else if (username && password) {
|
|
128
128
|
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
|
|
129
|
+
} else if (username) {
|
|
130
|
+
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:`).toString('base64')}`
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls })
|
|
@@ -92,10 +92,9 @@ class RoundRobinPool extends PoolBase {
|
|
|
92
92
|
|
|
93
93
|
[kGetDispatcher] () {
|
|
94
94
|
const clientTtlOption = this[kOptions].clientTtl
|
|
95
|
-
const clientsLength = this[kClients].length
|
|
96
95
|
|
|
97
96
|
// If we have no clients yet, create one
|
|
98
|
-
if (
|
|
97
|
+
if (this[kClients].length === 0) {
|
|
99
98
|
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
|
|
100
99
|
this[kAddClient](dispatcher)
|
|
101
100
|
return dispatcher
|
|
@@ -103,14 +102,14 @@ class RoundRobinPool extends PoolBase {
|
|
|
103
102
|
|
|
104
103
|
// Round-robin through existing clients
|
|
105
104
|
let checked = 0
|
|
106
|
-
while (checked <
|
|
107
|
-
this[kIndex] = (this[kIndex] + 1) %
|
|
105
|
+
while (checked < this[kClients].length) {
|
|
106
|
+
this[kIndex] = (this[kIndex] + 1) % this[kClients].length
|
|
108
107
|
const client = this[kClients][this[kIndex]]
|
|
109
108
|
|
|
110
109
|
// Check if client is stale (TTL expired)
|
|
111
110
|
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
|
|
112
111
|
this[kRemoveClient](client)
|
|
113
|
-
|
|
112
|
+
this[kIndex]--
|
|
114
113
|
continue
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -123,7 +122,7 @@ class RoundRobinPool extends PoolBase {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
// All clients are busy, create a new one if we haven't reached the limit
|
|
126
|
-
if (!this[kConnections] ||
|
|
125
|
+
if (!this[kConnections] || this[kClients].length < this[kConnections]) {
|
|
127
126
|
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
|
|
128
127
|
this[kAddClient](dispatcher)
|
|
129
128
|
return dispatcher
|
|
@@ -6,7 +6,7 @@ const { URL } = require('node:url')
|
|
|
6
6
|
let tls // include tls conditionally since it is not always available
|
|
7
7
|
const DispatcherBase = require('./dispatcher-base')
|
|
8
8
|
const { InvalidArgumentError } = require('../core/errors')
|
|
9
|
-
const { Socks5Client } = require('../core/socks5-client')
|
|
9
|
+
const { Socks5Client, STATES } = require('../core/socks5-client')
|
|
10
10
|
const { kDispatch, kClose, kDestroy } = require('../core/symbols')
|
|
11
11
|
const Pool = require('./pool')
|
|
12
12
|
const buildConnector = require('../core/connect')
|
|
@@ -17,7 +17,7 @@ const debug = debuglog('undici:socks5-proxy')
|
|
|
17
17
|
const kProxyUrl = Symbol('proxy url')
|
|
18
18
|
const kProxyHeaders = Symbol('proxy headers')
|
|
19
19
|
const kProxyAuth = Symbol('proxy auth')
|
|
20
|
-
const
|
|
20
|
+
const kPools = Symbol('pools')
|
|
21
21
|
const kConnector = Symbol('connector')
|
|
22
22
|
|
|
23
23
|
// Static flag to ensure warning is only emitted once per process
|
|
@@ -65,8 +65,8 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
65
65
|
servername: options.proxyTls?.servername || url.hostname
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
this[
|
|
68
|
+
// Pools for the actual HTTP connections (with SOCKS5 tunnel connect function), keyed by origin
|
|
69
|
+
this[kPools] = new Map()
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -133,7 +133,7 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Check if already authenticated (for NO_AUTH method)
|
|
136
|
-
if (socks5Client.state ===
|
|
136
|
+
if (socks5Client.state === STATES.AUTHENTICATED) {
|
|
137
137
|
clearTimeout(authenticationTimeout)
|
|
138
138
|
authenticationReady.resolve()
|
|
139
139
|
} else {
|
|
@@ -183,9 +183,11 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
183
183
|
debug('dispatching request to', origin, 'via SOCKS5')
|
|
184
184
|
|
|
185
185
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const originKey = String(origin)
|
|
187
|
+
let pool = this[kPools].get(originKey)
|
|
188
|
+
// Create a Pool per origin so requests are not routed to the wrong host
|
|
189
|
+
if (!pool || pool.destroyed || pool.closed) {
|
|
190
|
+
pool = new Pool(origin, {
|
|
189
191
|
pipelining: opts.pipelining,
|
|
190
192
|
connections: opts.connections,
|
|
191
193
|
connect: async (connectOpts, callback) => {
|
|
@@ -225,10 +227,11 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
})
|
|
230
|
+
this[kPools].set(originKey, pool)
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
// Dispatch the request through the pool
|
|
231
|
-
return
|
|
233
|
+
// Dispatch the request through the per-origin pool
|
|
234
|
+
return pool[kDispatch](opts, handler)
|
|
232
235
|
} catch (err) {
|
|
233
236
|
debug('dispatch error:', err)
|
|
234
237
|
if (typeof handler.onError === 'function') {
|
|
@@ -240,15 +243,21 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
async [kClose] () {
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
const closePromises = []
|
|
247
|
+
for (const pool of this[kPools].values()) {
|
|
248
|
+
closePromises.push(pool.close())
|
|
245
249
|
}
|
|
250
|
+
this[kPools].clear()
|
|
251
|
+
await Promise.all(closePromises)
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
async [kDestroy] (err) {
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
const destroyPromises = []
|
|
256
|
+
for (const pool of this[kPools].values()) {
|
|
257
|
+
destroyPromises.push(pool.destroy(err))
|
|
251
258
|
}
|
|
259
|
+
this[kPools].clear()
|
|
260
|
+
await Promise.all(destroyPromises)
|
|
252
261
|
}
|
|
253
262
|
}
|
|
254
263
|
|
|
@@ -479,7 +479,7 @@ function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheC
|
|
|
479
479
|
|
|
480
480
|
if (cacheControlDirectives.immutable) {
|
|
481
481
|
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
482
|
-
return
|
|
482
|
+
return 31536000000
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
return undefined
|
|
@@ -23,6 +23,10 @@ class RedirectHandler {
|
|
|
23
23
|
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
if (opts.throwOnMaxRedirect != null && typeof opts.throwOnMaxRedirect !== 'boolean') {
|
|
27
|
+
throw new InvalidArgumentError('throwOnMaxRedirect must be a boolean')
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
this.dispatch = dispatch
|
|
27
31
|
this.location = null
|
|
28
32
|
const { maxRedirections: _, ...cleanOpts } = opts
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const RedirectHandler = require('../handler/redirect-handler')
|
|
4
4
|
|
|
5
|
-
function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } = {}) {
|
|
5
|
+
function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections, throwOnMaxRedirect: defaultThrowOnMaxRedirect } = {}) {
|
|
6
6
|
return (dispatch) => {
|
|
7
7
|
return function Intercept (opts, handler) {
|
|
8
|
-
const { maxRedirections = defaultMaxRedirections, ...rest } = opts
|
|
8
|
+
const { maxRedirections = defaultMaxRedirections, throwOnMaxRedirect = defaultThrowOnMaxRedirect, ...rest } = opts
|
|
9
9
|
|
|
10
10
|
if (maxRedirections == null || maxRedirections === 0) {
|
|
11
11
|
return dispatch(opts, handler)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting.
|
|
14
|
+
const dispatchOpts = { ...rest, throwOnMaxRedirect } // Stop sub dispatcher from also redirecting.
|
|
15
15
|
const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
|
|
16
16
|
return dispatch(dispatchOpts, redirectHandler)
|
|
17
17
|
}
|