undici 7.12.0 → 7.13.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.
@@ -1,5 +1,3 @@
1
- // Ported from https://github.com/nodejs/undici/pull/907
2
-
3
1
  'use strict'
4
2
 
5
3
  const assert = require('node:assert')
@@ -50,23 +48,32 @@ class BodyReadable extends Readable {
50
48
 
51
49
  this[kAbort] = abort
52
50
 
53
- /**
54
- * @type {Consume | null}
55
- */
51
+ /** @type {Consume | null} */
56
52
  this[kConsume] = null
53
+
54
+ /** @type {number} */
57
55
  this[kBytesRead] = 0
58
- /**
59
- * @type {ReadableStream|null}
60
- */
56
+
57
+ /** @type {ReadableStream|null} */
61
58
  this[kBody] = null
59
+
60
+ /** @type {boolean} */
62
61
  this[kUsed] = false
62
+
63
+ /** @type {string} */
63
64
  this[kContentType] = contentType
65
+
66
+ /** @type {number|null} */
64
67
  this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
65
68
 
66
- // Is stream being consumed through Readable API?
67
- // This is an optimization so that we avoid checking
68
- // for 'data' and 'readable' listeners in the hot path
69
- // inside push().
69
+ /**
70
+ * Is stream being consumed through Readable API?
71
+ * This is an optimization so that we avoid checking
72
+ * for 'data' and 'readable' listeners in the hot path
73
+ * inside push().
74
+ *
75
+ * @type {boolean}
76
+ */
70
77
  this[kReading] = false
71
78
  }
72
79
 
@@ -96,7 +103,7 @@ class BodyReadable extends Readable {
96
103
  }
97
104
 
98
105
  /**
99
- * @param {string} event
106
+ * @param {string|symbol} event
100
107
  * @param {(...args: any[]) => void} listener
101
108
  * @returns {this}
102
109
  */
@@ -109,7 +116,7 @@ class BodyReadable extends Readable {
109
116
  }
110
117
 
111
118
  /**
112
- * @param {string} event
119
+ * @param {string|symbol} event
113
120
  * @param {(...args: any[]) => void} listener
114
121
  * @returns {this}
115
122
  */
