undici 8.2.0 → 8.4.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 (45) hide show
  1. package/README.md +67 -23
  2. package/docs/docs/api/Agent.md +3 -0
  3. package/docs/docs/api/Client.md +43 -5
  4. package/docs/docs/api/Connector.md +1 -0
  5. package/docs/docs/api/Dispatcher.md +7 -0
  6. package/docs/docs/api/Errors.md +12 -0
  7. package/docs/docs/api/EventSource.md +50 -3
  8. package/docs/docs/api/Fetch.md +3 -1
  9. package/docs/docs/api/GlobalInstallation.md +7 -5
  10. package/docs/docs/api/H2CClient.md +2 -2
  11. package/docs/docs/api/Pool.md +3 -0
  12. package/docs/docs/api/RedirectHandler.md +4 -1
  13. package/docs/docs/api/SnapshotAgent.md +23 -0
  14. package/lib/api/api-pipeline.js +4 -0
  15. package/lib/api/api-stream.js +51 -5
  16. package/lib/core/connect.js +29 -4
  17. package/lib/core/symbols.js +1 -0
  18. package/lib/core/util.js +10 -8
  19. package/lib/dispatcher/client-h1.js +59 -18
  20. package/lib/dispatcher/client-h2.js +418 -298
  21. package/lib/dispatcher/client.js +25 -4
  22. package/lib/dispatcher/pool-base.js +21 -3
  23. package/lib/dispatcher/pool.js +23 -0
  24. package/lib/dispatcher/proxy-agent.js +21 -4
  25. package/lib/dispatcher/round-robin-pool.js +26 -0
  26. package/lib/dispatcher/socks5-proxy-agent.js +19 -19
  27. package/lib/handler/redirect-handler.js +36 -11
  28. package/lib/handler/retry-handler.js +14 -0
  29. package/lib/interceptor/redirect.js +3 -3
  30. package/lib/mock/mock-call-history.js +1 -1
  31. package/lib/mock/mock-utils.js +3 -1
  32. package/lib/mock/snapshot-agent.js +11 -1
  33. package/lib/mock/snapshot-recorder.js +38 -3
  34. package/lib/web/fetch/body.js +2 -7
  35. package/lib/web/fetch/formdata.js +21 -2
  36. package/lib/web/fetch/index.js +19 -3
  37. package/lib/web/fetch/request.js +32 -3
  38. package/package.json +4 -4
  39. package/types/client.d.ts +7 -7
  40. package/types/connector.d.ts +1 -0
  41. package/types/dispatcher.d.ts +0 -2
  42. package/types/fetch.d.ts +4 -1
  43. package/types/formdata.d.ts +0 -6
  44. package/types/interceptors.d.ts +1 -1
  45. package/types/snapshot-agent.d.ts +4 -0
@@ -3,7 +3,7 @@
3
3
  const net = require('node:net')
4
4
  const assert = require('node:assert')
5
5
  const util = require('./util')
6
- const { InvalidArgumentError } = require('./errors')
6
+ const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
7
7
 
8
8
  let tls // include tls conditionally since it is not always available
9
9
 
@@ -59,7 +59,7 @@ const SessionCache = class WeakSessionCache {
59
59
  }
60
60
  }
61
61
 
62
- function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
62
+ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
63
63
  if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
64
64
  throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
65
65
  }
@@ -89,7 +89,7 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
89
89
  servername,
90
90
  session,
91
91
  localAddress,
92
- ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
92
+ ALPNProtocols: allowH2 ? (preferH2 ? ['h2', 'http/1.1'] : ['http/1.1', 'h2']) : ['http/1.1'],
93
93
  socket: httpSocket, // upgrade socket connection
94
94
  port,
95
95
  host: hostname
@@ -142,7 +142,7 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
142
142
  if (callback) {
143
143
  const cb = callback
144
144
  callback = null
145
- cb(err)
145
+ cb(maybeNormalizeConnectError(err, this, { timeout, hostname, port }))
146
146
  }
