undici 6.2.0 → 6.3.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.
package/README.md CHANGED
@@ -119,7 +119,7 @@ Returns a promise with the result of the `Dispatcher.request` method.
119
119
 
120
120
  Calls `options.dispatcher.request(options)`.
121
121
 
122
- See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details.
122
+ See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
123
123
 
124
124
  ### `undici.stream([url, options, ]factory): Promise`
125
125
 
@@ -0,0 +1,62 @@
1
+ # Debug
2
+
3
+ Undici (and subsenquently `fetch` and `websocket`) exposes a debug statement that can be enabled by setting `NODE_DEBUG` within the environment.
4
+
5
+ The flags availabile are:
6
+
7
+ ## `undici`
8
+
9
+ This flag enables debug statements for the core undici library.
10
+
11
+ ```sh
12
+ NODE_DEBUG=undici node script.js
13
+
14
+ UNDICI 16241: connecting to nodejs.org using https:h1
15
+ UNDICI 16241: connecting to nodejs.org using https:h1
16
+ UNDICI 16241: connected to nodejs.org using https:h1
17
+ UNDICI 16241: sending request to GET https://nodejs.org//
18
+ UNDICI 16241: received response to GET https://nodejs.org// - HTTP 307
19
+ UNDICI 16241: connecting to nodejs.org using https:h1
20
+ UNDICI 16241: trailers received from GET https://nodejs.org//
21
+ UNDICI 16241: connected to nodejs.org using https:h1
22
+ UNDICI 16241: sending request to GET https://nodejs.org//en
23
+ UNDICI 16241: received response to GET https://nodejs.org//en - HTTP 200
24
+ UNDICI 16241: trailers received from GET https://nodejs.org//en
25
+ ```
26
+
27
+ ## `fetch`
28
+
29
+ This flag enables debug statements for the `fetch` API.
30
+
31
+ > **Note**: statements are pretty similar to the ones in the `undici` flag, but scoped to `fetch`
32
+
33
+ ```sh
34
+ NODE_DEBUG=fetch node script.js
35
+
36
+ FETCH 16241: connecting to nodejs.org using https:h1
37
+ FETCH 16241: connecting to nodejs.org using https:h1
38
+ FETCH 16241: connected to nodejs.org using https:h1
39
+ FETCH 16241: sending request to GET https://nodejs.org//
40
+ FETCH 16241: received response to GET https://nodejs.org// - HTTP 307
41
+ FETCH 16241: connecting to nodejs.org using https:h1
42
+ FETCH 16241: trailers received from GET https://nodejs.org//
43
+ FETCH 16241: connected to nodejs.org using https:h1
44
+ FETCH 16241: sending request to GET https://nodejs.org//en
45
+ FETCH 16241: received response to GET https://nodejs.org//en - HTTP 200
46
+ FETCH 16241: trailers received from GET https://nodejs.org//en
47
+ ```
48
+
49
+ ## `websocket`
50
+
51
+ This flag enables debug statements for the `Websocket` API.
52
+
53
+ > **Note**: statements can overlap with `UNDICI` ones if `undici` or `fetch` flag has been enabled as well.
54
+
55
+ ```sh
56
+ NODE_DEBUG=fetch node script.js
57
+
58
+ WEBSOCKET 18309: connecting to echo.websocket.org using https:h1
59
+ WEBSOCKET 18309: connected to echo.websocket.org using https:h1
60
+ WEBSOCKET 18309: sending request to GET https://echo.websocket.org//
61
+ WEBSOCKET 18309: connection opened <ip_address>
62
+ ```
@@ -105,7 +105,7 @@ You can not assume that this event is related to any specific request.
105
105
  import diagnosticsChannel from 'diagnostics_channel'
106
106
 
107
107
  diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
108
- // const { host, hostname, protocol, port, servername } = connectParams
108
+ // const { host, hostname, protocol, port, servername, version } = connectParams
109
109
  // connector is a function that creates the socket
110
110
  })
111
111
  ```
@@ -118,7 +118,7 @@ This message is published after a connection is established.
118
118
  import diagnosticsChannel from 'diagnostics_channel'
119
119
 
120
120
  diagnosticsChannel.channel('undici:client:connected').subscribe(({ socket, connectParams, connector }) => {
121
- // const { host, hostname, protocol, port, servername } = connectParams
121
+ // const { host, hostname, protocol, port, servername, version } = connectParams
122
122
  // connector is a function that creates the socket
123
123
  })
124
124
  ```
@@ -131,7 +131,7 @@ This message is published if it did not succeed to create new connection
131
131
  import diagnosticsChannel from 'diagnostics_channel'
132
132
 
133
133
  diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, socket, connectParams, connector }) => {
134
- // const { host, hostname, protocol, port, servername } = connectParams
134
+ // const { host, hostname, protocol, port, servername, version } = connectParams
135
135
  // connector is a function that creates the socket
136
136
  console.log(`Connect failed with ${error.message}`)
137
137
  })
package/index-fetch.js CHANGED
@@ -4,7 +4,9 @@ const fetchImpl = require('./lib/fetch').fetch
4
4
 
5
5
  module.exports.fetch = function fetch (resource, init = undefined) {
6
6
  return fetchImpl(resource, init).catch((err) => {
7
- Error.captureStackTrace(err, this)
7
+ if (typeof err === 'object') {
8
+ Error.captureStackTrace(err, this)
9
+ }
8
10
  throw err
9
11
  })
10
12
  }
package/lib/agent.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { InvalidArgumentError } = require('./core/errors')
4
- const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors, kBusy } = require('./core/symbols')
4
+ const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
5
5
  const DispatcherBase = require('./dispatcher-base')
6
6
  const Pool = require('./pool')
7
7
  const Client = require('./client')
@@ -15,7 +15,6 @@ const kMaxRedirections = Symbol('maxRedirections')
15
15
  const kOnDrain = Symbol('onDrain')
16
16
  const kFactory = Symbol('factory')
17
17
  const kOptions = Symbol('options')
18
- const kDeleteScheduled = Symbol('deleteScheduled')
19
18
 
