undici 5.26.5 → 5.27.1

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/lib/client.js CHANGED
@@ -1462,23 +1462,7 @@ function _resume (client, sync) {
1462
1462
  return
1463
1463
  }
1464
1464
 
1465
- if (util.isStream(request.body) && util.bodyLength(request.body) === 0) {
1466
- request.body
1467
- .on('data', /* istanbul ignore next */ function () {
1468
- /* istanbul ignore next */
1469
- assert(false)
1470
- })
1471
- .on('error', function (err) {
1472
- errorRequest(client, request, err)
1473
- })
1474
- .on('end', function () {
1475
- util.destroy(this)
1476
- })
1477
-
1478
- request.body = null
1479
- }
1480
-
1481
- if (client[kRunning] > 0 &&
1465
+ if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 &&
1482
1466
  (util.isStream(request.body) || util.isAsyncIterable(request.body))) {
1483
1467
  // Request with stream or iterator body can error while other requests
1484
1468
  // are inflight and indirectly error those as well.
@@ -1499,6 +1483,11 @@ function _resume (client, sync) {
1499
1483
  }
1500
1484
  }
1501
1485
 
1486
+ // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
1487
+ function shouldSendContentLength (method) {
1488
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
1489
+ }
1490
+
1502
1491
  function write (client, request) {
1503
1492
  if (client[kHTTPConnVersion] === 'h2') {
1504
1493
  writeH2(client, client[kHTTP2Session], request)
@@ -1527,7 +1516,9 @@ function write (client, request) {
1527
1516
  body.read(0)
1528
1517
  }
1529
1518
 
1530
- let contentLength = util.bodyLength(body)
1519
+ const bodyLength = util.bodyLength(body)
1520
+
1521
+ let contentLength = bodyLength
1531
1522
 
1532
1523
  if (contentLength === null) {
1533
1524
  contentLength = request.contentLength
@@ -1542,7 +1533,9 @@ function write (client, request) {
1542
1533
  contentLength = null
1543
1534
  }
1544
1535
 
1545
- if (request.contentLength !== null && request.contentLength !== contentLength) {
1536
+ // https://github.com/nodejs/undici/issues/2046
1537
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
1538
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
1546
1539
  if (client[kStrictContentLength]) {
1547
1540
  errorRequest(client, request, new RequestContentLengthMismatchError())
1548
1541
  return false
@@ -1623,7 +1616,7 @@ function write (client, request) {
1623
1616
  }
1624
1617
 
1625
1618
  /* istanbul ignore else: assertion */
1626
- if (!body) {
1619
+ if (!body || bodyLength === 0) {
1627
1620
  if (contentLength === 0) {
1628
1621
  socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
1629
1622
  } else {
@@ -1763,7 +1756,9 @@ function writeH2 (client, session, request) {
1763
1756
  contentLength = null
1764
1757
  }
1765
1758
 
1766
- if (request.contentLength != null && request.contentLength !== contentLength) {
1759
+ // https://github.com/nodejs/undici/issues/2046
1760
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
1761
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
1767
1762
  if (client[kStrictContentLength]) {
1768
1763
  errorRequest(client, request, new RequestContentLengthMismatchError())
1769
1764
  return false
@@ -112,10 +112,28 @@ class Request {
112
112
 
113
113
  this.method = method
114
114
 
115
+ this.abort = null
116
+
115
117
  if (body == null) {
116
118
  this.body = null
117
119
  } else if (util.isStream(body)) {
118
120
  this.body = body
121
+
122
+ if (!this.body._readableState?.autoDestroy) {
123
+ this.endHandler = function autoDestroy () {
124
+ util.destroy(this)
125
+ }
126
+ this.body.on('end', this.endHandler)
127
+ }
128
+
129
+ this.errorHandler = err => {
130
+ if (this.abort) {
131
+ this.abort(err)
132
+ } else {
133
+ this.error = err
134
+ }
135
+ }
136
+ this.body.on('error', this.errorHandler)
119
137
  } else if (util.isBuffer(body)) {
120
138
  this.body = body.byteLength ? body : null
121
139
  } else if (ArrayBuffer.isView(body)) {
@@ -222,13 +240,26 @@ class Request {
222
240
  if (channels.bodySent.hasSubscribers) {
223
241
  channels.bodySent.publish({ request: this })
224
242
  }
243
+
244
+ if (this[kHandler].onRequestSent) {
245
+ try {
246
+ this[kHandler].onRequestSent()
247
+ } catch (err) {
248
+ this.onError(err)
249
+ }
250
+ }
225
251
  }
226
252
 
227
253
  onConnect (abort) {
228
254
  assert(!this.aborted)
229
255
  assert(!this.completed)
230
256
 
231
- return this[kHandler].onConnect(abort)
257
+ if (this.error) {
258
+ abort(this.error)
259
+ } else {
260
+ this.abort = abort
261
+ return this[kHandler].onConnect(abort)
262
+ }
232
263
  }
233
264
 
234
265
  onHeaders (statusCode, headers, resume, statusText) {
@@ -257,6 +288,8 @@ class Request {
257
288
  }
258
289
 
259
290
  onComplete (trailers) {
291
+ this.onFinally()
292
+
260
293
  assert(!this.aborted)
261
294
 
262
295
  this.completed = true
@@ -267,6 +300,8 @@ class Request {
267
300
  }
268
301
 
269
302
  onError (error) {
303
+ this.onFinally()
304
+
270
305
  if (channels.error.hasSubscribers) {
271
306
  channels.error.publish({ request: this, error })
272
307
  }
@@ -278,6 +313,18 @@ class Request {
278
313
  return this[kHandler].onError(error)
279
314
  }
280
315
 
316
+ onFinally () {
317
+ if (this.errorHandler) {
318
+ this.body.off('error', this.errorHandler)
319
+ this.errorHandler = null
320
+ }
321
+
322
+ if (this.endHandler) {
323
+ this.body.off('end', this.endHandler)
324
+ this.endHandler = null
325
+ }
326
+ }
327
+
281
328
  // TODO: adjust to support H2
282
329
  addHeader (key, value) {
283
330
  processHeader(this, key, value)
package/lib/core/util.js CHANGED
@@ -190,7 +190,7 @@ function isReadableAborted (stream) {
190
190
  }
191
191
 
192
192
  function destroy (stream, err) {
193
- if (!isStream(stream) || isDestroyed(stream)) {
193
+ if (stream == null || !isStream(stream) || isDestroyed(stream)) {
194
194
  return
195
195
  }
196
196
 
package/lib/fetch/body.js CHANGED
@@ -26,6 +26,8 @@ let ReadableStream = globalThis.ReadableStream
26
26
 
27
27
  /** @type {globalThis['File']} */
28
28
  const File = NativeFile ?? UndiciFile
29
+ const textEncoder = new TextEncoder()
30
+ const textDecoder = new TextDecoder()
29
31
 
30
32
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
31
33
  function extractBody (object, keepalive = false) {
@@ -49,7 +51,7 @@ function extractBody (object, keepalive = false) {
49
51
  stream = new ReadableStream({
50
52
  async pull (controller) {
51
53
  controller.enqueue(
52
- typeof source === 'string' ? new TextEncoder().encode(source) : source
54
+ typeof source === 'string' ? textEncoder.encode(source) : source
53
55
  )
54
56
  queueMicrotask(() => readableStreamClose(controller))
55
57
  },
@@ -119,7 +121,6 @@ function extractBody (object, keepalive = false) {
119
121
  // - That the content-length is calculated in advance.
120
122
  // - And that all parts are pre-encoded and ready to be sent.
121
123
 
122
- const enc = new TextEncoder()
123
124
  const blobParts = []
124
125
  const rn = new Uint8Array([13, 10]) // '\r\n'
125
126
  length = 0
@@ -127,13 +128,13 @@ function extractBody (object, keepalive = false) {
127
128
 
128
129
  for (const [name, value] of object) {
129
130
  if (typeof value === 'string') {
130
- const chunk = enc.encode(prefix +
131
+ const chunk = textEncoder.encode(prefix +
131
132
  `; name="${escape(normalizeLinefeeds(name))}"` +
132
133
  `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
133
134
  blobParts.push(chunk)
134
135
  length += chunk.byteLength
135
136
  } else {
136
- const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
137
+ const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
137
138
  (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
138
139
  `Content-Type: ${
139
140
  value.type || 'application/octet-stream'
@@ -147,7 +148,7 @@ function extractBody (object, keepalive = false) {
147
148
  }
148
149
  }
149
150
 
150
- const chunk = enc.encode(`--${boundary}--`)
151
+ const chunk = textEncoder.encode(`--${boundary}--`)
151
152
  blobParts.push(chunk)
152
153
  length += chunk.byteLength
153
154
  if (hasUnknownSizeValue) {
@@ -443,14 +444,16 @@ function bodyMixinMethods (instance) {
443
444
  let text = ''
444
445
  // application/x-www-form-urlencoded parser will keep the BOM.
445
446
  // https://url.spec.whatwg.org/#concept-urlencoded-parser
446
- const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
447
+ // Note that streaming decoder is stateful and cannot be reused
448
+ const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
449
+
447
450
  for await (const chunk of consumeBody(this[kState].body)) {
448
451
  if (!isUint8Array(chunk)) {
449
452
  throw new TypeError('Expected Uint8Array chunk')
450
453
  }
451
- text += textDecoder.decode(chunk, { stream: true })
454
+ text += streamingDecoder.decode(chunk, { stream: true })
452
455
  }
453
- text += textDecoder.decode()
456
+ text += streamingDecoder.decode()
454
457
  entries = new URLSearchParams(text)
455
458
  } catch (err) {
456
459
  // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
@@ -565,7 +568,7 @@ function utf8DecodeBytes (buffer) {
565
568
 
566
569
  // 3. Process a queue with an instance of UTF-8’s
567
570
  // decoder, ioQueue, output, and "replacement".
568
- const output = new TextDecoder().decode(buffer)
571
+ const output = textDecoder.decode(buffer)
569
572
 
570
573
  // 4. Return output.
571
574
  return output
@@ -3,10 +3,12 @@
3
3
  const { MessageChannel, receiveMessageOnPort } = require('worker_threads')
4
4
 
5
5
  const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
6
+ const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
6
7
 
7
8
  const nullBodyStatus = [101, 204, 205, 304]
8
9
 
9
10
  const redirectStatus = [301, 302, 303, 307, 308]
11
+ const redirectStatusSet = new Set(redirectStatus)
10
12
 
11
13
  // https://fetch.spec.whatwg.org/#block-bad-port
12
14
  const badPorts = [
@@ -18,6 +20,8 @@ const badPorts = [
18
20
  '10080'
19
21
  ]
20
22
 
23
+ const badPortsSet = new Set(badPorts)
24
+
21
25
  // https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
22
26
  const referrerPolicy = [
23
27
  '',
@@ -30,10 +34,12 @@ const referrerPolicy = [
30
34
  'strict-origin-when-cross-origin',
31
35
  'unsafe-url'
32
36
  ]
37
+ const referrerPolicySet = new Set(referrerPolicy)
33
38
 
34
39
  const requestRedirect = ['follow', 'manual', 'error']
35
40
 
36
41
  const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
42
+ const safeMethodsSet = new Set(safeMethods)
37
43
 
38
44
  const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
39
45
 
@@ -68,6 +74,7 @@ const requestDuplex = [
68
74
 
69
75
  // http://fetch.spec.whatwg.org/#forbidden-method
70
76
  const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
77
+ const forbiddenMethodsSet = new Set(forbiddenMethods)
71
78
 
72
79
  const subresource = [
73
80
  'audio',
@@ -83,6 +90,7 @@ const subresource = [
83
90
  'xslt',
84
91
  ''
85
92
  ]
93
+ const subresourceSet = new Set(subresource)
86
94
 
87
95
  /** @type {globalThis['DOMException']} */
88
96
  const DOMException = globalThis.DOMException ?? (() => {
@@ -132,5 +140,12 @@ module.exports = {
132
140
  nullBodyStatus,
133
141
  safeMethods,
134
142
  badPorts,
135
- requestDuplex
143
+ requestDuplex,
144
+ subresourceSet,
145
+ badPortsSet,
146
+ redirectStatusSet,
147
+ corsSafeListedMethodsSet,
148
+ safeMethodsSet,
149
+ forbiddenMethodsSet,
150
+ referrerPolicySet
136
151
  }
package/lib/fetch/file.js CHANGED
@@ -7,6 +7,7 @@ const { isBlobLike } = require('./util')
7
7
  const { webidl } = require('./webidl')
8
8
  const { parseMIMEType, serializeAMimeType } = require('./dataURL')
9
9
  const { kEnumerableProperty } = require('../core/util')
10
+ const encoder = new TextEncoder()
10
11
 
11
12
  class File extends Blob {
12
13
  constructor (fileBits, fileName, options = {}) {
@@ -280,7 +281,7 @@ function processBlobParts (parts, options) {
280
281
  }
281
282
 
282
283
  // 3. Append the result of UTF-8 encoding s to bytes.
283
- bytes.push(new TextEncoder().encode(s))
284
+ bytes.push(encoder.encode(s))
284
285
  } else if (
285
286
  types.isAnyArrayBuffer(element) ||
286
287
  types.isTypedArray(element)
@@ -46,11 +46,11 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
46
46
  const assert = require('assert')
47
47
  const { safelyExtractBody } = require('./body')
48
48
  const {
49
- redirectStatus,
49
+ redirectStatusSet,
50
50
  nullBodyStatus,
51
- safeMethods,
51
+ safeMethodsSet,
52
52
  requestBodyHeader,
53
- subresource,
53
+ subresourceSet,
54
54
  DOMException
55
55
  } = require('./constants')
56
56
  const { kHeadersList } = require('../core/symbols')
@@ -62,6 +62,7 @@ const { TransformStream } = require('stream/web')
62
62
  const { getGlobalDispatcher } = require('../global')
63
63
  const { webidl } = require('./webidl')
64
64
  const { STATUS_CODES } = require('http')
65
+ const GET_OR_HEAD = ['GET', 'HEAD']
65
66
 
66
67
  /** @type {import('buffer').resolveObjectURL} */
67
68
  let resolveObjectURL
@@ -509,7 +510,7 @@ function fetching ({
509
510
  }
510
511
 
511
512
  // 15. If request is a subresource request, then:
512
- if (subresource.includes(request.destination)) {
513
+ if (subresourceSet.has(request.destination)) {
513
514
  // TODO
514
515
  }
515
516
 
@@ -1063,7 +1064,7 @@ async function httpFetch (fetchParams) {
1063
1064
  }
1064
1065
 
1065
1066
  // 8. If actualResponse’s status is a redirect status, then:
1066
- if (redirectStatus.includes(actualResponse.status)) {
1067
+ if (redirectStatusSet.has(actualResponse.status)) {
1067
1068
  // 1. If actualResponse’s status is not 303, request’s body is not null,
1068
1069
  // and the connection uses HTTP/2, then user agents may, and are even
1069
1070
  // encouraged to, transmit an RST_STREAM frame.
@@ -1181,7 +1182,7 @@ function httpRedirectFetch (fetchParams, response) {
1181
1182
  if (
1182
1183
  ([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
1183
1184
  (actualResponse.status === 303 &&
1184
- !['GET', 'HEAD'].includes(request.method))
1185
+ !GET_OR_HEAD.includes(request.method))
1185
1186
  ) {
1186
1187
  // then:
1187
1188
  // 1. Set request’s method to `GET` and request’s body to null.
@@ -1465,7 +1466,7 @@ async function httpNetworkOrCacheFetch (
1465
1466
  // responses in httpCache, as per the "Invalidation" chapter of HTTP
1466
1467
  // Caching, and set storedResponse to null. [HTTP-CACHING]
1467
1468
  if (
1468
- !safeMethods.includes(httpRequest.method) &&
1469
+ !safeMethodsSet.has(httpRequest.method) &&
1469
1470
  forwardResponse.status >= 200 &&
1470
1471
  forwardResponse.status <= 399
1471
1472
  ) {
@@ -2025,7 +2026,7 @@ async function httpNetworkFetch (
2025
2026
 
2026
2027
  const willFollow = request.redirect === 'follow' &&
2027
2028
  location &&
2028
- redirectStatus.includes(status)
2029
+ redirectStatusSet.has(status)
2029
2030
 
2030
2031
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2031
2032
  if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
@@ -13,8 +13,8 @@ const {
13
13
  makePolicyContainer
14
14
  } = require('./util')
15
15
  const {
16
- forbiddenMethods,
17
- corsSafeListedMethods,
16
+ forbiddenMethodsSet,
17
+ corsSafeListedMethodsSet,
18
18
  referrerPolicy,
19
19
  requestRedirect,
20
20
  requestMode,
@@ -319,7 +319,7 @@ class Request {
319
319
  throw TypeError(`'${init.method}' is not a valid HTTP method.`)
320
320
  }
321
321
 
322
- if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) {
322
+ if (forbiddenMethodsSet.has(method.toUpperCase())) {
323
323
  throw TypeError(`'${init.method}' HTTP method is unsupported.`)
324
324
  }
325
325
 
@@ -404,7 +404,7 @@ class Request {
404
404
  if (mode === 'no-cors') {
405
405
  // 1. If this’s request’s method is not a CORS-safelisted method,
406
406
  // then throw a TypeError.
407
- if (!corsSafeListedMethods.includes(request.method)) {
407
+ if (!corsSafeListedMethodsSet.has(request.method)) {
408
408
  throw new TypeError(
409
409
  `'${request.method} is unsupported in no-cors mode.`
410
410
  )
@@ -14,7 +14,7 @@ const {
14
14
  isomorphicEncode
15
15
  } = require('./util')
16
16
  const {
17
- redirectStatus,
17
+ redirectStatusSet,
18
18
  nullBodyStatus,
19
19
  DOMException
20
20
  } = require('./constants')
@@ -28,6 +28,7 @@ const assert = require('assert')
28
28
  const { types } = require('util')
29
29
 
30
30
  const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
31
+ const textEncoder = new TextEncoder('utf-8')
31
32
 
32
33
  // https://fetch.spec.whatwg.org/#response-class
33
34
  class Response {
@@ -57,7 +58,7 @@ class Response {
57
58
  }
58
59
 
59
60
  // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
60
- const bytes = new TextEncoder('utf-8').encode(
61
+ const bytes = textEncoder.encode(
61
62
  serializeJavascriptValueToJSONString(data)
62
63
  )
63
64
 
@@ -102,7 +103,7 @@ class Response {
102
103
  }
103
104
 
104
105
  // 3. If status is not a redirect status, then throw a RangeError.
105
- if (!redirectStatus.includes(status)) {
106
+ if (!redirectStatusSet.has(status)) {
106
107
  throw new RangeError('Invalid status code ' + status)
107
108
  }
108
109
 
package/lib/fetch/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants')
3
+ const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
4
4
  const { getGlobalOrigin } = require('./global')
5
5
  const { performance } = require('perf_hooks')
6
6
  const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
@@ -29,7 +29,7 @@ function responseURL (response) {
29
29
  // https://fetch.spec.whatwg.org/#concept-response-location-url
30
30
  function responseLocationURL (response, requestFragment) {
31
31
  // 1. If response’s status is not a redirect status, then return null.
32
- if (!redirectStatus.includes(response.status)) {
32
+ if (!redirectStatusSet.has(response.status)) {
33
33
  return null
34
34
  }
35
35
 
@@ -64,7 +64,7 @@ function requestBadPort (request) {
64
64
 
65
65
  // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
66
66
  // then return blocked.
67
- if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) {
67
+ if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
68
68
  return 'blocked'
69
69
  }
70
70
 
@@ -206,7 +206,7 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
206
206
  // The left-most policy is the fallback.
207
207
  for (let i = policyHeader.length; i !== 0; i--) {
208
208
  const token = policyHeader[i - 1].trim()
209
- if (referrerPolicyTokens.includes(token)) {
209
+ if (referrerPolicyTokens.has(token)) {
210
210
  policy = token
211
211
  break
212
212
  }
package/lib/pool.js CHANGED
@@ -57,7 +57,7 @@ class Pool extends PoolBase {
57
57
  maxCachedSessions,
58
58
  allowH2,
59
59
  socketPath,
60
- timeout: connectTimeout == null ? 10e3 : connectTimeout,
60
+ timeout: connectTimeout,
61
61
  ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
62
62
  ...connect
63
63
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.26.5",
3
+ "version": "5.27.1",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -84,7 +84,7 @@
84
84
  "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
85
85
  "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts",
86
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 --no-warnings --expose-internals test/wpt/start-websockets.mjs)",
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)",
88
88
  "coverage": "nyc --reporter=text --reporter=html npm run test",
89
89
  "coverage:ci": "nyc --reporter=lcov npm run test",
90
90
  "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",