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.
Files changed (54) hide show
  1. package/docs/docs/api/Client.md +2 -0
  2. package/docs/docs/api/Dispatcher.md +2 -2
  3. package/lib/api/api-connect.js +1 -1
  4. package/lib/api/api-pipeline.js +2 -2
  5. package/lib/api/api-request.js +2 -2
  6. package/lib/api/api-stream.js +1 -1
  7. package/lib/api/api-upgrade.js +8 -2
  8. package/lib/api/readable.js +3 -2
  9. package/lib/cache/memory-cache-store.js +1 -1
  10. package/lib/cache/sqlite-cache-store.js +6 -4
  11. package/lib/core/connect.js +16 -0
  12. package/lib/core/constants.js +1 -24
  13. package/lib/core/errors.js +2 -2
  14. package/lib/core/request.js +17 -2
  15. package/lib/core/socks5-client.js +24 -9
  16. package/lib/core/socks5-utils.js +32 -23
  17. package/lib/core/util.js +28 -3
  18. package/lib/dispatcher/agent.js +38 -40
  19. package/lib/dispatcher/balanced-pool.js +21 -23
  20. package/lib/dispatcher/client-h1.js +34 -16
  21. package/lib/dispatcher/client-h2.js +400 -147
  22. package/lib/dispatcher/client.js +3 -2
  23. package/lib/dispatcher/dispatcher-base.js +18 -0
  24. package/lib/dispatcher/h2c-client.js +4 -4
  25. package/lib/dispatcher/pool-base.js +6 -6
  26. package/lib/dispatcher/pool.js +8 -3
  27. package/lib/dispatcher/proxy-agent.js +2 -0
  28. package/lib/dispatcher/round-robin-pool.js +5 -6
  29. package/lib/dispatcher/socks5-proxy-agent.js +23 -14
  30. package/lib/handler/cache-handler.js +1 -1
  31. package/lib/handler/redirect-handler.js +4 -0
  32. package/lib/interceptor/redirect.js +3 -3
  33. package/lib/llhttp/llhttp-wasm.js +1 -1
  34. package/lib/llhttp/llhttp_simd-wasm.js +1 -1
  35. package/lib/mock/mock-agent.js +8 -8
  36. package/lib/mock/mock-call-history.js +15 -15
  37. package/lib/util/cache.js +1 -1
  38. package/lib/web/eventsource/eventsource-stream.js +245 -150
  39. package/lib/web/fetch/formdata-parser.js +17 -6
  40. package/lib/web/fetch/index.js +38 -28
  41. package/lib/web/webidl/index.js +5 -5
  42. package/lib/web/websocket/frame.js +1 -7
  43. package/lib/web/websocket/permessage-deflate.js +13 -31
  44. package/lib/web/websocket/receiver.js +62 -22
  45. package/lib/web/websocket/stream/websocketstream.js +6 -5
  46. package/lib/web/websocket/websocket.js +6 -1
  47. package/package.json +1 -1
  48. package/types/client.d.ts +11 -0
  49. package/types/dispatcher.d.ts +4 -4
  50. package/types/header.d.ts +5 -0
  51. package/types/interceptors.d.ts +1 -1
  52. package/types/proxy-agent.d.ts +2 -2
  53. package/types/socks5-proxy-agent.d.ts +2 -2
  54. package/lib/llhttp/.gitkeep +0 -0
@@ -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
- let defaultMaxConcurrentStreams = 100
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 > 0
25
+ (!Number.isInteger(maxConcurrentStreams) ||
26
+ maxConcurrentStreams < 1)
27
27
  ) {
28
- defaultMaxConcurrentStreams = maxConcurrentStreams
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
- client.close(() => {
193
- const idx = this[kClients].indexOf(client)
194
- if (idx !== -1) {
195
- this[kClients].splice(idx, 1)
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] &&
@@ -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 (const client of this[kClients]) {
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 (clientsLength === 0) {
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 < clientsLength) {
107
- this[kIndex] = (this[kIndex] + 1) % clientsLength
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
- checked++
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] || clientsLength < 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 kPool = Symbol('pool')
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
- // Pool for the actual HTTP connections (with SOCKS5 tunnel connect function)
69
- this[kPool] = null
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 === 'authenticated') {
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
- // Create Pool with custom connect function if we don't have one yet
187
- if (!this[kPool] || this[kPool].destroyed || this[kPool].closed) {
188
- this[kPool] = new Pool(origin, {
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 this[kPool][kDispatch](opts, handler)
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
- if (this[kPool]) {
244
- await this[kPool].close()
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
- if (this[kPool]) {
250
- await this[kPool].destroy(err)
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 31536000
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
  }