20
19
  function defaultFactory (origin, opts) {
21
20
  return opts && opts.connections === 1
@@ -94,34 +93,15 @@ class Agent extends DispatcherBase {
94
93
 
95
94
  if (!dispatcher) {
96
95
  dispatcher = this[kFactory](opts.origin, this[kOptions])
97
- .on('drain', (...args) => {
98
- this[kOnDrain](...args)
99
-
100
- // We remove the client if it is not busy for 5 minutes
101
- // to avoid a long list of clients to saturate memory.
102
- // Ideally, we could use a FinalizationRegistry here, but
103
- // it is currently very buggy in Node.js.
104
- // See
105
- // * https://github.com/nodejs/node/issues/49344
106
- // * https://github.com/nodejs/node/issues/47748
107
- // TODO(mcollina): make the timeout configurable or
108
- // use an event to remove disconnected clients.
109
- this[kDeleteScheduled] = setTimeout(() => {
110
- if (dispatcher[kBusy] === 0) {
111
- this[kClients].destroy().then(() => {})
112
- this[kClients].delete(key)
113
- }
114
- }, 300_000)
115
- this[kDeleteScheduled].unref()
116
- })
96
+ .on('drain', this[kOnDrain])
117
97
  .on('connect', this[kOnConnect])
118
98
  .on('disconnect', this[kOnDisconnect])
119
99
  .on('connectionError', this[kOnConnectionError])
120
100
 
101
+ // This introduces a tiny memory leak, as dispatchers are never removed from the map.
102
+ // TODO(mcollina): remove te timer when the client/pool do not have any more
103
+ // active connections.
121
104
  this[kClients].set(key, dispatcher)
122
- } else if (dispatcher[kDeleteScheduled]) {
123
- clearTimeout(dispatcher[kDeleteScheduled])
124
- dispatcher[kDeleteScheduled] = null
125
105
  }
126
106
 
127
107
  return dispatcher.dispatch(opts, handler)
@@ -6,9 +6,9 @@ const kSignal = Symbol('kSignal')
6
6
 
7
7
  function abort (self) {
8
8
  if (self.abort) {
9
- self.abort()
9
+ self.abort(self[kSignal]?.reason)
10
10
  } else {
11
- self.onError(new RequestAbortedError())
11
+ self.onError(self[kSignal]?.reason ?? new RequestAbortedError())
12
12
  }
13
13
  }
14
14
 
@@ -112,14 +112,12 @@ class Cache {
112
112
  // 5.5.2
113
113
  for (const response of responses) {
114
114
  // 5.5.2.1
115
- const responseObject = new Response(response.body?.source ?? null)
116
- const body = responseObject[kState].body
115
+ const responseObject = new Response(null)
117
116
  responseObject[kState] = response
118
- responseObject[kState].body = body
119
117
  responseObject[kHeaders][kHeadersList] = response.headersList
120
118
  responseObject[kHeaders][kGuard] = 'immutable'
121
119
 
122
- responseList.push(responseObject)
120
+ responseList.push(responseObject.clone())
123
121
  }
124
122
 
125
123
  // 6.
@@ -146,8 +144,6 @@ class Cache {
146
144
  webidl.brandCheck(this, Cache)
147
145
  webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' })
148
146
 
149
- requests = webidl.converters['sequence<RequestInfo>'](requests)
150
-
151
147
  // 1.
152
148
  const responsePromises = []
153
149
 
@@ -155,7 +151,17 @@ class Cache {
155
151
  const requestList = []
156
152
 
157
153
  // 3.
158
- for (const request of requests) {
154
+ for (let request of requests) {
155
+ if (request === undefined) {
156
+ throw webidl.errors.conversionFailed({
157
+ prefix: 'Cache.addAll',
158
+ argument: 'Argument 1',
159
+ types: ['undefined is not allowed']
160
+ })
161
+ }
162
+
163
+ request = webidl.converters.RequestInfo(request)
164
+
159
165
  if (typeof request === 'string') {
160
166
  continue
161
167
  }
package/lib/client.js CHANGED
@@ -9,6 +9,7 @@ const net = require('net')
9
9
  const http = require('http')
10
10
  const { pipeline } = require('stream')
11
11
  const util = require('./core/util')
12
+ const { channels } = require('./core/diagnostics')
12
13
  const timers = require('./timers')
13
14
  const Request = require('./core/request')
14
15
  const DispatcherBase = require('./dispatcher-base')
@@ -108,21 +109,6 @@ const FastBuffer = Buffer[Symbol.species]
108
109
 
109
110
  const kClosedResolve = Symbol('kClosedResolve')
110
111
 
111
- const channels = {}
112
-
113
- try {
114
- const diagnosticsChannel = require('diagnostics_channel')
115
- channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders')
116
- channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect')
117
- channels.connectError = diagnosticsChannel.channel('undici:client:connectError')
118
- channels.connected = diagnosticsChannel.channel('undici:client:connected')
119
- } catch {
120
- channels.sendHeaders = { hasSubscribers: false }
121
- channels.beforeConnect = { hasSubscribers: false }
122
- channels.connectError = { hasSubscribers: false }
123
- channels.connected = { hasSubscribers: false }
124
- }
125
-
126
112
  /**
127
113
  * @type {import('../types/client').default}
128
114
  */
@@ -1191,6 +1177,7 @@ async function connect (client) {
1191
1177
  hostname,
1192
1178
  protocol,
1193
1179
  port,
1180
+ version: client[kHTTPConnVersion],
1194
1181
  servername: client[kServerName],
1195
1182
  localAddress: client[kLocalAddress]
1196
1183
  },
@@ -1284,6 +1271,7 @@ async function connect (client) {
1284
1271
  hostname,
1285
1272
  protocol,
1286
1273
  port,
1274
+ version: client[kHTTPConnVersion],
1287
1275
  servername: client[kServerName],
1288
1276
  localAddress: client[kLocalAddress]
1289
1277
  },
@@ -1306,6 +1294,7 @@ async function connect (client) {
1306
1294
  hostname,
1307
1295
  protocol,
1308
1296
  port,
1297
+ version: client[kHTTPConnVersion],
1309
1298
  servername: client[kServerName],
1310
1299
  localAddress: client[kLocalAddress]
1311
1300
  },
@@ -1658,19 +1647,6 @@ function writeH2 (client, session, request) {
1658
1647
  return false
1659
1648
  }
1660
1649
 
1661
- try {
1662
- // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event?
1663
- request.onConnect((err) => {
1664
- if (request.aborted || request.completed) {
1665
- return
1666
- }
1667
-
1668
- errorRequest(client, request, err || new RequestAbortedError())
1669
- })
1670
- } catch (err) {
1671
- errorRequest(client, request, err)
1672
- }
1673
-
1674
1650
  if (request.aborted) {
1675
1651
  return false
1676
1652
  }
@@ -1682,9 +1658,34 @@ function writeH2 (client, session, request) {
1682
1658
  headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost]
1683
1659
  headers[HTTP2_HEADER_METHOD] = method
1684
1660
 
1661
+ try {
1662
+ // We are already connected, streams are pending.
1663
+ // We can call on connect, and wait for abort
1664
+ request.onConnect((err) => {
1665
+ if (request.aborted || request.completed) {
1666
+ return
1667
+ }
1668
+
1669
+ err = err || new RequestAbortedError()
1670
+
1671
+ if (stream != null) {
1672
+ util.destroy(stream, err)
1673
+
1674
+ h2State.openStreams -= 1
1675
+ if (h2State.openStreams === 0) {
1676
+ session.unref()
1677
+ }
1678
+ }
1679
+
1680
+ errorRequest(client, request, err)
1681
+ })
1682
+ } catch (err) {
1683
+ errorRequest(client, request, err)
1684
+ }
1685
+
1685
1686
  if (method === 'CONNECT') {
1686
1687
  session.ref()
1687
- // we are already connected, streams are pending, first request
1688
+ // We are already connected, streams are pending, first request
1688
1689
  // will create a new stream. We trigger a request to create the stream and wait until
1689
1690
  // `ready` event is triggered
1690
1691
  // We disabled endStream to allow the user to write to the stream
@@ -28,6 +28,8 @@ class CompatFinalizer {
28
28
  })
29
29
  }
30
30
  }
31
+
32
+ unregister (key) {}
31
33
  }
32
34
 
33
35
  module.exports = function () {
@@ -0,0 +1,202 @@
1
+ 'use strict'
2
+ const diagnosticsChannel = require('diagnostics_channel')
3
+ const util = require('util')
4
+
5
+ const undiciDebugLog = util.debuglog('undici')
6
+ const fetchDebuglog = util.debuglog('fetch')
7
+ const websocketDebuglog = util.debuglog('websocket')
8
+ let isClientSet = false
9
+ const channels = {
10
+ // Client
11
+ beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'),
12
+ connected: diagnosticsChannel.channel('undici:client:connected'),
13
+ connectError: diagnosticsChannel.channel('undici:client:connectError'),
14
+ sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'),
15
+ // Request
16
+ create: diagnosticsChannel.channel('undici:request:create'),
17
+ bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
18
+ headers: diagnosticsChannel.channel('undici:request:headers'),
19
+ trailers: diagnosticsChannel.channel('undici:request:trailers'),
20
+ error: diagnosticsChannel.channel('undici:request:error'),
21
+ // WebSocket
22
+ open: diagnosticsChannel.channel('undici:websocket:open'),
23
+ close: diagnosticsChannel.channel('undici:websocket:close'),
24
+ socketError: diagnosticsChannel.channel('undici:websocket:socket_error'),
25
+ ping: diagnosticsChannel.channel('undici:websocket:ping'),
26
+ pong: diagnosticsChannel.channel('undici:websocket:pong')
27
+ }
28
+
29
+ if (undiciDebugLog.enabled || fetchDebuglog.enabled) {
30
+ const debuglog = fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog
31
+
32
+ // Track all Client events
33
+ diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(evt => {
34
+ const {
35
+ connectParams: { version, protocol, port, host }
36
+ } = evt
37
+ debuglog(
38
+ 'connecting to %s using %s%s',
39
+ `${host}${port ? `:${port}` : ''}`,
40
+ protocol,
41
+ version
42
+ )
43
+ })
44
+
45
+ diagnosticsChannel.channel('undici:client:connected').subscribe(evt => {
46
+ const {
47
+ connectParams: { version, protocol, port, host }
48
+ } = evt
49
+ debuglog(
50
+ 'connected to %s using %s%s',
51
+ `${host}${port ? `:${port}` : ''}`,
52
+ protocol,
53
+ version
54
+ )
55
+ })
56
+
57
+ diagnosticsChannel.channel('undici:client:connectError').subscribe(evt => {
58
+ const {
59
+ connectParams: { version, protocol, port, host },
60
+ error
61
+ } = evt
62
+ debuglog(
63
+ 'connection to %s using %s%s errored - %s',
64
+ `${host}${port ? `:${port}` : ''}`,
65
+ protocol,
66
+ version,
67
+ error.message
68
+ )
69
+ })
70
+
71
+ diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(evt => {
72
+ const {
73
+ request: { method, path, origin }
74
+ } = evt
75
+ debuglog('sending request to %s %s/%s', method, origin, path)
76
+ })
77
+
78
+ // Track Request events
79
+ diagnosticsChannel.channel('undici:request:headers').subscribe(evt => {
80
+ const {
81
+ request: { method, path, origin },
82
+ response: { statusCode }
83
+ } = evt
84
+ debuglog(
85
+ 'received response to %s %s/%s - HTTP %d',
86
+ method,
87
+ origin,
88
+ path,
89
+ statusCode
90
+ )
91
+ })
92
+
93
+ diagnosticsChannel.channel('undici:request:trailers').subscribe(evt => {
94
+ const {
95
+ request: { method, path, origin }
96
+ } = evt
97
+ debuglog('trailers received from %s %s/%s', method, origin, path)
98
+ })
99
+
100
+ diagnosticsChannel.channel('undici:request:error').subscribe(evt => {
101
+ const {
102
+ request: { method, path, origin },
103
+ error
104
+ } = evt
105
+ debuglog(
106
+ 'request to %s %s/%s errored - %s',
107
+ method,
108
+ origin,
109
+ path,
110
+ error.message
111
+ )
112
+ })
113
+
114
+ isClientSet = true
115
+ }
116
+
117
+ if (websocketDebuglog.enabled) {
118
+ if (!isClientSet) {
119
+ const debuglog = undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog
120
+ diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(evt => {
121
+ const {
122
+ connectParams: { version, protocol, port, host }
123
+ } = evt
124
+ debuglog(
125
+ 'connecting to %s%s using %s%s',
126
+ host,
127
+ port ? `:${port}` : '',
128
+ protocol,
129
+ version
130
+ )
131
+ })
132
+
133
+ diagnosticsChannel.channel('undici:client:connected').subscribe(evt => {
134
+ const {
135
+ connectParams: { version, protocol, port, host }
136
+ } = evt
137
+ debuglog(
138
+ 'connected to %s%s using %s%s',
139
+ host,
140
+ port ? `:${port}` : '',
141
+ protocol,
142
+ version
143
+ )
144
+ })
145
+
146
+ diagnosticsChannel.channel('undici:client:connectError').subscribe(evt => {
147
+ const {
148
+ connectParams: { version, protocol, port, host },
149
+ error
150
+ } = evt
151
+ debuglog(
152
+ 'connection to %s%s using %s%s errored - %s',
153
+ host,
154
+ port ? `:${port}` : '',
155
+ protocol,
156
+ version,
157
+ error.message
158
+ )
159
+ })
160
+
161
+ diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(evt => {
162
+ const {
163
+ request: { method, path, origin }
164
+ } = evt
165
+ debuglog('sending request to %s %s/%s', method, origin, path)
166
+ })
167
+ }
168
+
169
+ // Track all WebSocket events
170
+ diagnosticsChannel.channel('undici:websocket:open').subscribe(evt => {
171
+ const {
172
+ address: { address, port }
173
+ } = evt
174
+ websocketDebuglog('connection opened %s%s', address, port ? `:${port}` : '')
175
+ })
176
+
177
+ diagnosticsChannel.channel('undici:websocket:close').subscribe(evt => {
178
+ const { websocket, code, reason } = evt
179
+ websocketDebuglog(
180
+ 'closed connection to %s - %s %s',
181
+ websocket.url,
182
+ code,
183
+ reason
184
+ )
185
+ })
186
+
187
+ diagnosticsChannel.channel('undici:websocket:socket_error').subscribe(err => {
188
+ websocketDebuglog('connection errored - %s', err.message)
189
+ })
190
+
191
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(evt => {
192
+ websocketDebuglog('ping received')
193
+ })
194
+
195
+ diagnosticsChannel.channel('undici:websocket:pong').subscribe(evt => {
196
+ websocketDebuglog('pong received')
197
+ })
198
+ }
199
+
200
+ module.exports = {
201
+ channels
202
+ }
@@ -7,6 +7,7 @@ const {
7
7
  const assert = require('assert')
8
8
  const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
9
9
  const util = require('./util')
10
+ const { channels } = require('./diagnostics.js')
10
11
  const { headerNameLowerCasedRecord } = require('./constants')
11
12
 
12
13
  // headerCharRegex have been lifted from
@@ -25,25 +26,8 @@ const invalidPathRegex = /[^\u0021-\u00ff]/
25
26
 
26
27
  const kHandler = Symbol('handler')
27
28
 
28
- const channels = {}
29
-
30
29
  let extractBody
31
30
 
32
- try {
33
- const diagnosticsChannel = require('diagnostics_channel')
34
- channels.create = diagnosticsChannel.channel('undici:request:create')
35
- channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
36
- channels.headers = diagnosticsChannel.channel('undici:request:headers')
37
- channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
38
- channels.error = diagnosticsChannel.channel('undici:request:error')
39
- } catch {
40
- channels.create = { hasSubscribers: false }
41
- channels.bodySent = { hasSubscribers: false }
42
- channels.headers = { hasSubscribers: false }
43
- channels.trailers = { hasSubscribers: false }
44
- channels.error = { hasSubscribers: false }
45
- }
46
-
47
31
  class Request {
48
32
  constructor (origin, {
49
33
  path,
package/lib/fetch/body.js CHANGED
@@ -14,7 +14,7 @@ const { FormData } = require('./formdata')
14
14
  const { kState } = require('./symbols')
15
15
  const { webidl } = require('./webidl')
16
16
  const { Blob, File: NativeFile } = require('buffer')
17
- const { kBodyUsed } = require('../core/symbols')
17
+ const { kBodyUsed, kHeadersList } = require('../core/symbols')
18
18
  const assert = require('assert')
19
19
  const { isErrored } = require('../core/util')
20
20
  const { isUint8Array, isArrayBuffer } = require('util/types')
@@ -369,10 +369,12 @@ function bodyMixinMethods (instance) {
369
369
 
370
370
  throwIfAborted(this[kState])
371
371
 
372
- const contentType = this.headers.get('Content-Type')
372
+ const contentType = this.headers[kHeadersList].get('content-type', true)
373
+
374
+ const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
373
375
 
374
376
  // If mimeType’s essence is "multipart/form-data", then:
375
- if (/multipart\/form-data/.test(contentType)) {
377
+ if (mimeType !== 'failure' && mimeType.essence === 'multipart/form-data') {
376
378
  const headers = {}
377
379
  for (const [key, value] of this.headers) headers[key] = value
378
380
 
@@ -430,7 +432,7 @@ function bodyMixinMethods (instance) {
430
432
  await busboyResolve
431
433
 
432
434
  return responseFormData
433
- } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
435
+ } else if (mimeType !== 'failure' && mimeType.essence === 'application/x-www-form-urlencoded') {
434
436
  // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
435
437
 
436
438
  // 1. Let entries be the result of parsing bytes.
@@ -188,11 +188,28 @@ function stringPercentDecode (input) {
188
188
  return percentDecode(bytes)
189
189
  }
190
190
 
191
+ /**
192
+ * @param {number} byte
193
+ */
191
194
  function isHexCharByte (byte) {
192
195
  // 0-9 A-F a-f
193
196
  return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
194
197
  }
195
198
 
199
+ /**
200
+ * @param {number} byte
201
+ */
202
+ function hexByteToNumber (byte) {
203
+ return (
204
+ // 0-9
205
+ byte >= 0x30 && byte <= 0x39
206
+ ? (byte - 48)
207
+ // Convert to uppercase
208
+ // ((byte & 0xDF) - 65) + 10
209
+ : ((byte & 0xDF) - 55)
210
+ )
211
+ }
212
+
196
213
  // https://url.spec.whatwg.org/#percent-decode
197
214
  /** @param {Uint8Array} input */
198
215
  function percentDecode (input) {
@@ -224,11 +241,8 @@ function percentDecode (input) {
224
241
  } else {
225
242
  // 1. Let bytePoint be the two bytes after byte in input,
226
243
  // decoded, and then interpreted as hexadecimal number.
227
- const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2])
228
- const bytePoint = Number.parseInt(nextTwoBytes, 16)
229
-
230
244
  // 2. Append a byte whose value is bytePoint to output.
231
- output[j++] = bytePoint
245
+ output[j++] = (hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2])
232
246
 
233
247
  // 3. Skip the next two bytes in input.
234
248
  i += 2
@@ -590,14 +604,18 @@ function isHTTPWhiteSpace (char) {
590
604
  * @param {boolean} [trailing=true]
591
605
  */
592
606
  function removeHTTPWhitespace (str, leading = true, trailing = true) {
593
- let i = 0; let j = str.length
607
+ let lead = 0
608
+ let trail = str.length - 1
609
+
594
610
  if (leading) {
595
- while (j > i && isHTTPWhiteSpace(str.charCodeAt(i))) --i
611
+ while (lead < str.length && isHTTPWhiteSpace(str.charCodeAt(lead))) lead++
596
612
  }
613
+
597
614
  if (trailing) {
598
- while (j > i && isHTTPWhiteSpace(str.charCodeAt(j - 1))) --j
615
+ while (trail > 0 && isHTTPWhiteSpace(str.charCodeAt(trail))) trail--
599
616
  }
600
- return i === 0 && j === str.length ? str : str.substring(i, j)
617
+
618
+ return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
601
619
  }
602
620
 
603
621
  /**
@@ -616,14 +634,18 @@ function isASCIIWhitespace (char) {
616
634
  * @param {boolean} [trailing=true]
617
635
  */
618
636
  function removeASCIIWhitespace (str, leading = true, trailing = true) {
619
- let i = 0; let j = str.length
637
+ let lead = 0
638
+ let trail = str.length - 1
639
+
620
640
  if (leading) {
621
- while (j > i && isASCIIWhitespace(str.charCodeAt(i))) --i
641
+ while (lead < str.length && isASCIIWhitespace(str.charCodeAt(lead))) lead++
622
642
  }
643
+
623
644
  if (trailing) {
624
- while (j > i && isASCIIWhitespace(str.charCodeAt(j - 1))) --j
645
+ while (trail > 0 && isASCIIWhitespace(str.charCodeAt(trail))) trail--
625
646
  }
626
- return i === 0 && j === str.length ? str : str.substring(i, j)
647
+
648
+ return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
627
649
  }
628
650
 
629
651
  module.exports = {
package/lib/fetch/file.js CHANGED
@@ -211,10 +211,7 @@ webidl.converters.BlobPart = function (V, opts) {
211
211
  return webidl.converters.Blob(V, { strict: false })
212
212
  }
213
213
 
214
- if (
215
- ArrayBuffer.isView(V) ||
216
- types.isAnyArrayBuffer(V)
217
- ) {
214
+ if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) {
218
215
  return webidl.converters.BufferSource(V, opts)
219
216
  }
220
217
  }
@@ -282,10 +279,7 @@ function processBlobParts (parts, options) {
282
279
 
283
280
  // 3. Append the result of UTF-8 encoding s to bytes.
284
281
  bytes.push(encoder.encode(s))
285
- } else if (
286
- types.isAnyArrayBuffer(element) ||
287
- types.isTypedArray(element)
288
- ) {
282
+ } else if (ArrayBuffer.isView(element) || types.isArrayBuffer(element)) {
289
283
  // 2. If element is a BufferSource, get a copy of the
290
284
  // bytes held by the buffer source, and append those
291
285
  // bytes to bytes.
@@ -1206,7 +1206,7 @@ async function httpFetch (fetchParams) {
1206
1206
  // encouraged to, transmit an RST_STREAM frame.
1207
1207
  // See, https://github.com/whatwg/fetch/issues/1288
1208
1208
  if (request.redirect !== 'manual') {
1209
- fetchParams.controller.connection.destroy()
1209
+ fetchParams.controller.connection.destroy(undefined, false)
1210
1210
  }
1211
1211
 
1212
1212
  // 2. Switch on request’s redirect mode:
@@ -1718,10 +1718,12 @@ async function httpNetworkFetch (
1718
1718
  fetchParams.controller.connection = {
1719
1719
  abort: null,
1720
1720
  destroyed: false,
1721
- destroy (err) {
1721
+ destroy (err, abort = true) {
1722
1722
  if (!this.destroyed) {
1723
1723
  this.destroyed = true
1724
- this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
1724
+ if (abort) {
1725
+ this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
1726
+ }
1725
1727
  }
1726
1728
  }
1727
1729
  }
@@ -2152,7 +2154,8 @@ async function httpNetworkFetch (
2152
2154
  } else {
2153
2155
  const keys = Object.keys(rawHeaders)
2154
2156
  for (let i = 0; i < keys.length; ++i) {
2155
- headersList.append(keys[i], rawHeaders[keys[i]])
2157
+ // The header names are already in lowercase.
2158
+ headersList.append(keys[i], rawHeaders[keys[i]], true)
2156
2159
  }
2157
2160
  // For H2, The header names are already in lowercase,
2158
2161
  // so we can avoid the `HeadersList#get` call here.
@@ -38,6 +38,8 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
38
38
  signal.removeEventListener('abort', abort)
39
39
  })
40
40
 
41
+ let patchMethodWarning = false
42
+
41
43
  // https://fetch.spec.whatwg.org/#request-class
42
44
  class Request {
43
45
  // https://fetch.spec.whatwg.org/#dom-request
@@ -313,21 +315,36 @@ class Request {
313
315
  // 1. Let method be init["method"].
314
316
  let method = init.method
315
317
 
316
- // 2. If method is not a method or method is a forbidden method, then
317
- // throw a TypeError.
318
- if (!isValidHTTPToken(method)) {
319
- throw new TypeError(`'${method}' is not a valid HTTP method.`)
320
- }
318
+ const mayBeNormalized = normalizeMethodRecord[method]
319
+
320
+ if (mayBeNormalized !== undefined) {
321
+ // Note: Bypass validation DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH and these lowercase ones
322
+ request.method = mayBeNormalized
323
+ } else {
324
+ // 2. If method is not a method or method is a forbidden method, then
325
+ // throw a TypeError.
326
+ if (!isValidHTTPToken(method)) {
327
+ throw new TypeError(`'${method}' is not a valid HTTP method.`)
328
+ }
321
329
 
322
- if (forbiddenMethodsSet.has(method.toUpperCase())) {
323
- throw new TypeError(`'${method}' HTTP method is unsupported.`)
330
+ if (forbiddenMethodsSet.has(method.toUpperCase())) {
331
+ throw new TypeError(`'${method}' HTTP method is unsupported.`)
332
+ }
333
+
334
+ // 3. Normalize method.
335
+ method = normalizeMethod(method)
336
+
337
+ // 4. Set request’s method to method.
338
+ request.method = method
324
339
  }
325
340
 
326
- // 3. Normalize method.
327
- method = normalizeMethodRecord[method] ?? normalizeMethod(method)
341
+ if (!patchMethodWarning && request.method === 'patch') {
342
+ process.emitWarning('Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.', {
343
+ code: 'UNDICI-FETCH-patch'
344
+ })
328
345
 
329
- // 4. Set request’s method to method.
330
- request.method = method
346
+ patchMethodWarning = true
347
+ }
331
348
  }
332
349
 
333
350
  // 26. If init["signal"] exists, then set signal to it.
@@ -371,6 +388,18 @@ class Request {
371
388
  const abort = function () {
372
389
  const ac = acRef.deref()
373
390
  if (ac !== undefined) {
391
+ // Currently, there is a problem with FinalizationRegistry.
392
+ // https://github.com/nodejs/node/issues/49344
393
+ // https://github.com/nodejs/node/issues/47748
394
+ // In the case of abort, the first step is to unregister from it.
395
+ // If the controller can refer to it, it is still registered.
396
+ // It will be removed in the future.
397
+ requestFinalizer.unregister(abort)
398
+
399
+ // Unsubscribe a listener.
400
+ // FinalizationRegistry will no longer be called, so this must be done.
401
+ this.removeEventListener('abort', abort)
402
+
374
403
  ac.abort(this.reason)
375
404
  }
376
405
  }
@@ -388,7 +417,11 @@ class Request {
388
417
  } catch {}
389
418
 
390
419
  util.addAbortListener(signal, abort)
391
- requestFinalizer.register(ac, { signal, abort })
420
+ // The third argument must be a registry key to be unregistered.
421
+ // Without it, you cannot unregister.
422
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
423
+ // abort is used as the unregister key. (because it is unique)
424
+ requestFinalizer.register(ac, { signal, abort }, abort)
392
425
  }
393
426
  }
394
427
 
@@ -471,7 +504,7 @@ class Request {
471
504
  // 3, If Content-Type is non-null and this’s headers’s header list does
472
505
  // not contain `Content-Type`, then append `Content-Type`/Content-Type to
473
506
  // this’s headers.
474
- if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) {
507
+ if (contentType && !this[kHeaders][kHeadersList].contains('content-type', true)) {
475
508
  this[kHeaders].append('content-type', contentType)
476
509
  }
477
510
  }
@@ -524,7 +524,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V) {
524
524
  return webidl.converters.Blob(V, { strict: false })
525
525
  }
526
526
 
527
- if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) {
527
+ if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
528
528
  return webidl.converters.BufferSource(V)
529
529
  }
530
530
 
package/lib/fetch/util.js CHANGED
@@ -584,7 +584,7 @@ function bytesMatch (bytes, metadataList) {
584
584
  // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
585
585
  // https://www.w3.org/TR/CSP2/#source-list-syntax
586
586
  // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
587
- const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i
587
+ const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-(?<hash>[A-Za-z0-9+/]+={0,2}(?=\s|$))( +[!-~]*)?/i
588
588
 
589
589
  /**
590
590
  * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -681,7 +681,7 @@ function isCancelled (fetchParams) {
681
681
  fetchParams.controller.state === 'terminated'
682
682
  }
683
683
 
684
- const normalizeMethodRecord = {
684
+ const normalizeMethodRecordBase = {
685
685
  delete: 'DELETE',
686
686
  DELETE: 'DELETE',
687
687
  get: 'GET',
@@ -696,7 +696,14 @@ const normalizeMethodRecord = {
696
696
  PUT: 'PUT'
697
697
  }
698
698
 
699
+ const normalizeMethodRecord = {
700
+ ...normalizeMethodRecordBase,
701
+ patch: 'patch',
702
+ PATCH: 'PATCH'
703
+ }
704
+
699
705
  // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
706
+ Object.setPrototypeOf(normalizeMethodRecordBase, null)
700
707
  Object.setPrototypeOf(normalizeMethodRecord, null)
701
708
 
702
709
  /**
@@ -704,7 +711,7 @@ Object.setPrototypeOf(normalizeMethodRecord, null)
704
711
  * @param {string} method
705
712
  */
706
713
  function normalizeMethod (method) {
707
- return normalizeMethodRecord[method.toLowerCase()] ?? method
714
+ return normalizeMethodRecordBase[method.toLowerCase()] ?? method
708
715
  }
709
716
 
710
717
  // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
@@ -1213,5 +1220,6 @@ module.exports = {
1213
1220
  readAllBytes,
1214
1221
  normalizeMethodRecord,
1215
1222
  simpleRangeHeaderValue,
1216
- buildContentRange
1223
+ buildContentRange,
1224
+ parseMetadata
1217
1225
  }
@@ -614,15 +614,15 @@ webidl.converters.DataView = function (V, opts = {}) {
614
614
  // https://webidl.spec.whatwg.org/#BufferSource
615
615
  webidl.converters.BufferSource = function (V, opts = {}) {
616
616
  if (types.isAnyArrayBuffer(V)) {
617
- return webidl.converters.ArrayBuffer(V, opts)
617
+ return webidl.converters.ArrayBuffer(V, { ...opts, allowShared: false })
618
618
  }
619
619
 
620
620
  if (types.isTypedArray(V)) {
621
- return webidl.converters.TypedArray(V, V.constructor)
621
+ return webidl.converters.TypedArray(V, V.constructor, { ...opts, allowShared: false })
622
622
  }
623
623
 
624
624
  if (types.isDataView(V)) {
625
- return webidl.converters.DataView(V, opts)
625
+ return webidl.converters.DataView(V, opts, { ...opts, allowShared: false })
626
626
  }
627
627
 
628
628
  throw new TypeError(`Could not convert ${V} to a BufferSource.`)
@@ -176,7 +176,7 @@ function parseLocation (statusCode, headers) {
176
176
  }
177
177
 
178
178
  for (let i = 0; i < headers.length; i += 2) {
179
- if (headers[i].toString().toLowerCase() === 'location') {
179
+ if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') {
180
180
  return headers[i + 1]
181
181
  }
182
182
  }
@@ -184,12 +184,17 @@ function parseLocation (statusCode, headers) {
184
184
 
185
185
  // https://tools.ietf.org/html/rfc7231#section-6.4.4
186
186
  function shouldRemoveHeader (header, removeContent, unknownOrigin) {
187
- return (
188
- (header.length === 4 && header.toString().toLowerCase() === 'host') ||
189
- (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
190
- (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
191
- (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
192
- )
187
+ if (header.length === 4) {
188
+ return util.headerNameToString(header) === 'host'
189
+ }
190
+ if (removeContent && util.headerNameToString(header).startsWith('content-')) {
191
+ return true
192
+ }
193
+ if (unknownOrigin && (header.length === 13 || header.length === 6)) {
194
+ const name = util.headerNameToString(header)
195
+ return name === 'authorization' || name === 'cookie'
196
+ }
197
+ return false
193
198
  }
194
199
 
195
200
  // https://tools.ietf.org/html/rfc7231#section-6.4
@@ -66,7 +66,7 @@ class ProxyAgent extends DispatcherBase {
66
66
  this[kProxyHeaders] = opts.headers || {}
67
67
 
68
68
  const resolvedUrl = new URL(opts.uri)
69
- const { origin, port, host, username, password } = resolvedUrl
69
+ const { origin, port, username, password } = resolvedUrl
70
70
 
71
71
  if (opts.auth && opts.token) {
72
72
  throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
@@ -97,7 +97,7 @@ class ProxyAgent extends DispatcherBase {
97
97
  signal: opts.signal,
98
98
  headers: {
99
99
  ...this[kProxyHeaders],
100
- host
100
+ host: requestedHost
101
101
  }
102
102
  })
103
103
  if (statusCode !== 200) {
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const diagnosticsChannel = require('diagnostics_channel')
4
3
  const { uid, states } = require('./constants')
5
4
  const {
6
5
  kReadyState,
@@ -9,6 +8,7 @@ const {
9
8
  kReceivedClose
10
9
  } = require('./symbols')
11
10
  const { fireEvent, failWebsocketConnection } = require('./util')
11
+ const { channels } = require('../core/diagnostics')
12
12
  const { CloseEvent } = require('./events')
13
13
  const { makeRequest } = require('../fetch/request')
14
14
  const { fetching } = require('../fetch/index')
@@ -16,11 +16,6 @@ const { Headers } = require('../fetch/headers')
16
16
  const { getGlobalDispatcher } = require('../global')
17
17
  const { kHeadersList } = require('../core/symbols')
18
18
 
19
- const channels = {}
20
- channels.open = diagnosticsChannel.channel('undici:websocket:open')
21
- channels.close = diagnosticsChannel.channel('undici:websocket:close')
22
- channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error')
23
-
24
19
  /** @type {import('crypto')} */
25
20
  let crypto
26
21
  try {
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { Writable } = require('stream')
4
- const diagnosticsChannel = require('diagnostics_channel')
5
4
  const { parserStates, opcodes, states, emptyBuffer } = require('./constants')
6
5
  const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
6
+ const { channels } = require('../core/diagnostics')
7
7
  const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util')
8
8
  const { WebsocketFrameSend } = require('./frame')
9
9
 
@@ -12,10 +12,6 @@ const { WebsocketFrameSend } = require('./frame')
12
12
  // Copyright (c) 2013 Arnout Kazemier and contributors
13
13
  // Copyright (c) 2016 Luigi Pinca and contributors
14
14
 
15
- const channels = {}
16
- channels.ping = diagnosticsChannel.channel('undici:websocket:ping')
17
- channels.pong = diagnosticsChannel.channel('undici:websocket:pong')
18
-
19
15
  class ByteParser extends Writable {
20
16
  #buffers = []
21
17
  #byteOffset = 0
@@ -627,7 +627,7 @@ webidl.converters.WebSocketSendData = function (V) {
627
627
  return webidl.converters.Blob(V, { strict: false })
628
628
  }
629
629
 
630
- if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) {
630
+ if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
631
631
  return webidl.converters.BufferSource(V)
632
632
  }
633
633
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.2.0",
3
+ "version": "6.3.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": {
@@ -75,16 +75,18 @@
75
75
  "build:wasm": "node build/wasm.js --docker",
76
76
  "lint": "standard | snazzy",
77
77
  "lint:fix": "standard --fix | snazzy",
78
- "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript",
79
- "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js",
80
- "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha --exit test/node-fetch",
81
- "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)",
82
- "test:jest": "node scripts/verifyVersion.js 14 || jest",
83
- "test:tap": "tap test/*.js test/diagnostics-channel/*.js",
84
- "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
85
- "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts",
86
- "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js",
87
- "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs)",
78
+ "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test",
79
+ "test:cookies": "borp --coverage -p \"test/cookie/*.js\"",
80
+ "test:node-fetch": "mocha --exit test/node-fetch",
81
+ "test:fetch": "npm run build:node && borp --expose-gc --coverage -p \"test/fetch/*.js\" && borp --coverage -p \"test/webidl/*.js\"",
82
+ "test:jest": "jest",
83
+ "test:tap": "tap test/*.js",
84
+ "test:node-test": "borp --coverage -p \"test/node-test/**/*.js\"",
85
+ "test:tdd": "tap test/*.js --coverage -w",
86
+ "test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
87
+ "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
88
+ "test:websocket": "borp --coverage -p \"test/websocket/*.js\"",
89
+ "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.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",
88
90
  "coverage": "nyc --reporter=text --reporter=html npm run test",
89
91
  "coverage:ci": "nyc --reporter=lcov npm run test",
90
92
  "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",
@@ -96,10 +98,12 @@
96
98
  "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus"
97
99
  },
98
100
  "devDependencies": {
101
+ "@matteo.collina/tspl": "^0.1.1",
99
102
  "@sinonjs/fake-timers": "^11.1.0",
100
103
  "@types/node": "^18.0.3",
101
104
  "abort-controller": "^3.0.0",
102
105
  "atomic-sleep": "^1.0.0",
106
+ "borp": "^0.5.0",
103
107
  "chai": "^4.3.4",
104
108
  "chai-as-promised": "^7.1.1",
105
109
  "chai-iterator": "^3.0.2",
@@ -119,7 +123,6 @@
119
123
  "jsfuzz": "^1.0.15",
120
124
  "mitata": "^0.1.6",
121
125
  "mocha": "^10.0.0",
122
- "mockttp": "^3.9.2",
123
126
  "p-timeout": "^3.2.0",
124
127
  "pre-commit": "^1.2.2",
125
128
  "proxy": "^1.0.2",
@@ -130,7 +133,7 @@
130
133
  "standard": "^17.0.0",
131
134
  "table": "^6.8.0",
132
135
  "tap": "^16.1.0",
133
- "tsd": "^0.29.0",
136
+ "tsd": "^0.30.1",
134
137
  "typescript": "^5.0.2",
135
138
  "wait-on": "^7.0.1",
136
139
  "ws": "^8.11.0"
@@ -4,6 +4,8 @@ import { URL } from 'url'
4
4
 
5
5
  export default BalancedPool
6
6
 
7
+ type BalancedPoolConnectOptions = Omit<Dispatcher.ConnectOptions, "origin">;
8
+
7
9
  declare class BalancedPool extends Dispatcher {
8
10
  constructor(url: string | string[] | URL | URL[], options?: Pool.Options);
9
11
 
@@ -15,4 +17,13 @@ declare class BalancedPool extends Dispatcher {
15
17
  closed: boolean;
16
18
  /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */
17
19
  destroyed: boolean;
20
+
21
+ // Override dispatcher APIs.
22
+ override connect(
23
+ options: BalancedPoolConnectOptions
24
+ ): Promise<Dispatcher.ConnectData>;
25
+ override connect(
26
+ options: BalancedPoolConnectOptions,
27
+ callback: (err: Error | null, data: Dispatcher.ConnectData) => void
28
+ ): void;
18
29
  }
package/types/client.d.ts CHANGED
@@ -3,6 +3,8 @@ import { TlsOptions } from 'tls'
3
3
  import Dispatcher from './dispatcher'
4
4
  import buildConnector from "./connector";
5
5
 
6
+ type ClientConnectOptions = Omit<Dispatcher.ConnectOptions, "origin">;
7
+
6
8
  /**
7
9
  * A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
8
10
  */
@@ -14,6 +16,15 @@ export class Client extends Dispatcher {
14
16
  closed: boolean;
15
17
  /** `true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */
16
18
  destroyed: boolean;
19
+
20
+ // Override dispatcher APIs.
21
+ override connect(
22
+ options: ClientConnectOptions
23
+ ): Promise<Dispatcher.ConnectData>;
24
+ override connect(
25
+ options: ClientConnectOptions,
26
+ callback: (err: Error | null, data: Dispatcher.ConnectData) => void
27
+ ): void;
17
28
  }
18
29
 
19
30
  export declare namespace Client {
@@ -121,6 +121,7 @@ declare namespace Dispatcher {
121
121
  expectContinue?: boolean;
122
122
  }
123
123
  export interface ConnectOptions {
124
+ origin: string | URL;
124
125
  path: string;
125
126
  /** Default: `null` */
126
127
  headers?: IncomingHttpHeaders | string[] | null;
package/types/pool.d.ts CHANGED
@@ -5,6 +5,8 @@ import Dispatcher from "./dispatcher";
5
5
 
6
6
  export default Pool
7
7
 
8
+ type PoolConnectOptions = Omit<Dispatcher.ConnectOptions, "origin">;
9
+
8
10
  declare class Pool extends Dispatcher {
9
11
  constructor(url: string | URL, options?: Pool.Options)
10
12
  /** `true` after `pool.close()` has been called. */
@@ -13,6 +15,15 @@ declare class Pool extends Dispatcher {
13
15
  destroyed: boolean;
14
16
  /** Aggregate stats for a Pool. */
15
17
  readonly stats: TPoolStats;
18
+
19
+ // Override dispatcher APIs.
20
+ override connect(
21
+ options: PoolConnectOptions
22
+ ): Promise<Dispatcher.ConnectData>;
23
+ override connect(
24
+ options: PoolConnectOptions,
25
+ callback: (err: Error | null, data: Dispatcher.ConnectData) => void
26
+ ): void;
16
27
  }
17
28
 
18
29
  declare namespace Pool {
@@ -1,9 +1,7 @@
1
1
  import Agent from './agent'
2
2
  import buildConnector from './connector';
3
- import Client from './client'
4
3
  import Dispatcher from './dispatcher'
5
4
  import { IncomingHttpHeaders } from './header'
6
- import Pool from './pool'
7
5
 
8
6
  export default ProxyAgent
9
7