undici 7.10.0 → 7.12.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 (69) hide show
  1. package/README.md +159 -0
  2. package/docs/docs/api/CacheStore.md +3 -3
  3. package/docs/docs/api/Debug.md +13 -13
  4. package/docs/docs/api/DiagnosticsChannel.md +32 -4
  5. package/docs/docs/api/Dispatcher.md +22 -3
  6. package/docs/docs/api/GlobalInstallation.md +91 -0
  7. package/docs/docs/api/MockClient.md +4 -0
  8. package/docs/docs/api/MockPool.md +6 -0
  9. package/docs/docs/api/ProxyAgent.md +2 -0
  10. package/docs/docs/api/RetryAgent.md +6 -1
  11. package/docs/docs/api/RetryHandler.md +1 -0
  12. package/docs/docs/api/WebSocket.md +27 -0
  13. package/index.js +18 -1
  14. package/lib/api/api-stream.js +1 -1
  15. package/lib/api/readable.js +1 -3
  16. package/lib/cache/memory-cache-store.js +3 -3
  17. package/lib/cache/sqlite-cache-store.js +1 -1
  18. package/lib/core/connect.js +21 -51
  19. package/lib/core/diagnostics.js +6 -4
  20. package/lib/core/request.js +12 -1
  21. package/lib/core/tree.js +1 -1
  22. package/lib/core/util.js +0 -45
  23. package/lib/dispatcher/client-h1.js +9 -18
  24. package/lib/dispatcher/proxy-agent.js +2 -1
  25. package/lib/handler/cache-handler.js +4 -1
  26. package/lib/handler/redirect-handler.js +2 -2
  27. package/lib/handler/retry-handler.js +110 -56
  28. package/lib/interceptor/cache.js +2 -2
  29. package/lib/interceptor/redirect.js +1 -1
  30. package/lib/mock/mock-client.js +4 -0
  31. package/lib/mock/mock-pool.js +4 -0
  32. package/lib/util/cache.js +12 -2
  33. package/lib/util/promise.js +28 -0
  34. package/lib/util/timers.js +11 -9
  35. package/lib/web/cache/cache.js +11 -9
  36. package/lib/web/cache/cachestorage.js +1 -1
  37. package/lib/web/cookies/index.js +1 -1
  38. package/lib/web/eventsource/eventsource.js +3 -6
  39. package/lib/web/eventsource/util.js +1 -1
  40. package/lib/web/fetch/body.js +36 -24
  41. package/lib/web/fetch/formdata-parser.js +4 -4
  42. package/lib/web/fetch/formdata.js +1 -1
  43. package/lib/web/fetch/headers.js +1 -1
  44. package/lib/web/fetch/index.js +228 -226
  45. package/lib/web/fetch/request.js +16 -8
  46. package/lib/web/fetch/response.js +6 -4
  47. package/lib/web/fetch/util.js +23 -25
  48. package/lib/web/{fetch/webidl.js → webidl/index.js} +57 -9
  49. package/lib/web/websocket/connection.js +4 -12
  50. package/lib/web/websocket/events.js +1 -1
  51. package/lib/web/websocket/frame.js +2 -1
  52. package/lib/web/websocket/receiver.js +2 -12
  53. package/lib/web/websocket/stream/websocketerror.js +1 -1
  54. package/lib/web/websocket/stream/websocketstream.js +8 -5
  55. package/lib/web/websocket/websocket.js +61 -5
  56. package/package.json +5 -5
  57. package/types/diagnostics-channel.d.ts +9 -0
  58. package/types/dispatcher.d.ts +3 -2
  59. package/types/env-http-proxy-agent.d.ts +2 -1
  60. package/types/eventsource.d.ts +3 -3
  61. package/types/fetch.d.ts +1 -0
  62. package/types/handlers.d.ts +1 -1
  63. package/types/mock-client.d.ts +2 -0
  64. package/types/mock-interceptor.d.ts +2 -0
  65. package/types/mock-pool.d.ts +2 -0
  66. package/types/retry-handler.d.ts +9 -0
  67. package/types/webidl.d.ts +29 -15
  68. package/types/websocket.d.ts +3 -1
  69. package/lib/web/fetch/dispatcher-weakref.js +0 -46
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
4
- const { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, streamRegistry, bodyUnusable } = require('./body')
4
+ const { extractBody, cloneBody, mixinBody, streamRegistry, bodyUnusable } = require('./body')
5
5
  const util = require('../../core/util')