147
147
  })
148
148
 
@@ -150,4 +150,29 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
150
150
  }
151
151
  }
152
152
 
153
+ // `net.connect` with `autoSelectFamily` raises an `AggregateError` when every
154
+ // attempted address fails. If any of those failures is a timeout, surface the
155
+ // error as a `ConnectTimeoutError` so callers see the same error regardless of
156
+ // which timer (Node's internal one or undici's `connectTimeout`) wins the race.
157
+ // The original `AggregateError` is preserved on `.cause`.
158
+ function maybeNormalizeConnectError (err, socket, opts) {
159
+ if (
160
+ err instanceof AggregateError &&
161
+ (err.code === 'ETIMEDOUT' || err.errors.some((e) => e != null && e.code === 'ETIMEDOUT'))
162
+ ) {
163
+ let message = 'Connect Timeout Error'
164
+ if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
165
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
166
+ } else {
167
+ message += ` (attempted address: ${opts.hostname}:${opts.port},`
168
+ }
169
+ message += ` timeout: ${opts.timeout}ms)`
170
+
171
+ const wrapped = new ConnectTimeoutError(message)
172
+ wrapped.cause = err
173
+ return wrapped
174
+ }
175
+ return err
176
+ }
177
+
153
178
  module.exports = buildConnector