@@ -147,12 +154,14 @@ class BodyReadable extends Readable {
147
154
  * @returns {boolean}
148
155
  */
149
156
  push (chunk) {
150
- this[kBytesRead] += chunk ? chunk.length : 0
151
-
152
- if (this[kConsume] && chunk !== null) {
153
- consumePush(this[kConsume], chunk)
154
- return this[kReading] ? super.push(chunk) : true
157
+ if (chunk) {
158
+ this[kBytesRead] += chunk.length
159
+ if (this[kConsume]) {
160
+ consumePush(this[kConsume], chunk)
161
+ return this[kReading] ? super.push(chunk) : true
162
+ }
155
163
  }
164
+
156
165
  return super.push(chunk)
157
166
  }
158
167
 
@@ -338,9 +347,23 @@ function isUnusable (bodyReadable) {
338
347
  return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
339
348
  }
340
349
 
350
+ /**
351
+ * @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
352
+ */
353
+
354
+ /**
355
+ * @template {ConsumeType} T
356
+ * @typedef {T extends 'text' ? string :
357
+ * T extends 'json' ? unknown :
358
+ * T extends 'blob' ? Blob :
359
+ * T extends 'arrayBuffer' ? ArrayBuffer :
360
+ * T extends 'bytes' ? Uint8Array :
361
+ * never
362
+ * } ConsumeReturnType
363
+ */
341
364
  /**
342
365
  * @typedef {object} Consume
343
- * @property {string} type
366
+ * @property {ConsumeType} type
344
367
  * @property {BodyReadable} stream
345
368
  * @property {((value?: any) => void)} resolve
346
369
  * @property {((err: Error) => void)} reject
@@ -349,9 +372,10 @@ function isUnusable (bodyReadable) {
349
372
  */
350
373
 
351
374
  /**
375
+ * @template {ConsumeType} T
352
376
  * @param {BodyReadable} stream
353
- * @param {string} type
354
- * @returns {Promise<any>}
377
+ * @param {T} type
378
+ * @returns {Promise<ConsumeReturnType<T>>}
355
379
  */
356
380
  function consume (stream, type) {
357
381
  assert(!stream[kConsume])
@@ -361,9 +385,7 @@ function consume (stream, type) {
361
385
  const rState = stream._readableState
362
386
  if (rState.destroyed && rState.closeEmitted === false) {
363
387
  stream
364
- .on('error', err => {
365
- reject(err)
366
- })
388
+ .on('error', reject)
367
389
  .on('close', () => {
368
390
  reject(new TypeError('unusable'))
369
391
  })
@@ -438,7 +460,7 @@ function consumeStart (consume) {
438
460
  /**
439
461
  * @param {Buffer[]} chunks
440
462
  * @param {number} length
441
- * @param {BufferEncoding} encoding
463
+ * @param {BufferEncoding} [encoding='utf8']
442
464
  * @returns {string}
443
465
  */
444
466
  function chunksDecode (chunks, length, encoding) {
package/lib/core/util.js CHANGED
@@ -5,7 +5,6 @@ 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 { Blob } = require('node:buffer')
9
8
  const { stringify } = require('node:querystring')
10
9
  const { EventEmitter: EE } = require('node:events')
11
10
  const timers = require('../util/timers')
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
3
+ const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
4
4
  const { URL } = require('node:url')
5
5
  const Agent = require('./agent')
6
6
  const Pool = require('./pool')
@@ -27,61 +27,69 @@ function defaultFactory (origin, opts) {
27
27
 
28
28
  const noop = () => {}
29
29
 
30
- class ProxyClient extends DispatcherBase {
31
- #client = null
32
- constructor (origin, opts) {
33
- if (typeof origin === 'string') {
34
- origin = new URL(origin)
35
- }
30
+ function defaultAgentFactory (origin, opts) {
31
+ if (opts.connections === 1) {
32
+ return new Client(origin, opts)
33
+ }
34
+ return new Pool(origin, opts)
35
+ }
36
36
 
37
- if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
38
- throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
39
- }
37
+ class Http1ProxyWrapper extends DispatcherBase {
38
+ #client
40
39
 
40
+ constructor (proxyUrl, { headers = {}, connect, factory }) {
41
41
  super()
42
+ if (!proxyUrl) {
43
+ throw new InvalidArgumentError('Proxy URL is mandatory')
44
+ }
42
45
 
43
- this.#client = new Client(origin, opts)
44
- }
45
-
46
- async [kClose] () {
47
- await this.#client.close()
48
- }
49
-
50
- async [kDestroy] () {
51
- await this.#client.destroy()
46
+ this[kProxyHeaders] = headers
47
+ if (factory) {
48
+ this.#client = factory(proxyUrl, { connect })
49
+ } else {
50
+ this.#client = new Client(proxyUrl, { connect })
51
+ }
52
52
  }
53
53
 
54
- async [kDispatch] (opts, handler) {
55
- const { method, origin } = opts
56
- if (method === 'CONNECT') {
57
- this.#client[kConnector]({
58
- origin,
59
- port: opts.port || defaultProtocolPort(opts.protocol),
60
- path: opts.host,
61
- signal: opts.signal,
62
- headers: {
63
- ...this[kProxyHeaders],
64
- host: opts.host
65
- },
66
- servername: this[kProxyTls]?.servername || opts.servername
67
- },
68
- (err, socket) => {
69
- if (err) {
70
- handler.callback(err)
71
- } else {
72
- handler.callback(null, { socket, statusCode: 200 })
54
+ [kDispatch] (opts, handler) {
55
+ const onHeaders = handler.onHeaders
56
+ handler.onHeaders = function (statusCode, data, resume) {
57
+ if (statusCode === 407) {
58
+ if (typeof handler.onError === 'function') {
59
+ handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
73
60
  }
61
+ return
74
62
  }
75
- )
76
- return
63
+ if (onHeaders) onHeaders.call(this, statusCode, data, resume)
77
64
  }
78
- if (typeof origin === 'string') {
79
- opts.origin = new URL(origin)
65
+
66
+ // Rewrite request as an HTTP1 Proxy request, without tunneling.
67
+ const {
68
+ origin,
69
+ path = '/',
70
+ headers = {}
71
+ } = opts
72
+
73
+ opts.path = origin + path
74
+
75
+ if (!('host' in headers) && !('Host' in headers)) {
76
+ const { host } = new URL(origin)
77
+ headers.host = host
80
78
  }
79
+ opts.headers = { ...this[kProxyHeaders], ...headers }
80
+
81
+ return this.#client[kDispatch](opts, handler)
82
+ }
81
83
 
82
- return this.#client.dispatch(opts, handler)
84
+ async [kClose] () {
85
+ return this.#client.close()
86
+ }
87
+
88
+ async [kDestroy] (err) {
89
+ return this.#client.destroy(err)
83
90
  }
84
91
  }
92
+
85
93
  class ProxyAgent extends DispatcherBase {
86
94
  constructor (opts) {
87
95
  if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@@ -104,6 +112,7 @@ class ProxyAgent extends DispatcherBase {
104
112
  this[kRequestTls] = opts.requestTls
105
113
  this[kProxyTls] = opts.proxyTls
106
114
  this[kProxyHeaders] = opts.headers || {}
115
+ this[kTunnelProxy] = proxyTunnel
107
116
 
108
117
  if (opts.auth && opts.token) {
109
118
  throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
@@ -116,21 +125,25 @@ class ProxyAgent extends DispatcherBase {
116
125
  this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
117
126
  }
118
127
 
119
- const factory = (!proxyTunnel && protocol === 'http:')
120
- ? (origin, options) => {
121
- if (origin.protocol === 'http:') {
122
- return new ProxyClient(origin, options)
123
- }
124
- return new Client(origin, options)
125
- }
126
- : undefined
127
-
128
128
  const connect = buildConnector({ ...opts.proxyTls })
129
129
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
130
- this[kClient] = clientFactory(url, { connect, factory })
131
- this[kTunnelProxy] = proxyTunnel
130
+
131
+ const agentFactory = opts.factory || defaultAgentFactory
132
+ const factory = (origin, options) => {
133
+ const { protocol } = new URL(origin)
134
+ if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
135
+ return new Http1ProxyWrapper(this[kProxy].uri, {
136
+ headers: this[kProxyHeaders],
137
+ connect,
138
+ factory: agentFactory
139
+ })
140
+ }
141
+ return agentFactory(origin, options)
142
+ }
143
+ this[kClient] = clientFactory(url, { connect })
132
144
  this[kAgent] = new Agent({
133
145
  ...opts,
146
+ factory,
134
147
  connect: async (opts, callback) => {
135
148
  let requestedPath = opts.host
136
149
  if (!opts.port) {
@@ -185,10 +198,6 @@ class ProxyAgent extends DispatcherBase {
185
198
  headers.host = host
186
199
  }
187
200
 
188
- if (!this.#shouldConnect(new URL(opts.origin))) {
189
- opts.path = opts.origin + opts.path
190
- }
191
-
192
201
  return this[kAgent].dispatch(
193
202
  {
194
203
  ...opts,
@@ -221,19 +230,6 @@ class ProxyAgent extends DispatcherBase {
221
230
  await this[kAgent].destroy()
222
231
  await this[kClient].destroy()
223
232
  }
224
-
225
- #shouldConnect (uri) {
226
- if (typeof uri === 'string') {
227
- uri = new URL(uri)
228
- }
229
- if (this[kTunnelProxy]) {
230
- return true
231
- }
232
- if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
233
- return true
234
- }
235
- return false
236
- }
237
233
  }
238
234
 
239
235
  /**
@@ -133,6 +133,16 @@ class RedirectHandler {
133
133
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
134
134
  const path = search ? `${pathname}${search}` : pathname
135
135
 
136
+ // Check for redirect loops by seeing if we've already visited this URL in our history
137
+ // This catches the case where Client/Pool try to handle cross-origin redirects but fail
138
+ // and keep redirecting to the same URL in an infinite loop
139
+ const redirectUrlString = `${origin}${path}`
140
+ for (const historyUrl of this.history) {
141
+ if (historyUrl.toString() === redirectUrlString) {
142
+ throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`)
143
+ }
144
+ }
145
+
136
146
  // Remove headers referring to the original URL.
137
147
  // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
138
148
  // https://tools.ietf.org/html/rfc7231#section-6.4
@@ -57,7 +57,8 @@ class DumpHandler extends DecoratorHandler {
57
57
  return
58
58
  }
59
59
 
60
- err = this.#controller.reason ?? err
60
+ // On network errors before connect, controller will be null
61
+ err = this.#controller?.reason ?? err
61
62
 
62
63
  super.onResponseError(controller, err)
63
64
  }
@@ -17,7 +17,8 @@ const {
17
17
  kMockAgentAddCallHistoryLog,
18
18
  kMockAgentMockCallHistoryInstance,
19
19
  kMockAgentAcceptsNonStandardSearchParameters,
20
- kMockCallHistoryAddLog
20
+ kMockCallHistoryAddLog,
21
+ kIgnoreTrailingSlash
21
22
  } = require('./mock-symbols')
22
23
  const MockClient = require('./mock-client')
23
24
  const MockPool = require('./mock-pool')
@@ -37,6 +38,7 @@ class MockAgent extends Dispatcher {
37
38
  this[kIsMockActive] = true
38
39
  this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
39
40
  this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false
41
+ this[kIgnoreTrailingSlash] = mockOptions?.ignoreTrailingSlash ?? false
40
42
 
41
43
  // Instantiate Agent and encapsulate
42
44
  if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
@@ -54,11 +56,15 @@ class MockAgent extends Dispatcher {
54
56
  }
55
57
 
56
58
  get (origin) {
57
- let dispatcher = this[kMockAgentGet](origin)
59
+ const originKey = this[kIgnoreTrailingSlash]
60
+ ? origin.replace(/\/$/, '')
61
+ : origin
62
+
63
+ let dispatcher = this[kMockAgentGet](originKey)
58
64
 
59
65
  if (!dispatcher) {
60
- dispatcher = this[kFactory](origin)
61
- this[kMockAgentSet](origin, dispatcher)
66
+ dispatcher = this[kFactory](originKey)
67
+ this[kMockAgentSet](originKey, dispatcher)
62
68
  }
63
69
  return dispatcher
64
70
  }