undici 6.18.0 → 6.18.2

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/README.md CHANGED
@@ -123,7 +123,7 @@ This section documents our most commonly used API methods. Additional APIs are d
123
123
  Arguments:
124
124
 
125
125
  * **url** `string | URL | UrlObject`
126
- * **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
126
+ * **options** [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions)
127
127
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
128
128
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
129
129
  * **maxRedirections** `Integer` - Default: `0`
@@ -132,14 +132,14 @@ Returns a promise with the result of the `Dispatcher.request` method.
132
132
 
133
133
  Calls `options.dispatcher.request(options)`.
134
134
 
135
- See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
135
+ See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
136
136
 
137
137
  ### `undici.stream([url, options, ]factory): Promise`
138
138
 
139
139
  Arguments:
140
140
 
141
141
  * **url** `string | URL | UrlObject`
142
- * **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
142
+ * **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions)
143
143
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
144
144
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
145
145
  * **maxRedirections** `Integer` - Default: `0`
@@ -149,14 +149,14 @@ Returns a promise with the result of the `Dispatcher.stream` method.
149
149
 
150
150
  Calls `options.dispatcher.stream(options, factory)`.
151
151
 
152
- See [Dispatcher.stream](./docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
152
+ See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
153
153
 
154
154
  ### `undici.pipeline([url, options, ]handler): Duplex`
155
155
 
156
156
  Arguments:
157
157
 
158
158
  * **url** `string | URL | UrlObject`
159
- * **options** [`PipelineOptions`](./docs/api/Dispatcher.md#parameter-pipelineoptions)
159
+ * **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions)
160
160
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
161
161
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
162
162
  * **maxRedirections** `Integer` - Default: `0`
@@ -166,7 +166,7 @@ Returns: `stream.Duplex`
166
166
 
167
167
  Calls `options.dispatch.pipeline(options, handler)`.
168
168
 
169
- See [Dispatcher.pipeline](./docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
169
+ See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
170
170
 
171
171
  ### `undici.connect([url, options]): Promise`
172
172
 
@@ -175,7 +175,7 @@ Starts two-way communications with the requested resource using [HTTP CONNECT](h
175
175
  Arguments:
176
176
 
177
177
  * **url** `string | URL | UrlObject`
178
- * **options** [`ConnectOptions`](./docs/api/Dispatcher.md#parameter-connectoptions)
178
+ * **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions)
179
179
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
180
180
  * **maxRedirections** `Integer` - Default: `0`
181
181
  * **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
@@ -184,7 +184,7 @@ Returns a promise with the result of the `Dispatcher.connect` method.
184
184
 
185
185
  Calls `options.dispatch.connect(options)`.
186
186
 
187
- See [Dispatcher.connect](./docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
187
+ See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
188
188
 
189
189
  ### `undici.fetch(input[, init]): Promise`
190
190
 
@@ -335,7 +335,7 @@ Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](h
335
335
  Arguments:
336
336
 
337
337
  * **url** `string | URL | UrlObject`
338
- * **options** [`UpgradeOptions`](./docs/api/Dispatcher.md#parameter-upgradeoptions)
338
+ * **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions)
339
339
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
340
340
  * **maxRedirections** `Integer` - Default: `0`
341
341
  * **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
@@ -344,7 +344,7 @@ Returns a promise with the result of the `Dispatcher.upgrade` method.
344
344
 
345
345
  Calls `options.dispatcher.upgrade(options)`.
346
346
 
347
- See [Dispatcher.upgrade](./docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
347
+ See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
348
348
 
349
349
  ### `undici.setGlobalDispatcher(dispatcher)`
350
350
 
@@ -46,6 +46,9 @@ It represents the retry state for a given request.
46
46
  - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
47
47
  - **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
48
48
 
49
+ >__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in an state that cannot be reutilized. For these situations the `RetryHandler` will identify
50
+ >the body as stateful and will not retry the request rejecting with the error `UND_ERR_REQ_RETRY`.
51
+
49
52
  Examples:
50
53
 
51
54
  ```js
@@ -20,6 +20,7 @@ module.exports = {
20
20
  kHost: Symbol('host'),
21
21
  kNoRef: Symbol('no ref'),
22
22
  kBodyUsed: Symbol('used'),
23
+ kBody: Symbol('abstracted request body'),
23
24
  kRunning: Symbol('running'),
24
25
  kBlocking: Symbol('blocking'),
25
26
  kPending: Symbol('pending'),
package/lib/core/util.js CHANGED
@@ -1,19 +1,72 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const { kDestroyed, kBodyUsed, kListeners } = require('./symbols')
4
+ const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
5
5
  const { IncomingMessage } = require('node:http')
6
6
  const stream = require('node:stream')
7
7
  const net = require('node:net')
8
- const { InvalidArgumentError } = require('./errors')
9
8
  const { Blob } = require('node:buffer')
10
9
  const nodeUtil = require('node:util')
11
10
  const { stringify } = require('node:querystring')
11
+ const { EventEmitter: EE } = require('node:events')
12
+ const { InvalidArgumentError } = require('./errors')
12
13
  const { headerNameLowerCasedRecord } = require('./constants')
13
14
  const { tree } = require('./tree')
14
15
 
15
16
  const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
16
17
 
18
+ class BodyAsyncIterable {
19
+ constructor (body) {
20
+ this[kBody] = body
21
+ this[kBodyUsed] = false
22
+ }
23
+
24
+ async * [Symbol.asyncIterator] () {
25
+ assert(!this[kBodyUsed], 'disturbed')
26
+ this[kBodyUsed] = true
27
+ yield * this[kBody]
28
+ }
29
+ }
30
+
31
+ function wrapRequestBody (body) {
32
+ if (isStream(body)) {
33
+ // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
34
+ // so that it can be dispatched again?
35
+ // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
36
+ if (bodyLength(body) === 0) {
37
+ body
38
+ .on('data', function () {
39
+ assert(false)
40
+ })
41
+ }
42
+
43
+ if (typeof body.readableDidRead !== 'boolean') {
44
+ body[kBodyUsed] = false
45
+ EE.prototype.on.call(body, 'data', function () {
46
+ this[kBodyUsed] = true
47
+ })
48
+ }
49
+
50
+ return body
51
+ } else if (body && typeof body.pipeTo === 'function') {
52
+ // TODO (fix): We can't access ReadableStream internal state
53
+ // to determine whether or not it has been disturbed. This is just
54
+ // a workaround.
55
+ return new BodyAsyncIterable(body)
56
+ } else if (
57
+ body &&
58
+ typeof body !== 'string' &&
59
+ !ArrayBuffer.isView(body) &&
60
+ isIterable(body)
61
+ ) {
62
+ // TODO: Should we allow re-using iterable if !this.opts.idempotent
63
+ // or through some other flag?
64
+ return new BodyAsyncIterable(body)
65
+ } else {
66
+ return body
67
+ }
68
+ }
69
+
17
70
  function nop () {}
18
71
 
19
72
  function isStream (obj) {
@@ -634,5 +687,6 @@ module.exports = {
634
687
  isHttpOrHttpsPrefixed,
635
688
  nodeMajor,
636
689
  nodeMinor,
637
- safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
690
+ safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
691
+ wrapRequestBody
638
692
  }
@@ -3,7 +3,12 @@ const assert = require('node:assert')
3
3
 
4
4
  const { kRetryHandlerDefaultRetry } = require('../core/symbols')
5
5
  const { RequestRetryError } = require('../core/errors')
6
- const { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util')
6
+ const {
7
+ isDisturbed,
8
+ parseHeaders,
9
+ parseRangeHeader,
10
+ wrapRequestBody
11
+ } = require('../core/util')
7
12
 
8
13
  function calculateRetryAfterHeader (retryAfter) {
9
14
  const current = Date.now()
@@ -29,7 +34,7 @@ class RetryHandler {
29
34
 
30
35
  this.dispatch = handlers.dispatch
31
36
  this.handler = handlers.handler
32
- this.opts = dispatchOpts
37
+ this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
33
38
  this.abort = null
34
39
  this.aborted = false
35
40
  this.retryOpts = {
@@ -174,7 +179,9 @@ class RetryHandler {
174
179
  this.abort(
175
180
  new RequestRetryError('Request failed', statusCode, {
176
181
  headers,
177
- count: this.retryCount
182
+ data: {
183
+ count: this.retryCount
184
+ }
178
185
  })
179
186
  )
180
187
  return false
@@ -278,7 +285,7 @@ class RetryHandler {
278
285
 
279
286
  const err = new RequestRetryError('Request failed', statusCode, {
280
287
  headers,
281
- count: this.retryCount
288
+ data: { count: this.retryCount }
282
289
  })
283
290
 
284
291
  this.abort(err)
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { parseSetCookie } = require('./parse')
4
- const { stringify, getHeadersList } = require('./util')
4
+ const { stringify } = require('./util')
5
5
  const { webidl } = require('../fetch/webidl')
6
6
  const { Headers } = require('../fetch/headers')
7
7
 
@@ -78,14 +78,13 @@ function getSetCookies (headers) {
78
78
 
79
79
  webidl.brandCheck(headers, Headers, { strict: false })
80
80
 
81
- const cookies = getHeadersList(headers).cookies
81
+ const cookies = headers.getSetCookie()
82
82
 
83
83
  if (!cookies) {
84
84
  return []
85
85
  }
86
86
 
87
- // In older versions of undici, cookies is a list of name:value.
88
- return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
87
+ return cookies.map((pair) => parseSetCookie(pair))
89
88
  }
90
89
 
91
90
  /**
@@ -1,8 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('node:assert')
4
- const { getHeadersList: internalGetHeadersList } = require('../fetch/headers')
5
-
6
3
  /**
7
4
  * @param {string} value
8
5
  * @returns {boolean}
@@ -275,35 +272,11 @@ function stringify (cookie) {
275
272
  return out.join('; ')
276
273
  }
277
274
 
278
- let kHeadersListNode
279
-
280
- function getHeadersList (headers) {
281
- try {
282
- return internalGetHeadersList(headers)
283
- } catch {
284
- // fall-through
285
- }
286
-
287
- if (!kHeadersListNode) {
288
- kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
289
- (symbol) => symbol.description === 'headers list'
290
- )
291
-
292
- assert(kHeadersListNode, 'Headers cannot be parsed')
293
- }
294
-
295
- const headersList = headers[kHeadersListNode]
296
- assert(headersList)
297
-
298
- return headersList
299
- }
300
-
301
275
  module.exports = {
302
276
  isCTLExcludingHtab,
303
277
  validateCookieName,
304
278
  validateCookiePath,
305
279
  validateCookieValue,
306
280
  toIMFDate,
307
- stringify,
308
- getHeadersList
281
+ stringify
309
282
  }
@@ -626,10 +626,6 @@ Reflect.deleteProperty(Headers, 'setHeadersGuard')
626
626
  Reflect.deleteProperty(Headers, 'getHeadersList')
627
627
  Reflect.deleteProperty(Headers, 'setHeadersList')
628
628
 
629
- Object.defineProperty(Headers.prototype, util.inspect.custom, {
630
- enumerable: false
631
- })
632
-
633
629
  iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
634
630
 
635
631
  Object.defineProperties(Headers.prototype, {
@@ -642,6 +638,9 @@ Object.defineProperties(Headers.prototype, {
642
638
  [Symbol.toStringTag]: {
643
639
  value: 'Headers',
644
640
  configurable: true
641
+ },
642
+ [util.inspect.custom]: {
643
+ enumerable: false
645
644
  }
646
645
  })
647
646
 
@@ -240,8 +240,8 @@ class ByteParser extends Writable {
240
240
 
241
241
  this.#loop = true
242
242
  this.#state = parserStates.INFO
243
- this.run(callback)
244
243
  this.#fragments.length = 0
244
+ this.run(callback)
245
245
  })
246
246
 
247
247
  this.#loop = false
@@ -2,15 +2,30 @@
2
2
 
3
3
  const { WebsocketFrameSend } = require('./frame')
4
4
  const { opcodes, sendHints } = require('./constants')
5
+ const FixedQueue = require('../../dispatcher/fixed-queue')
5
6
 
6
- /** @type {Uint8Array} */
7
+ /** @type {typeof Uint8Array} */
7
8
  const FastBuffer = Buffer[Symbol.species]
8
9
 
10
+ /**
11
+ * @typedef {object} SendQueueNode
12
+ * @property {Promise<void> | null} promise
13
+ * @property {((...args: any[]) => any)} callback
14
+ * @property {Buffer | null} frame
15
+ */
16
+
9
17
  class SendQueue {
10
- #queued = new Set()
11
- #size = 0
18
+ /**
19
+ * @type {FixedQueue}
20
+ */
21
+ #queue = new FixedQueue()
22
+
23
+ /**
24
+ * @type {boolean}
25
+ */
26
+ #running = false
12
27
 
13
- /** @type {import('net').Socket} */
28
+ /** @type {import('node:net').Socket} */
14
29
  #socket
15
30
 
16
31
  constructor (socket) {
@@ -19,58 +34,62 @@ class SendQueue {
19
34
 
20
35
  add (item, cb, hint) {
21
36
  if (hint !== sendHints.blob) {
22
- const data = clone(item, hint)
23
-
24
- if (this.#size === 0) {
25
- this.#dispatch(data, cb, hint)
37
+ const frame = createFrame(item, hint)
38
+ if (!this.#running) {
39
+ // fast-path
40
+ this.#socket.write(frame, cb)
26
41
  } else {
27
- this.#queued.add([data, cb, true, hint])
28
- this.#size++
29
-
30
- this.#run()
42
+ /** @type {SendQueueNode} */
43
+ const node = {
44
+ promise: null,
45
+ callback: cb,
46
+ frame
47
+ }
48
+ this.#queue.push(node)
31
49
  }
32
-
33
50
  return
34
51
  }
35
52
 
36
- const promise = item.arrayBuffer()
37
- const queue = [null, cb, false, hint]
38
- promise.then((ab) => {
39
- queue[0] = clone(ab, hint)
40
- queue[2] = true
53
+ /** @type {SendQueueNode} */
54
+ const node = {
55
+ promise: item.arrayBuffer().then((ab) => {
56
+ node.promise = null
57
+ node.frame = createFrame(ab, hint)
58
+ }),
59
+ callback: cb,
60
+ frame: null
61
+ }
41
62
 
42
- this.#run()
43
- })
63
+ this.#queue.push(node)
44
64
 
45
- this.#queued.add(queue)
46
- this.#size++
65
+ if (!this.#running) {
66
+ this.#run()
67
+ }
47
68
  }
48
69
 
49
- #run () {
50
- for (const queued of this.#queued) {
51
- const [data, cb, done, hint] = queued
52
-
53
- if (!done) return
54
-
55
- this.#queued.delete(queued)
56
- this.#size--
57
-
58
- this.#dispatch(data, cb, hint)
70
+ async #run () {
71
+ this.#running = true
72
+ const queue = this.#queue
73
+ while (!queue.isEmpty()) {
74
+ const node = queue.shift()
75
+ // wait pending promise
76
+ if (node.promise !== null) {
77
+ await node.promise
78
+ }
79
+ // write
80
+ this.#socket.write(node.frame, node.callback)
81
+ // cleanup
82
+ node.callback = node.frame = null
59
83
  }
84
+ this.#running = false
60
85
  }
86
+ }
61
87
 
62
- #dispatch (data, cb, hint) {
63
- const frame = new WebsocketFrameSend()
64
- const opcode = hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY
65
-
66
- frame.frameData = data
67
- const buffer = frame.createFrame(opcode)
68
-
69
- this.#socket.write(buffer, cb)
70
- }
88
+ function createFrame (data, hint) {
89
+ return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY)
71
90
  }
72
91
 
73
- function clone (data, hint) {
92
+ function toBuffer (data, hint) {
74
93
  switch (hint) {
75
94
  case sendHints.string:
76
95
  return Buffer.from(data)
@@ -78,7 +97,7 @@ function clone (data, hint) {
78
97
  case sendHints.blob:
79
98
  return new FastBuffer(data)
80
99
  case sendHints.typedArray:
81
- return Buffer.copyBytesFrom(data)
100
+ return new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
82
101
  }
83
102
  }
84
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.18.0",
3
+ "version": "6.18.2",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -107,7 +107,7 @@
107
107
  "@sinonjs/fake-timers": "^11.1.0",
108
108
  "@types/node": "^18.0.3",
109
109
  "abort-controller": "^3.0.0",
110
- "borp": "^0.13.0",
110
+ "borp": "^0.14.0",
111
111
  "c8": "^9.1.0",
112
112
  "cross-env": "^7.0.3",
113
113
  "dns-packet": "^5.4.0",