@@ -62,6 +62,7 @@ module.exports = {
62
62
  kListeners: Symbol('listeners'),
63
63
  kHTTPContext: Symbol('http context'),
64
64
  kMaxConcurrentStreams: Symbol('max concurrent streams'),
65
+ kHostAuthority: Symbol('host authority'),
65
66
  kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
66
67
  kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
67
68
  kEnableConnectProtocol: Symbol('http2session connect protocol'),
package/lib/core/util.js CHANGED
@@ -698,9 +698,8 @@ function isFormDataLike (object) {
698
698
  }
699
699
 
700
700
  function addAbortListener (signal, listener) {
701
- if (signal instanceof AbortSignal) {
702
- const disposable = addAbortListenerNative(signal, listener)
703
- return () => disposable[Symbol.dispose]()
701
+ if (!signal || 'aborted' in signal) {
702
+ return addAbortListenerNative(signal, listener)[Symbol.dispose]
704
703
  }
705
704
 
706
705
  if (typeof signal.addEventListener === 'function') {
@@ -776,7 +775,7 @@ function isValidHeaderValue (characters) {
776
775
  return !headerCharRegex.test(characters)
777
776
  }
778
777
 
779
- const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
778
+ const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+|\*)?$/
780
779
 
781
780
  /**
782
781
  * @typedef {object} RangeHeader
@@ -793,13 +792,14 @@ const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
793
792
  */
794
793
  function parseRangeHeader (range) {
795
794
  if (range == null || range === '') return { start: 0, end: null, size: null }
795
+ if (!range) return null
796
796
 
797
- const m = range ? range.match(rangeHeaderRegex) : null
797
+ const m = rangeHeaderRegex.exec(range)
798
798
  return m
799
799
  ? {
800
800
  start: parseInt(m[1]),
801
801
  end: m[2] ? parseInt(m[2]) : null,
802
- size: m[3] ? parseInt(m[3]) : null
802
+ size: m[3] && m[3] !== '*' ? parseInt(m[3]) : null
803
803
  }
804
804
  : null
805
805
  }
@@ -943,8 +943,10 @@ function getProtocolFromUrlString (urlString) {
943
943
  return urlString.slice(0, urlString.indexOf(':') + 1)
944
944
  }
945
945
 
946
- const kEnumerableProperty = Object.create(null)
947
- kEnumerableProperty.enumerable = true
946
+ const kEnumerableProperty = {
947
+ __proto__: null,
948
+ enumerable: true
949
+ }
948
950
 
949
951
  const normalizedMethodRecordsBase = {
950
952
  delete: 'DELETE',
@@ -360,16 +360,7 @@ class Parser {
360
360
  this.paused = true
361
361
  socket.unshift(data)
362
362
  } else {
363
- const ptr = llhttp.llhttp_get_error_reason(this.ptr)
364
- let message = ''
365
- if (ptr) {
366
- const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
367
- message =
368
- 'Response does not match the HTTP/1.1 protocol (' +
369
- Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
370
- ')'
371
- }
372
- throw new HTTPParserError(message, constants.ERROR[ret], data)
363
+ throw this.createError(ret, data)
373
364
  }
374
365
  }
375
366
  } catch (err) {
@@ -377,6 +368,54 @@ class Parser {
377
368
  }
378
369
  }
379
370
 
371
+ finish () {
372
+ assert(currentParser === null)
373
+ assert(this.ptr != null)
374
+ assert(!this.paused)
375
+
376
+ const { llhttp } = this
377
+
378
+ let ret
379
+
380
+ try {
381
+ currentParser = this
382
+ ret = llhttp.llhttp_finish(this.ptr)
383
+ } finally {
384
+ currentParser = null
385
+ }
386
+
387
+ if (ret === constants.ERROR.OK) {
388
+ return null
389
+ }
390
+
391
+ if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
392
+ this.paused = true
393
+ return null
394
+ }
395
+
396
+ return this.createError(ret, EMPTY_BUF)
397
+ }
398
+
399
+ createError (ret, data) {
400
+ const { llhttp, contentLength, bytesRead } = this
401
+
402
+ if (contentLength !== -1 && bytesRead !== contentLength) {
403
+ return new ResponseContentLengthMismatchError()
404
+ }
405
+
406
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
407
+ let message = ''
408
+ if (ptr) {
409
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
410
+ message =
411
+ 'Response does not match the HTTP/1.1 protocol (' +
412
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
413
+ ')'
414
+ }
415
+
416
+ return new HTTPParserError(message, constants.ERROR[ret], data)
417
+ }
418
+
380
419
  destroy () {
381
420
  assert(currentParser === null)
382
421
  assert(this.ptr != null)
@@ -888,8 +927,11 @@ function onHttpSocketError (err) {
888
927
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
889
928
  // to the user.
890
929
  if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
891
- // We treat all incoming data so for as a valid response.
892
- parser.onMessageComplete()
930
+ const parserErr = parser.finish()
931
+ if (parserErr) {
932
+ this[kError] = parserErr
933
+ this[kClient][kOnError](parserErr)
934
+ }
893
935
  return
894
936
  }
895
937
 
@@ -906,8 +948,10 @@ function onHttpSocketEnd () {
906
948
  const parser = this[kParser]
907
949
 
908
950
  if (parser.statusCode && !parser.shouldKeepAlive) {
909
- // We treat all incoming data so far as a valid response.
910
- parser.onMessageComplete()
951
+ const parserErr = parser.finish()
952
+ if (parserErr) {
953
+ util.destroy(this, parserErr)
954
+ }
911
955
  return
912
956
  }
913
957
 
@@ -919,8 +963,7 @@ function onHttpSocketClose () {
919
963
 
920
964
  if (parser) {
921
965
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
922
- // We treat all incoming data so far as a valid response.
923
- parser.onMessageComplete()
966
+ this[kError] = parser.finish() || this[kError]
924
967
  }
925
968
 
926
969
  this[kParser].destroy()
@@ -1382,8 +1425,6 @@ function writeBuffer (abort, body, client, request, socket, contentLength, heade
1382
1425
  * @returns {Promise<void>}
1383
1426
  */
1384
1427
  async function writeBlob (abort, body, client, request, socket, contentLength, header, expectsPayload) {
1385
- assert(contentLength === body.size, 'blob body must have content length')
1386
-
1387
1428
  try {
1388
1429
  if (contentLength != null && contentLength !== body.size) {
1389
1430
  throw new RequestContentLengthMismatchError()