6
6
  const nodeUtil = require('node:util')
7
7
  const { kEnumerableProperty } = util
@@ -18,7 +18,7 @@ const {
18
18
  redirectStatusSet,
19
19
  nullBodyStatus
20
20
  } = require('./constants')
21
- const { webidl } = require('./webidl')
21
+ const { webidl } = require('../webidl')
22
22
  const { URLSerializer } = require('./data-url')
23
23
  const { kConstruct } = require('../../core/symbols')
24
24
  const assert = require('node:assert')
@@ -352,7 +352,9 @@ function cloneResponse (response) {
352
352
  // 3. If response’s body is non-null, then set newResponse’s body to the
353
353
  // result of cloning response’s body.
354
354
  if (response.body != null) {
355
- newResponse.body = cloneBody(newResponse, response.body)
355
+ newResponse.body = cloneBody(response.body)
356
+
357
+ streamRegistry.register(newResponse, new WeakRef(response.body.stream))
356
358
  }
357
359
 
358
360
  // 4. Return newResponse.
@@ -552,7 +554,7 @@ function fromInnerResponse (innerResponse, guard) {
552
554
  setHeadersList(headers, innerResponse.headersList)
553
555
  setHeadersGuard(headers, guard)
554
556
 
555
- if (hasFinalizationRegistry && innerResponse.body?.stream) {
557
+ if (innerResponse.body?.stream) {
556
558
  // If the target (response) is reclaimed, the cleanup callback may be called at some point with
557
559
  // the held value provided for it (innerResponse.body.stream). The held value can be any value:
558
560
  // a primitive or an object, even undefined. If the held value is an object, the registry keeps
@@ -9,7 +9,7 @@ const { performance } = require('node:perf_hooks')
9
9
  const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
10
10
  const assert = require('node:assert')
11
11
  const { isUint8Array } = require('node:util/types')
12
- const { webidl } = require('./webidl')
12
+ const { webidl } = require('../webidl')
13
13
 
14
14
  let supportedHashes = []
15
15
 
@@ -924,17 +924,6 @@ function sameOrigin (A, B) {
924
924
  return false
925
925
  }
926
926
 
927
- function createDeferredPromise () {
928
- let res
929
- let rej
930
- const promise = new Promise((resolve, reject) => {
931
- res = resolve
932
- rej = reject
933
- })
934
-
935
- return { promise, resolve: res, reject: rej }
936
- }
937
-
938
927
  function isAborted (fetchParams) {
939
928
  return fetchParams.controller.state === 'aborted'
940
929
  }
@@ -1177,6 +1166,11 @@ function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueInde
1177
1166
  }
1178
1167
 
1179
1168
  /**
1169
+ * @param {import('./body').ExtractBodyResult} body
1170
+ * @param {(bytes: Uint8Array) => void} processBody
1171
+ * @param {(error: Error) => void} processBodyError
1172
+ * @returns {void}
1173
+ *
1180
1174
  * @see https://fetch.spec.whatwg.org/#body-fully-read
1181
1175
  */
1182
1176
  function fullyReadBody (body, processBody, processBodyError) {
@@ -1191,20 +1185,17 @@ function fullyReadBody (body, processBody, processBodyError) {
1191
1185
  // with taskDestination.
1192
1186
  const errorSteps = processBodyError
1193
1187
 
1188
+ try {
1194
1189
  // 4. Let reader be the result of getting a reader for body’s stream.
1195
1190
  // If that threw an exception, then run errorSteps with that
1196
1191
  // exception and return.
1197
- let reader
1192
+ const reader = body.stream.getReader()
1198
1193
 
1199
- try {
1200
- reader = body.stream.getReader()
1194
+ // 5. Read all bytes from reader, given successSteps and errorSteps.
1195
+ readAllBytes(reader, successSteps, errorSteps)
1201
1196
  } catch (e) {
1202
1197
  errorSteps(e)
1203
- return
1204
1198
  }
1205
-
1206
- // 5. Read all bytes from reader, given successSteps and errorSteps.
1207
- readAllBytes(reader, successSteps, errorSteps)
1208
1199
  }
1209
1200
 
1210
1201
  /**
@@ -1241,15 +1232,16 @@ function isomorphicEncode (input) {
1241
1232
  /**
1242
1233
  * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
1243
1234
  * @see https://streams.spec.whatwg.org/#read-loop
1244
- * @param {ReadableStreamDefaultReader} reader
1235
+ * @param {ReadableStream<Uint8Array<ArrayBuffer>>} reader
1245
1236
  * @param {(bytes: Uint8Array) => void} successSteps
1246
1237
  * @param {(error: Error) => void} failureSteps
1238
+ * @returns {Promise<void>}
1247
1239
  */
1248
1240
  async function readAllBytes (reader, successSteps, failureSteps) {
1249
- const bytes = []
1250
- let byteLength = 0
1251
-
1252
1241
  try {
1242
+ const bytes = []
1243
+ let byteLength = 0
1244
+
1253
1245
  do {
1254
1246
  const { done, value: chunk } = await reader.read()
1255
1247
 
@@ -1262,7 +1254,7 @@ async function readAllBytes (reader, successSteps, failureSteps) {
1262
1254
  // 1. If chunk is not a Uint8Array object, call failureSteps
1263
1255
  // with a TypeError and abort these steps.
1264
1256
  if (!isUint8Array(chunk)) {
1265
- failureSteps(TypeError('Received non-Uint8Array chunk'))
1257
+ failureSteps(new TypeError('Received non-Uint8Array chunk'))
1266
1258
  return
1267
1259
  }
1268
1260
 
@@ -1324,10 +1316,17 @@ function urlIsHttpHttpsScheme (url) {
1324
1316
  return protocol === 'http:' || protocol === 'https:'
1325
1317
  }
1326
1318
 
1319
+ /**
1320
+ * @typedef {Object} RangeHeaderValue
1321
+ * @property {number|null} rangeStartValue
1322
+ * @property {number|null} rangeEndValue
1323
+ */
1324
+
1327
1325
  /**
1328
1326
  * @see https://fetch.spec.whatwg.org/#simple-range-header-value
1329
1327
  * @param {string} value
1330
1328
  * @param {boolean} allowWhitespace
1329
+ * @return {RangeHeaderValue|'failure'}
1331
1330
  */
1332
1331
  function simpleRangeHeaderValue (value, allowWhitespace) {
1333
1332
  // 1. Let data be the isomorphic decoding of value.
@@ -1732,7 +1731,6 @@ module.exports = {
1732
1731
  isAborted,
1733
1732
  isCancelled,
1734
1733
  isValidEncodedURL,
1735
- createDeferredPromise,
1736
1734
  ReadableStreamFrom,
1737
1735
  tryUpgradeRequestToAPotentiallyTrustworthyURL,
1738
1736
  clampAndCoarsenConnectionTimingInfo,
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { types, inspect } = require('node:util')
4
4
  const { markAsUncloneable } = require('node:worker_threads')
5
- const { toUSVString } = require('../../core/util')
6
5
 
7
6
  const UNDEFINED = 1
8
7
  const BOOLEAN = 2
@@ -23,22 +22,48 @@ const webidl = {
23
22
  is: {}
24
23
  }
25
24
 
25
+ /**
26
+ * @description Instantiate an error.
27
+ *
28
+ * @param {Object} opts
29
+ * @param {string} opts.header
30
+ * @param {string} opts.message
31
+ * @returns {TypeError}
32
+ */
26
33
  webidl.errors.exception = function (message) {
27
34
  return new TypeError(`${message.header}: ${message.message}`)
28
35
  }
29
36
 
30
- webidl.errors.conversionFailed = function (context) {
31
- const plural = context.types.length === 1 ? '' : ' one of'
37
+ /**
38
+ * @description Instantiate an error when conversion from one type to another has failed.
39
+ *
40
+ * @param {Object} opts
41
+ * @param {string} opts.prefix
42
+ * @param {string} opts.argument
43
+ * @param {string[]} opts.types
44
+ * @returns {TypeError}
45
+ */
46
+ webidl.errors.conversionFailed = function (opts) {
47
+ const plural = opts.types.length === 1 ? '' : ' one of'
32
48
  const message =
33
- `${context.argument} could not be converted to` +
34
- `${plural}: ${context.types.join(', ')}.`
49
+ `${opts.argument} could not be converted to` +
50
+ `${plural}: ${opts.types.join(', ')}.`
35
51
 
36
52
  return webidl.errors.exception({
37
- header: context.prefix,
53
+ header: opts.prefix,
38
54
  message
39
55
  })
40
56
  }
41
57
 
58
+ /**
59
+ * @description Instantiate an error when an invalid argument is provided
60
+ *
61
+ * @param {Object} context
62
+ * @param {string} context.prefix
63
+ * @param {string} context.value
64
+ * @param {string} context.type
65
+ * @returns {TypeError}
66
+ */
42
67
  webidl.errors.invalidArgument = function (context) {
43
68
  return webidl.errors.exception({
44
69
  header: context.prefix,
@@ -278,6 +303,8 @@ webidl.util.Stringify = function (V) {
278
303
  return inspect(V)
279
304
  case STRING:
280
305
  return `"${V}"`
306
+ case BIGINT:
307
+ return `${V}n`
281
308
  default:
282
309
  return `${V}`
283
310
  }
@@ -468,6 +495,17 @@ webidl.nullableConverter = function (converter) {
468
495
  }
469
496
  }
470
497
 
498
+ /**
499
+ * @param {*} value
500
+ * @returns {boolean}
501
+ */
502
+ webidl.is.USVString = function (value) {
503
+ return (
504
+ typeof value === 'string' &&
505
+ value.isWellFormed()
506
+ )
507
+ }
508
+
471
509
  webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
472
510
  webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
473
511
  webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
@@ -529,13 +567,23 @@ webidl.converters.ByteString = function (V, prefix, argument) {
529
567
  return x
530
568
  }
531
569
 
532
- // https://webidl.spec.whatwg.org/#es-USVString
533
- // TODO: rewrite this so we can control the errors thrown
534
- webidl.converters.USVString = toUSVString
570
+ /**
571
+ * @param {unknown} value
572
+ * @returns {string}
573
+ * @see https://webidl.spec.whatwg.org/#es-USVString
574
+ */
575
+ webidl.converters.USVString = function (value) {
576
+ // TODO: rewrite this so we can control the errors thrown
577
+ if (typeof value === 'string') {
578
+ return value.toWellFormed()
579
+ }
580
+ return `${value}`.toWellFormed()
581
+ }
535
582
 
536
583
  // https://webidl.spec.whatwg.org/#es-boolean
537
584
  webidl.converters.boolean = function (V) {
538
585
  // 1. Let x be the result of computing ToBoolean(V).
586
+ // https://262.ecma-international.org/10.0/index.html#table-10
539
587
  const x = Boolean(V)
540
588
 
541
589
  // 2. Return the IDL boolean value that is the one that represents
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
4
4
  const { parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
5
- const { channels } = require('../../core/diagnostics')
6
5
  const { makeRequest } = require('../fetch/request')
7
6
  const { fetching } = require('../fetch/index')
8
7
  const { Headers, getHeadersList } = require('../fetch/headers')
@@ -105,7 +104,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
105
104
  // 1. If response is a network error or its status is not 101,
106
105
  // fail the WebSocket connection.
107
106
  if (response.type === 'error' || response.status !== 101) {
108
- failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.')
107
+ failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
109
108
  return
110
109
  }
111
110
 
@@ -200,14 +199,6 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
200
199
  response.socket.on('close', handler.onSocketClose)
201
200
  response.socket.on('error', handler.onSocketError)
202
201
 
203
- if (channels.open.hasSubscribers) {
204
- channels.open.publish({
205
- address: response.socket.address(),
206
- protocol: secProtocol,
207
- extensions: secExtension
208
- })
209
- }
210
-
211
202
  handler.wasEverConnected = true
212
203
  handler.onConnectionEstablished(response, extensions)
213
204
  }
@@ -298,9 +289,10 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
298
289
  * @param {import('./websocket').Handler} handler
299
290
  * @param {number} code
300
291
  * @param {string|undefined} reason
292
+ * @param {unknown} cause
301
293
  * @returns {void}
302
294
  */
303
- function failWebsocketConnection (handler, code, reason) {
295
+ function failWebsocketConnection (handler, code, reason, cause) {
304
296
  // If _The WebSocket Connection is Established_ prior to the point where
305
297
  // the endpoint is required to _Fail the WebSocket Connection_, the
306
298
  // endpoint SHOULD send a Close frame with an appropriate status code
@@ -315,7 +307,7 @@ function failWebsocketConnection (handler, code, reason) {
315
307
  handler.socket.destroy()
316
308
  }
317
309
 
318
- handler.onFail(code, reason)
310
+ handler.onFail(code, reason, cause)
319
311
  }
320
312
 
321
313
  module.exports = {
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { webidl } = require('../fetch/webidl')
3
+ const { webidl } = require('../webidl')
4
4
  const { kEnumerableProperty } = require('../../core/util')
5
5
  const { kConstruct } = require('../../core/symbols')
6
6
 
@@ -134,5 +134,6 @@ class WebsocketFrameSend {
134
134
  }
135
135
 
136
136
  module.exports = {
137
- WebsocketFrameSend
137
+ WebsocketFrameSend,
138
+ generateMask // for benchmark
138
139
  }
@@ -3,7 +3,6 @@
3
3
  const { Writable } = require('node:stream')
4
4
  const assert = require('node:assert')
5
5
  const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
6
- const { channels } = require('../../core/diagnostics')
7
6
  const {
8
7
  isValidStatusCode,
9
8
  isValidOpcode,
@@ -423,22 +422,13 @@ class ByteParser extends Writable {
423
422
 
424
423
  this.#handler.socket.write(frame.createFrame(opcodes.PONG))
425
424
 
426
- if (channels.ping.hasSubscribers) {
427
- channels.ping.publish({
428
- payload: body
429
- })
430
- }
425
+ this.#handler.onPing(body)
431
426
  }
432
427
  } else if (opcode === opcodes.PONG) {
433
428
  // A Pong frame MAY be sent unsolicited. This serves as a
434
429
  // unidirectional heartbeat. A response to an unsolicited Pong frame is
435
430
  // not expected.
436
-
437
- if (channels.pong.hasSubscribers) {
438
- channels.pong.publish({
439
- payload: body
440
- })
441
- }
431
+ this.#handler.onPong(body)
442
432
  }
443
433
 
444
434
  return true
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { webidl } = require('../../fetch/webidl')
3
+ const { webidl } = require('../../webidl')
4
4
  const { validateCloseCodeAndReason } = require('../util')
5
5
  const { kConstruct } = require('../../../core/symbols')
6
6
  const { kEnumerableProperty } = require('../../../core/util')
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { createDeferredPromise, environmentSettingsObject } = require('../../fetch/util')
3
+ const { createDeferredPromise } = require('../../../util/promise')
4
+ const { environmentSettingsObject } = require('../../fetch/util')
4
5
  const { states, opcodes, sentCloseFrameState } = require('../constants')
5
- const { webidl } = require('../../fetch/webidl')
6
+ const { webidl } = require('../../webidl')
6
7
  const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
7
8
  const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
8
9
  const { types } = require('node:util')
@@ -21,11 +22,11 @@ class WebSocketStream {
21
22
  #url
22
23
 
23
24
  // Each WebSocketStream object has an associated opened promise , which is a promise.
24
- /** @type {ReturnType<typeof createDeferredPromise>} */
25
+ /** @type {import('../../../util/promise').DeferredPromise} */
25
26
  #openedPromise
26
27
 
27
28
  // Each WebSocketStream object has an associated closed promise , which is a promise.
28
- /** @type {ReturnType<typeof createDeferredPromise>} */
29
+ /** @type {import('../../../util/promise').DeferredPromise} */
29
30
  #closedPromise
30
31
 
31
32
  // Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
@@ -64,6 +65,8 @@ class WebSocketStream {
64
65
  this.#handler.socket.destroy()
65
66
  },
66
67
  onSocketClose: () => this.#onSocketClose(),
68
+ onPing: () => {},
69
+ onPong: () => {},
67
70
 
68
71
  readyState: states.CONNECTING,
69
72
  socket: null,
@@ -388,7 +391,7 @@ class WebSocketStream {
388
391
  // 6. If the connection was closed cleanly ,
389
392
  if (wasClean) {
390
393
  // 6.1. Close stream ’s readable stream .
391
- this.#readableStream.cancel().catch(() => {})
394
+ this.#readableStreamController.close()
392
395
 
393
396
  // 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
394
397
  if (!this.#writableStream.locked) {
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { webidl } = require('../fetch/webidl')
3
+ const { webidl } = require('../webidl')
4
4
  const { URLSerializer } = require('../fetch/data-url')
5
5
  const { environmentSettingsObject } = require('../fetch/util')
6
6
  const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
@@ -8,6 +8,7 @@ const {
8
8
  isConnecting,
9
9
  isEstablished,
10
10
  isClosing,
11
+ isClosed,
11
12
  isValidSubprotocol,
12
13
  fireEvent,
13
14
  utf8Decode,
@@ -21,6 +22,7 @@ const { getGlobalDispatcher } = require('../../global')
21
22
  const { types } = require('node:util')
22
23
  const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
23
24
  const { SendQueue } = require('./sender')
25
+ const { WebsocketFrameSend } = require('./frame')
24
26
  const { channels } = require('../../core/diagnostics')
25
27
 
26
28
  /**
@@ -33,6 +35,8 @@ const { channels } = require('../../core/diagnostics')
33
35
  * @property {(chunk: Buffer) => void} onSocketData
34
36
  * @property {(err: Error) => void} onSocketError
35
37
  * @property {() => void} onSocketClose
38
+ * @property {(body: Buffer) => void} onPing
39
+ * @property {(body: Buffer) => void} onPong
36
40
  *
37
41
  * @property {number} readyState
38
42
  * @property {import('stream').Duplex} socket
@@ -60,7 +64,7 @@ class WebSocket extends EventTarget {
60
64
  /** @type {Handler} */
61
65
  #handler = {
62
66
  onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
63
- onFail: (code, reason) => this.#onFail(code, reason),
67
+ onFail: (code, reason, cause) => this.#onFail(code, reason, cause),
64
68
  onMessage: (opcode, data) => this.#onMessage(opcode, data),
65
69
  onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
66
70
  onParserDrain: () => this.#onParserDrain(),
@@ -79,6 +83,22 @@ class WebSocket extends EventTarget {
79
83
  this.#handler.socket.destroy()
80
84
  },
81
85
  onSocketClose: () => this.#onSocketClose(),
86
+ onPing: (body) => {
87
+ if (channels.ping.hasSubscribers) {
88
+ channels.ping.publish({
89
+ payload: body,
90
+ websocket: this
91
+ })
92
+ }
93
+ },
94
+ onPong: (body) => {
95
+ if (channels.pong.hasSubscribers) {
96
+ channels.pong.publish({
97
+ payload: body,
98
+ websocket: this
99
+ })
100
+ }
101
+ },
82
102
 
83
103
  readyState: states.CONNECTING,
84
104
  socket: null,
@@ -460,13 +480,22 @@ class WebSocket extends EventTarget {
460
480
 
461
481
  // 4. Fire an event named open at the WebSocket object.
462
482
  fireEvent('open', this)
483
+
484
+ if (channels.open.hasSubscribers) {
485
+ channels.open.publish({
486
+ address: response.socket.address(),
487
+ protocol: this.#protocol,
488
+ extensions: this.#extensions,
489
+ websocket: this
490
+ })
491
+ }
463
492
  }
464
493
 
465
- #onFail (code, reason) {
494
+ #onFail (code, reason, cause) {
466
495
  if (reason) {
467
496
  // TODO: process.nextTick
468
497
  fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
469
- error: new Error(reason),
498
+ error: new Error(reason, cause ? { cause } : undefined),
470
499
  message: reason
471
500
  })
472
501
  }
@@ -586,8 +615,34 @@ class WebSocket extends EventTarget {
586
615
  })
587
616
  }
588
617
  }
618
+
619
+ /**
620
+ * @param {WebSocket} ws
621
+ * @param {Buffer|undefined} buffer
622
+ */
623
+ static ping (ws, buffer) {
624
+ if (Buffer.isBuffer(buffer)) {
625
+ if (buffer.length > 125) {
626
+ throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
627
+ }
628
+ } else if (buffer !== undefined) {
629
+ throw new TypeError('Expected buffer payload')
630
+ }
631
+
632
+ // An endpoint MAY send a Ping frame any time after the connection is
633
+ // established and before the connection is closed.
634
+ const readyState = ws.#handler.readyState
635
+
636
+ if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
637
+ const frame = new WebsocketFrameSend(buffer)
638
+ ws.#handler.socket.write(frame.createFrame(opcodes.PING))
639
+ }
640
+ }
589
641
  }
590
642
 
643
+ const { ping } = WebSocket
644
+ Reflect.deleteProperty(WebSocket, 'ping')
645
+
591
646
  // https://websockets.spec.whatwg.org/#dom-websocket-connecting
592
647
  WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
593
648
  // https://websockets.spec.whatwg.org/#dom-websocket-open
@@ -682,5 +737,6 @@ webidl.converters.WebSocketSendData = function (V) {
682
737
  }
683
738
 
684
739
  module.exports = {
685
- WebSocket
740
+ WebSocket,
741
+ ping
686
742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.10.0",
3
+ "version": "7.12.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -91,7 +91,7 @@
91
91
  "test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
92
92
  "test:typescript": "tsd && tsc test/imports/undici-import.ts --typeRoots ./types --noEmit && tsc ./types/*.d.ts --noEmit --typeRoots ./types",
93
93
  "test:webidl": "borp -p \"test/webidl/*.js\"",
94
- "test:websocket": "borp -p \"test/websocket/*.js\"",
94
+ "test:websocket": "borp -p \"test/websocket/**/*.js\"",
95
95
  "test:websocket:autobahn": "node test/autobahn/client.js",
96
96
  "test:websocket:autobahn:report": "node test/autobahn/report.js",
97
97
  "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
@@ -112,20 +112,20 @@
112
112
  "@sinonjs/fake-timers": "^12.0.0",
113
113
  "@types/node": "^18.19.50",
114
114
  "abort-controller": "^3.0.0",
115
- "borp": "^0.19.0",
115
+ "borp": "^0.20.0",
116
116
  "c8": "^10.0.0",
117
117
  "cross-env": "^7.0.3",
118
118
  "dns-packet": "^5.4.0",
119
119
  "esbuild": "^0.25.2",
120
120
  "eslint": "^9.9.0",
121
- "fast-check": "^3.17.1",
121
+ "fast-check": "^4.1.1",
122
122
  "https-pem": "^3.0.0",
123
123
  "husky": "^9.0.7",
124
124
  "jest": "^29.0.2",
125
125
  "neostandard": "^0.12.0",
126
126
  "node-forge": "^1.3.1",
127
127
  "proxy": "^2.1.1",
128
- "tsd": "^0.31.2",
128
+ "tsd": "^0.32.0",
129
129
  "typescript": "^5.6.2",
130
130
  "ws": "^8.11.0"
131
131
  },
@@ -31,6 +31,15 @@ declare namespace DiagnosticsChannel {
31
31
  export interface RequestBodySentMessage {
32
32
  request: Request;
33
33
  }
34
+
35
+ export interface RequestBodyChunkSentMessage {
36
+ request: Request;
37
+ chunk: Uint8Array | string;
38
+ }
39
+ export interface RequestBodyChunkReceivedMessage {
40
+ request: Request;
41
+ chunk: Buffer;
42
+ }
34
43
  export interface RequestHeadersMessage {
35
44
  request: Request;
36
45
  response: Response;
@@ -97,7 +97,8 @@ declare class Dispatcher extends EventEmitter {
97
97
 
98
98
  declare namespace Dispatcher {
99
99
  export interface ComposedDispatcher extends Dispatcher {}
100
- export type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']
100
+ export type Dispatch = Dispatcher['dispatch']
101
+ export type DispatcherComposeInterceptor = (dispatch: Dispatch) => Dispatch
101
102
  export interface DispatchOptions {
102
103
  origin?: string | URL;
103
104
  path: string;
@@ -276,6 +277,6 @@ declare namespace Dispatcher {
276
277
  }
277
278
 
278
279
  export interface DispatchInterceptor {
279
- (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch']
280
+ (dispatch: Dispatch): Dispatch
280
281
  }
281
282
  }
@@ -1,4 +1,5 @@
1
1
  import Agent from './agent'
2
+ import ProxyAgent from './proxy-agent'
2
3
  import Dispatcher from './dispatcher'
3
4
 
4
5
  export default EnvHttpProxyAgent
@@ -10,7 +11,7 @@ declare class EnvHttpProxyAgent extends Dispatcher {
10
11
  }
11
12
 
12
13
  declare namespace EnvHttpProxyAgent {
13
- export interface Options extends Agent.Options {
14
+ export interface Options extends Omit<ProxyAgent.Options, 'uri'> {
14
15
  /** Overrides the value of the HTTP_PROXY environment variable */
15
16
  httpProxy?: string;
16
17
  /** Overrides the value of the HTTPS_PROXY environment variable */
@@ -18,9 +18,9 @@ interface EventSource extends EventTarget {
18
18
  readonly CLOSED: 2
19
19
  readonly CONNECTING: 0
20
20
  readonly OPEN: 1
21
- onerror: (this: EventSource, ev: ErrorEvent) => any
22
- onmessage: (this: EventSource, ev: MessageEvent) => any
23
- onopen: (this: EventSource, ev: Event) => any
21
+ onerror: ((this: EventSource, ev: ErrorEvent) => any) | null
22
+ onmessage: ((this: EventSource, ev: MessageEvent) => any) | null
23
+ onopen: ((this: EventSource, ev: Event) => any) | null
24
24
  readonly readyState: 0 | 1 | 2
25
25
  readonly url: string
26
26
  readonly withCredentials: boolean
package/types/fetch.d.ts CHANGED
@@ -33,6 +33,7 @@ export class BodyMixin {
33
33
 
34
34
  readonly arrayBuffer: () => Promise<ArrayBuffer>
35
35
  readonly blob: () => Promise<Blob>
36
+ readonly bytes: () => Promise<Uint8Array>
36
37
  /**
37
38
  * @deprecated This method is not recommended for parsing multipart/form-data bodies in server environments.
38
39
  * It is recommended to use a library such as [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy) as follows: