undici 4.8.2 → 4.9.3

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.
@@ -25,10 +25,6 @@ See: [`PoolOptions`](docs/api/Pool.md#parameter-pooloptions)
25
25
 
26
26
  Returns an array of upstreams that were previously added.
27
27
 
28
- ### `BalancedPool.busy`
29
-
30
- Implements [Client.busy](docs/api/Client.md#clientbusy)
31
-
32
28
  ### `BalancedPool.closed`
33
29
 
34
30
  Implements [Client.closed](docs/api/Client.md#clientclosed)
@@ -113,12 +113,6 @@ See [`Dispatcher.upgrade(options[, callback])`](docs/api/Dispatcher.md#clientupg
113
113
 
114
114
  ## Instance Properties
115
115
 
116
- ### `Client.busy`
117
-
118
- * `boolean`
119
-
120
- `true` if pipeline is saturated or blocked. Indicates whether dispatching further requests is meaningful.
121
-
122
116
  ### `Client.closed`
123
117
 
124
118
  * `boolean`
@@ -161,7 +155,7 @@ const server = createServer((request, response) => {
161
155
  response.end('Hello, World!')
162
156
  }).listen()
163
157
 
164
- await once(server, 'listening')
158
+ await once(server, 'listening')
165
159
 
166
160
  const client = new Client(`http://localhost:${server.address().port}`)
167
161
 
@@ -251,7 +245,6 @@ const client = new Client(`http://localhost:${server.address().port}`)
251
245
 
252
246
  client.on('drain', () => {
253
247
  console.log('drain event')
254
- console.log(`Is Client busy: ${client.busy}`)
255
248
  client.close()
256
249
  server.close()
257
250
  })
@@ -262,8 +255,6 @@ const requests = [
262
255
  client.request({ path: '/', method: 'GET' })
263
256
  ]
264
257
 
265
- console.log(`Is Client busy: ${client.busy}`)
266
-
267
258
  await Promise.all(requests)
268
259
 
269
260
  console.log('requests completed')
@@ -177,14 +177,15 @@ try {
177
177
  ### `Dispatcher.dispatch(options, handler)`
178
178
 
179
179
  This is the low level API which all the preceding APIs are implemented on top of.
180
- This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this.
180
+ This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs.
181
+ It is primarily intended for library developers who implement higher level APIs on top of this.
181
182
 
182
183
  Arguments:
183
184
 
184
185
  * **options** `DispatchOptions`
185
186
  * **handler** `DispatchHandler`
186
187
 
187
- Returns: `void`
188
+ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls won't make any progress until the `'drain'` event has been emitted.
188
189
 
189
190
  #### Parameter: `DispatchOptions`
190
191
 
@@ -330,7 +331,7 @@ Extends: [`RequestOptions`](#parameter-requestoptions)
330
331
  * **opaque** `unknown`
331
332
  * **body** `stream.Readable`
332
333
  * **context** `object`
333
- * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
334
+ * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
334
335
 
335
336
  #### Example 1 - Pipeline Echo
336
337
 
@@ -413,7 +414,7 @@ Extends: [`DispatchOptions`](#parameter-dispatchoptions)
413
414
 
414
415
  * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`
415
416
  * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`
416
- * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
417
+ * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
417
418
 
418
419
  The `RequestOptions.method` property should not be value `'CONNECT'`.
419
420
 
@@ -581,7 +582,7 @@ Returns: `void | Promise<StreamData>` - Only returns a `Promise` if no `callback
581
582
  * **statusCode** `number`
582
583
  * **headers** `http.IncomingHttpHeaders`
583
584
  * **opaque** `unknown`
584
- * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
585
+ * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
585
586
 
586
587
  #### Parameter: `StreamData`
587
588
 
package/docs/api/Pool.md CHANGED
@@ -22,10 +22,6 @@ Extends: [`ClientOptions`](docs/api/Client.md#parameter-clientoptions)
22
22
 
23
23
  ## Instance Properties
24
24
 
25
- ### `Pool.busy`
26
-
27
- Implements [Client.busy](docs/api/Client.md#clientbusy)
28
-
29
25
  ### `Pool.closed`
30
26
 
31
27
  Implements [Client.closed](docs/api/Client.md#clientclosed)
@@ -59,4 +59,4 @@ This sub-state is only entered when a `Client` instance has queued requests and
59
59
 
60
60
  ### destroyed
61
61
 
62
- The **destroyed** state is a final state for the `Client` instance. Once in this state, a `Client` is nonfunctional. Calling any other `Client` methods will result in an `ClientDestroyedError`.
62
+ The **destroyed** state is a final state for the `Client` instance. Once in this state, a `Client` is nonfunctional. Calling any other `Client` methods will result in an `ClientDestroyedError`.
@@ -44,6 +44,8 @@ class BalancedPool extends Dispatcher {
44
44
  })
45
45
 
46
46
  pool.on('drain', (...args) => {
47
+ pool[kNeedDrain] = false
48
+
47
49
  if (this[kNeedDrain]) {
48
50
  this[kNeedDrain] = false
49
51
  this.emit('drain', ...args)
@@ -62,12 +64,16 @@ class BalancedPool extends Dispatcher {
62
64
  throw new BalancedPoolMissingUpstreamError()
63
65
  }
64
66
 
65
- let pool = this[kPools].shift()
66
- this[kPools].push(pool)
67
- if (pool.busy) {
68
- pool = this[kPools].find(pool => !pool.busy) || pool
67
+ const pool = this[kPools].find(pool => !pool[kNeedDrain]) || this[kPools][0]
68
+
69
+ if (!pool.dispatch(opts, handler)) {
70
+ pool[kNeedDrain] = true
71
+ this[kNeedDrain] = true
69
72
  }
70
- this[kNeedDrain] = !pool.dispatch(opts, handler)
73
+
74
+ this[kPools].splice(this[kPools].indexOf(pool), 1)
75
+ this[kPools].push(pool)
76
+
71
77
  return !this[kNeedDrain]
72
78
  }
73
79
 
@@ -87,10 +93,6 @@ class BalancedPool extends Dispatcher {
87
93
  return this[kPools].reduce((acc, pool) => acc && pool.destroyed, true)
88
94
  }
89
95
 
90
- get busy () {
91
- return this[kPools].reduce((acc, pool) => acc && pool.busy, true) || false
92
- }
93
-
94
96
  get closed () {
95
97
  return this[kPools].reduce((acc, pool) => acc && pool.closed, true)
96
98
  }
package/lib/client.js CHANGED
@@ -62,7 +62,9 @@ const {
62
62
  kBodyTimeout,
63
63
  kStrictContentLength,
64
64
  kConnector,
65
- kMaxRedirections
65
+ kMaxRedirections,
66
+ kMaxRequests,
67
+ kCounter
66
68
  } = require('./core/symbols')
67
69
 
68
70
  const channels = {}
@@ -100,7 +102,8 @@ class Client extends Dispatcher {
100
102
  strictContentLength,
101
103
  maxCachedSessions,
102
104
  maxRedirections,
103
- connect
105
+ connect,
106
+ maxRequestsPerClient
104
107
  } = {}) {
105
108
  super()
106
109
 
@@ -164,6 +167,10 @@ class Client extends Dispatcher {
164
167
  throw new InvalidArgumentError('maxRedirections must be a positive number')
165
168
  }
166
169
 
170
+ if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) {
171
+ throw new InvalidArgumentError('maxRequestsPerClient must be a positive number')
172
+ }
173
+
167
174
  if (typeof connect !== 'function') {
168
175
  connect = buildConnector({
169
176
  ...tls,
@@ -194,6 +201,7 @@ class Client extends Dispatcher {
194
201
  this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 30e3
195
202
  this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
196
203
  this[kMaxRedirections] = maxRedirections
204
+ this[kMaxRequests] = maxRequestsPerClient
197
205
 
198
206
  // kQueue is built up of 3 sections separated by
199
207
  // the kRunningIdx and kPendingIdx indices.
@@ -400,8 +408,6 @@ class Client extends Dispatcher {
400
408
  const constants = require('./llhttp/constants')
401
409
  const EMPTY_BUF = Buffer.alloc(0)
402
410
 
403
- let llhttpPromise
404
- let llhttpInstance
405
411
  async function lazyllhttp () {
406
412
  const { resolve } = require('path')
407
413
  const { readFile } = require('fs').promises
@@ -419,7 +425,7 @@ async function lazyllhttp () {
419
425
  mod = await WebAssembly.compile(await readFile(resolve(__dirname, './llhttp/llhttp.wasm')))
420
426
  }
421
427
 
422
- llhttpInstance = new WebAssembly.Instance(mod, {
428
+ return await WebAssembly.instantiate(mod, {
423
429
  env: {
424
430
  /* eslint-disable camelcase */
425
431
 
@@ -467,10 +473,14 @@ async function lazyllhttp () {
467
473
  /* eslint-enable camelcase */
468
474
  }
469
475
  })
470
-
471
- return llhttpInstance
472
476
  }
473
477
 
478
+ let llhttpInstance = null
479
+ let llhttpPromise = lazyllhttp()
480
+ .catch(() => {
481
+ // TODO: Emit warning?
482
+ })
483
+
474
484
  let currentParser = null
475
485
  let currentBufferRef = null
476
486
  let currentBufferSize = 0
@@ -1130,15 +1140,6 @@ async function connect (client) {
1130
1140
  }
1131
1141
 
1132
1142
  try {
1133
- if (!llhttpInstance) {
1134
- if (!llhttpPromise) {
1135
- llhttpPromise = lazyllhttp()
1136
- }
1137
- await llhttpPromise
1138
- assert(llhttpInstance)
1139
- llhttpPromise = null
1140
- }
1141
-
1142
1143
  const socket = await new Promise((resolve, reject) => {
1143
1144
  client[kConnector]({
1144
1145
  host,
@@ -1155,6 +1156,11 @@ async function connect (client) {
1155
1156
  })
1156
1157
  })
1157
1158
 
1159
+ if (!llhttpInstance) {
1160
+ llhttpInstance = await llhttpPromise
1161
+ llhttpPromise = null
1162
+ }
1163
+
1158
1164
  client[kConnecting] = false
1159
1165
 
1160
1166
  assert(socket)
@@ -1168,6 +1174,8 @@ async function connect (client) {
1168
1174
  socket[kError] = null
1169
1175
  socket[kParser] = new Parser(client, socket, llhttpInstance)
1170
1176
  socket[kClient] = client
1177
+ socket[kCounter] = 0
1178
+ socket[kMaxRequests] = client[kMaxRequests]
1171
1179
  socket
1172
1180
  .on('error', onSocketError)
1173
1181
  .on('readable', onSocketReadable)
@@ -1187,7 +1195,6 @@ async function connect (client) {
1187
1195
  socket
1188
1196
  })
1189
1197
  }
1190
-
1191
1198
  client.emit('connect', client[kUrl], [client])
1192
1199
  } catch (err) {
1193
1200
  client[kConnecting] = false
@@ -1233,6 +1240,7 @@ function resume (client, sync) {
1233
1240
  }
1234
1241
 
1235
1242
  client[kResuming] = 2
1243
+
1236
1244
  _resume(client, sync)
1237
1245
  client[kResuming] = 0
1238
1246
 
@@ -1465,6 +1473,10 @@ function write (client, request) {
1465
1473
  socket[kReset] = true
1466
1474
  }
1467
1475
 
1476
+ if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) {
1477
+ socket[kReset] = true
1478
+ }
1479
+
1468
1480
  if (blocking) {
1469
1481
  socket[kBlocking] = true
1470
1482
  }
@@ -41,5 +41,7 @@ module.exports = {
41
41
  kConnector: Symbol('connector'),
42
42
  kStrictContentLength: Symbol('strict content length'),
43
43
  kMaxRedirections: Symbol('maxRedirections'),
44
- kProxy: Symbol('proxy agent options')
44
+ kMaxRequests: Symbol('maxRequestsPerClient'),
45
+ kProxy: Symbol('proxy agent options'),
46
+ kCounter: Symbol('socket request counter')
45
47
  }
@@ -313,20 +313,23 @@ class Headers {
313
313
  }
314
314
 
315
315
  * keys () {
316
- for (let index = 0; index < this[kHeadersList].length; index += 2) {
317
- yield this[kHeadersList][index]
316
+ const clone = this[kHeadersList].slice()
317
+ for (let index = 0; index < clone.length; index += 2) {
318
+ yield clone[index]
318
319
  }
319
320
  }
320
321
 
321
322
  * values () {
322
- for (let index = 1; index < this[kHeadersList].length; index += 2) {
323
- yield this[kHeadersList][index]
323
+ const clone = this[kHeadersList].slice()
324
+ for (let index = 1; index < clone.length; index += 2) {
325
+ yield clone[index]
324
326
  }
325
327
  }
326
328
 
327
329
  * entries () {
328
- for (let index = 0; index < this[kHeadersList].length; index += 2) {
329
- yield [this[kHeadersList][index], this[kHeadersList][index + 1]]
330
+ const clone = this[kHeadersList].slice()
331
+ for (let index = 0; index < clone.length; index += 2) {
332
+ yield [clone[index], clone[index + 1]]
330
333
  }
331
334
  }
332
335
 
@@ -171,7 +171,7 @@ async function fetch (...args) {
171
171
 
172
172
  // 3. If response is a network error, then reject p with a TypeError
173
173
  // and terminate these substeps.
174
- if (response.status === 0) {
174
+ if (response.type === 'error') {
175
175
  p.reject(
176
176
  Object.assign(new TypeError('fetch failed'), { cause: response.error })
177
177
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "4.8.2",
3
+ "version": "4.9.3",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
package/types/agent.d.ts CHANGED
@@ -11,7 +11,7 @@ declare class Agent extends Dispatcher{
11
11
  /** `true` after `dispatcher.destroyed()` has been called or `dispatcher.close()` has been called and the dispatcher shutdown has completed. */
12
12
  destroyed: boolean;
13
13
  /** Dispatches a request. */
14
- dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): void;
14
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
15
15
  }
16
16
 
17
17
  declare namespace Agent {
package/types/client.d.ts CHANGED
@@ -39,6 +39,8 @@ declare namespace Client {
39
39
  strictContentLength?: boolean;
40
40
  /** @deprecated use the connect option instead */
41
41
  tls?: TlsOptions | null;
42
+ /** */
43
+ maxRequestsPerClient?: number;
42
44
  }
43
45
 
44
46
  export interface SocketInfo {
@@ -11,7 +11,7 @@ export = Dispatcher;
11
11
  /** Dispatcher is the core API used to dispatch requests. */
12
12
  declare class Dispatcher extends EventEmitter {
13
13
  /** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */
14
- dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): void;
14
+ dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
15
15
  /** Starts two-way communications with the requested resource. */
16
16
  connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
17
17
  connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
@@ -148,7 +148,7 @@ declare namespace Dispatcher {
148
148
  export type PipelineHandler = (data: PipelineHandlerData) => Readable;
149
149
  export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';
150
150
 
151
- /**
151
+ /**
152
152
  * @link https://fetch.spec.whatwg.org/#body-mixin
153
153
  */
154
154
  interface BodyMixin {
@@ -12,7 +12,7 @@ declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.
12
12
  get<TInterceptable extends Interceptable>(origin: RegExp): TInterceptable;
13
13
  get<TInterceptable extends Interceptable>(origin: ((origin: string) => boolean)): TInterceptable;
14
14
  /** Dispatches a mocked request. */
15
- dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): void;
15
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
16
16
  /** Closes the mock agent and waits for registered mock pools and clients to also close before resolving. */
17
17
  close(): Promise<void>;
18
18
  /** Disables mocking in MockAgent. */
@@ -11,7 +11,7 @@ declare class MockClient extends Client implements Interceptable {
11
11
  /** Intercepts any matching requests that use the same origin as this mock client. */
12
12
  intercept(options: MockInterceptor.Options): MockInterceptor;
13
13
  /** Dispatches a mocked request. */
14
- dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): void;
14
+ dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean;
15
15
  /** Closes the mock client and gracefully waits for enqueued requests to complete. */
16
16
  close(): Promise<void>;
17
17
  }
@@ -11,7 +11,7 @@ declare class MockPool extends Pool implements Interceptable {
11
11
  /** Intercepts any matching requests that use the same origin as this mock pool. */
12
12
  intercept(options: MockInterceptor.Options): MockInterceptor;
13
13
  /** Dispatches a mocked request. */
14
- dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): void;
14
+ dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean;
15
15
  /** Closes the mock pool and gracefully waits for enqueued requests to complete. */
16
16
  close(): Promise<void>;
17
17
  }
@@ -6,7 +6,7 @@ export = ProxyAgent
6
6
  declare class ProxyAgent extends Dispatcher {
7
7
  constructor(options: ProxyAgent.Options | string)
8
8
 
9
- dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): void;
9
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
10
10
  close(): Promise<void>;
11
11
  }
12
12