undici 7.12.0 → 7.14.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,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
4
- const { URL } = require('node:url')
3
+ const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
5
4
  const Agent = require('./agent')
6
5
  const Pool = require('./pool')
7
6
  const DispatcherBase = require('./dispatcher-base')
@@ -27,61 +26,69 @@ function defaultFactory (origin, opts) {
27
26
 
28
27
  const noop = () => {}
29
28
 
30
- class ProxyClient extends DispatcherBase {
31
- #client = null
32
- constructor (origin, opts) {
33
- if (typeof origin === 'string') {
34
- origin = new URL(origin)
35
- }
29
+ function defaultAgentFactory (origin, opts) {
30
+ if (opts.connections === 1) {
31
+ return new Client(origin, opts)
32
+ }
33
+ return new Pool(origin, opts)
34
+ }
36
35
 
37
- if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
38
- throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
39
- }
36
+ class Http1ProxyWrapper extends DispatcherBase {
37
+ #client
40
38
 
39
+ constructor (proxyUrl, { headers = {}, connect, factory }) {
41
40
  super()
41
+ if (!proxyUrl) {
42
+ throw new InvalidArgumentError('Proxy URL is mandatory')
43
+ }
42
44
 
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()
45
+ this[kProxyHeaders] = headers
46
+ if (factory) {
47
+ this.#client = factory(proxyUrl, { connect })
48
+ } else {
49
+ this.#client = new Client(proxyUrl, { connect })
50
+ }
52
51
  }
53
52
 
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 })
53
+ [kDispatch] (opts, handler) {
54
+ const onHeaders = handler.onHeaders
55
+ handler.onHeaders = function (statusCode, data, resume) {
56
+ if (statusCode === 407) {
57
+ if (typeof handler.onError === 'function') {
58
+ handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
73
59
  }
60
+ return
74
61
  }
75
- )
76
- return
62
+ if (onHeaders) onHeaders.call(this, statusCode, data, resume)
77
63
  }
78
- if (typeof origin === 'string') {
79
- opts.origin = new URL(origin)
64
+
65
+ // Rewrite request as an HTTP1 Proxy request, without tunneling.
66
+ const {
67
+ origin,
68
+ path = '/',
69
+ headers = {}
70
+ } = opts
71
+
72
+ opts.path = origin + path
73
+
74
+ if (!('host' in headers) && !('Host' in headers)) {
75
+ const { host } = new URL(origin)
76
+ headers.host = host
80
77
  }
78
+ opts.headers = { ...this[kProxyHeaders], ...headers }
79
+
80
+ return this.#client[kDispatch](opts, handler)
81
+ }
81
82
 
82
- return this.#client.dispatch(opts, handler)
83
+ async [kClose] () {
84
+ return this.#client.close()
85
+ }
86
+
87
+ async [kDestroy] (err) {
88
+ return this.#client.destroy(err)
83
89
  }
84
90
  }
91
+
85
92
  class ProxyAgent extends DispatcherBase {
86
93
  constructor (opts) {
87
94
  if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@@ -104,6 +111,7 @@ class ProxyAgent extends DispatcherBase {
104
111
  this[kRequestTls] = opts.requestTls
105
112
  this[kProxyTls] = opts.proxyTls
106
113
  this[kProxyHeaders] = opts.headers || {}
114
+ this[kTunnelProxy] = proxyTunnel
107
115
 
108
116
  if (opts.auth && opts.token) {
109
117
  throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
@@ -116,21 +124,25 @@ class ProxyAgent extends DispatcherBase {
116
124
  this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
117
125
  }
118
126
 
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
127
  const connect = buildConnector({ ...opts.proxyTls })
129
128
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
130
- this[kClient] = clientFactory(url, { connect, factory })
131
- this[kTunnelProxy] = proxyTunnel
129
+
130
+ const agentFactory = opts.factory || defaultAgentFactory
131
+ const factory = (origin, options) => {
132
+ const { protocol } = new URL(origin)
133
+ if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
134
+ return new Http1ProxyWrapper(this[kProxy].uri, {
135
+ headers: this[kProxyHeaders],
136
+ connect,
137
+ factory: agentFactory
138
+ })
139
+ }
140
+ return agentFactory(origin, options)
141
+ }
142
+ this[kClient] = clientFactory(url, { connect })
132
143
  this[kAgent] = new Agent({
133
144
  ...opts,
145
+ factory,
134
146
  connect: async (opts, callback) => {
135
147
  let requestedPath = opts.host
136
148
  if (!opts.port) {
@@ -185,10 +197,6 @@ class ProxyAgent extends DispatcherBase {
185
197
  headers.host = host
186
198
  }
187
199
 
188
- if (!this.#shouldConnect(new URL(opts.origin))) {
189
- opts.path = opts.origin + opts.path
190
- }
191
-
192
200
  return this[kAgent].dispatch(
193
201
  {
194
202
  ...opts,
@@ -199,7 +207,7 @@ class ProxyAgent extends DispatcherBase {
199
207
  }
200
208
 
201
209
  /**
202
- * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts
210
+ * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts
203
211
  * @returns {URL}
204
212
  */
205
213
  #getUrl (opts) {
@@ -221,19 +229,6 @@ class ProxyAgent extends DispatcherBase {
221
229
  await this[kAgent].destroy()
222
230
  await this[kClient].destroy()
223
231
  }
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
232
  }
238
233
 
239
234
  /**
@@ -15,6 +15,15 @@ const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
15
15
  200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
16
16
  ]
17
17
 
18
+ // Status codes which semantic is not handled by the cache
19
+ // https://datatracker.ietf.org/doc/html/rfc9111#section-3
20
+ // This list should not grow beyond 206 and 304 unless the RFC is updated
21
+ // by a newer one including more. Please introduce another list if
22
+ // implementing caching of responses with the 'must-understand' directive.
23
+ const NOT_UNDERSTOOD_STATUS_CODES = [
24
+ 206, 304
25
+ ]
26
+
18
27
  const MAX_RESPONSE_AGE = 2147483647000
19
28
 
20
29
  /**
@@ -241,10 +250,19 @@ class CacheHandler {
241
250
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
242
251
  */
243
252
  function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
244
- // Allow caching for status codes 200 and 307 (original behavior)
245
- // Also allow caching for other status codes that are heuristically cacheable
246
- // when they have explicit cache directives
247
- if (statusCode !== 200 && statusCode !== 307 && !HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)) {
253
+ // Status code must be final and understood.
254
+ if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
255
+ return false
256
+ }
257
+ // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
258
+ // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
259
+ if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] &&
260
+ !cacheControlDirectives.public &&
261
+ cacheControlDirectives['max-age'] === undefined &&
262
+ // RFC 9111: a private response directive, if the cache is not shared
263
+ !(cacheControlDirectives.private && cacheType === 'private') &&
264
+ !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
265
+ ) {
248
266
  return false
249
267
  }
250
268
 
@@ -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
@@ -6,7 +6,7 @@ const util = require('../core/util')
6
6
  const CacheHandler = require('../handler/cache-handler')
7
7
  const MemoryCacheStore = require('../cache/memory-cache-store')
8
8
  const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
9
- const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js')
9
+ const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
10
10
  const { AbortError } = require('../core/errors.js')
11
11
 
12
12
  /**
@@ -326,7 +326,7 @@ module.exports = (opts = {}) => {
326
326
 
327
327
  opts = {
328
328
  ...opts,
329
- headers: normaliseHeaders(opts)
329
+ headers: normalizeHeaders(opts)
330
330
  }
331
331
 
332
332
  const reqCacheControl = opts.headers?.['cache-control']
@@ -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
  }