undici 6.15.0 → 6.16.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.
@@ -8,11 +8,11 @@ Receives a header object and returns the parsed value.
8
8
 
9
9
  Arguments:
10
10
 
11
- - **headers** `Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]` (required) - Header object.
11
+ - **headers** `(Buffer | string | (Buffer | string)[])[]` (required) - Header object.
12
12
 
13
13
  - **obj** `Record<string, string | string[]>` (optional) - Object to specify a proxy object. The parsed value is assigned to this object. But, if **headers** is an object, it is not used.
14
14
 
15
- Returns: `Record<string, string | string[]>` If **headers** is an object, it is **headers**. Otherwise, if **obj** is specified, it is equivalent to **obj**.
15
+ Returns: `Record<string, string | string[]>` If **obj** is specified, it is equivalent to **obj**.
16
16
 
17
17
  ## `headerNameToString(value)`
18
18
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Connecting through a proxy is possible by:
4
4
 
5
- - Using [AgentProxy](../api/ProxyAgent.md).
5
+ - Using [ProxyAgent](../api/ProxyAgent.md).
6
6
  - Configuring `Client` or `Pool` constructor.
7
7
 
8
8
  The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url
@@ -2,11 +2,10 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
  const { Readable } = require('./readable')
5
- const { InvalidArgumentError } = require('../core/errors')
5
+ const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
6
6
  const util = require('../core/util')
7
7
  const { getResolveErrorBodyCallback } = require('./util')
8
8
  const { AsyncResource } = require('node:async_hooks')
9
- const { addSignal, removeSignal } = require('./abort-signal')
10
9
 
11
10
  class RequestHandler extends AsyncResource {
12
11
  constructor (opts, callback) {
@@ -45,6 +44,7 @@ class RequestHandler extends AsyncResource {
45
44
  throw err
46
45
  }
47
46
 
47
+ this.method = method
48
48
  this.responseHeaders = responseHeaders || null
49
49
  this.opaque = opaque || null
50
50
  this.callback = callback
@@ -56,6 +56,9 @@ class RequestHandler extends AsyncResource {
56
56
  this.onInfo = onInfo || null
57
57
  this.throwOnError = throwOnError
58
58
  this.highWaterMark = highWaterMark
59
+ this.signal = signal
60
+ this.reason = null
61
+ this.removeAbortListener = null
59
62
 
60
63
  if (util.isStream(body)) {
61
64
  body.on('error', (err) => {
@@ -63,7 +66,26 @@ class RequestHandler extends AsyncResource {
63
66
  })
64
67
  }
65
68
 
66
- addSignal(this, signal)
69
+ if (this.signal) {
70
+ if (this.signal.aborted) {
71
+ this.reason = this.signal.reason ?? new RequestAbortedError()
72
+ } else {
73
+ this.removeAbortListener = util.addAbortListener(this.signal, () => {
74
+ this.reason = this.signal.reason ?? new RequestAbortedError()
75
+ if (this.res) {
76
+ util.destroy(this.res, this.reason)
77
+ } else if (this.abort) {
78
+ this.abort(this.reason)
79
+ }
80
+
81
+ if (this.removeAbortListener) {
82
+ this.res?.off('close', this.removeAbortListener)
83
+ this.removeAbortListener()
84
+ this.removeAbortListener = null
85
+ }
86
+ })
87
+ }
88
+ }
67
89
  }
68
90
 
69
91
  onConnect (abort, context) {
@@ -93,14 +115,26 @@ class RequestHandler extends AsyncResource {
93
115
  const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
94
116
  const contentType = parsedHeaders['content-type']
95
117
  const contentLength = parsedHeaders['content-length']
96
- const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
118
+ const res = new Readable({
119
+ resume,
120
+ abort,
121
+ contentType,
122
+ contentLength: this.method !== 'HEAD' && contentLength
123
+ ? Number(contentLength)
124
+ : null,
125
+ highWaterMark
126
+ })
127
+
128
+ if (this.removeAbortListener) {
129
+ res.on('close', this.removeAbortListener)
130
+ }
97
131
 
98
132
  this.callback = null
99
- this.res = body
133
+ this.res = res
100
134
  if (callback !== null) {
101
135
  if (this.throwOnError && statusCode >= 400) {
102
136
  this.runInAsyncScope(getResolveErrorBodyCallback, null,
103
- { callback, body, contentType, statusCode, statusMessage, headers }
137
+ { callback, body: res, contentType, statusCode, statusMessage, headers }
104
138
  )
105
139
  } else {
106
140
  this.runInAsyncScope(callback, null, null, {
@@ -108,7 +142,7 @@ class RequestHandler extends AsyncResource {
108
142
  headers,
109
143
  trailers: this.trailers,
110
144
  opaque,
111
- body,
145
+ body: res,
112
146
  context
113
147
  })
114
148
  }
@@ -116,25 +150,17 @@ class RequestHandler extends AsyncResource {
116
150
  }
117
151
 
118
152
  onData (chunk) {
119
- const { res } = this
120
- return res.push(chunk)
153
+ return this.res.push(chunk)
121
154
  }
122
155
 
123
156
  onComplete (trailers) {
124
- const { res } = this
125
-
126
- removeSignal(this)
127
-
128
157
  util.parseHeaders(trailers, this.trailers)
129
-
130
- res.push(null)
158
+ this.res.push(null)
131
159
  }
132
160
 
133
161
  onError (err) {
134
162
  const { res, callback, body, opaque } = this
135
163
 
136
- removeSignal(this)
137
-
138
164
  if (callback) {
139
165
  // TODO: Does this need queueMicrotask?
140
166
  this.callback = null
@@ -155,6 +181,12 @@ class RequestHandler extends AsyncResource {
155
181
  this.body = null
156
182
  util.destroy(body, err)
157
183
  }
184
+
185
+ if (this.removeAbortListener) {
186
+ res?.off('close', this.removeAbortListener)
187
+ this.removeAbortListener()
188
+ this.removeAbortListener = null
189
+ }
158
190
  }
159
191
  }
160
192
 
@@ -63,9 +63,13 @@ class BodyReadable extends Readable {
63
63
  // tick as it is created, then a user who is waiting for a
64
64
  // promise (i.e micro tick) for installing a 'error' listener will
65
65
  // never get a chance and will always encounter an unhandled exception.
66
- setImmediate(() => {
66
+ if (!this[kReading]) {
67
+ setImmediate(() => {
68
+ callback(err)
69
+ })
70
+ } else {
67
71
  callback(err)
68
- })
72
+ }
69
73
  }
70
74
 
71
75
  on (ev, ...args) {
@@ -165,7 +165,7 @@ function setupTimeout (onConnectTimeout, timeout) {
165
165
  let s1 = null
166
166
  let s2 = null
167
167
  const timeoutId = setTimeout(() => {
168
- // setImmediate is added to make sure that we priotorise socket error events over timeouts
168
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
169
169
  s1 = setImmediate(() => {
170
170
  if (process.platform === 'win32') {
171
171
  // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
@@ -524,6 +524,7 @@ function writeH2 (client, request) {
524
524
  }
525
525
  } else if (util.isStream(body)) {
526
526
  writeStream({
527
+ abort,
527
528
  body,
528
529
  client,
529
530
  request,
@@ -535,6 +536,7 @@ function writeH2 (client, request) {
535
536
  })
536
537
  } else if (util.isIterable(body)) {
537
538
  writeIterable({
539
+ abort,
538
540
  body,
539
541
  client,
540
542
  request,
@@ -7,13 +7,13 @@ const encoder = new TextEncoder()
7
7
  /**
8
8
  * @see https://mimesniff.spec.whatwg.org/#http-token-code-point
9
9
  */
10
- const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/
10
+ const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+\-.^_|~A-Za-z0-9]+$/
11
11
  const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/ // eslint-disable-line
12
12
  const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line
13
13
  /**
14
14
  * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
15
15
  */
16
- const HTTP_QUOTED_STRING_TOKENS = /[\u0009\u0020-\u007E\u0080-\u00FF]/ // eslint-disable-line
16
+ const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/ // eslint-disable-line
17
17
 
18
18
  // https://fetch.spec.whatwg.org/#data-url-processor
19
19
  /** @param {URL} dataURL */
@@ -250,7 +250,7 @@ class HeadersList {
250
250
  get entries () {
251
251
  const headers = {}
252
252
 
253
- if (this[kHeadersMap].size) {
253
+ if (this[kHeadersMap].size !== 0) {
254
254
  for (const { name, value } of this[kHeadersMap].values()) {
255
255
  headers[name] = value
256
256
  }
@@ -259,6 +259,10 @@ class HeadersList {
259
259
  return headers
260
260
  }
261
261
 
262
+ rawValues () {
263
+ return this[kHeadersMap].values()
264
+ }
265
+
262
266
  get entriesList () {
263
267
  const headers = []
264
268
 
@@ -120,12 +120,16 @@ class Fetch extends EE {
120
120
  }
121
121
  }
122
122
 
123
+ function handleFetchDone (response) {
124
+ finalizeAndReportTiming(response, 'fetch')
125
+ }
126
+
123
127
  // https://fetch.spec.whatwg.org/#fetch-method
124
128
  function fetch (input, init = undefined) {
125
129
  webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch')
126
130
 
127
131
  // 1. Let p be a new promise.
128
- const p = createDeferredPromise()
132
+ let p = createDeferredPromise()
129
133
 
130
134
  // 2. Let requestObject be the result of invoking the initial value of
131
135
  // Request as constructor with input and init as arguments. If this throws
@@ -185,16 +189,17 @@ function fetch (input, init = undefined) {
185
189
  // 3. Abort controller with requestObject’s signal’s abort reason.
186
190
  controller.abort(requestObject.signal.reason)
187
191
 
192
+ const realResponse = responseObject?.deref()
193
+
188
194
  // 4. Abort the fetch() call with p, request, responseObject,
189
195
  // and requestObject’s signal’s abort reason.
190
- abortFetch(p, request, responseObject, requestObject.signal.reason)
196
+ abortFetch(p, request, realResponse, requestObject.signal.reason)
191
197
  }
192
198
  )
193
199
 
194
200
  // 12. Let handleFetchDone given response response be to finalize and
195
201
  // report timing with response, globalObject, and "fetch".
196
- const handleFetchDone = (response) =>
197
- finalizeAndReportTiming(response, 'fetch')
202
+ // see function handleFetchDone
198
203
 
199
204
  // 13. Set controller to the result of calling fetch given request,
200
205
  // with processResponseEndOfBody set to handleFetchDone, and processResponse
@@ -228,10 +233,11 @@ function fetch (input, init = undefined) {
228
233
 
229
234
  // 4. Set responseObject to the result of creating a Response object,
230
235
  // given response, "immutable", and relevantRealm.
231
- responseObject = fromInnerResponse(response, 'immutable')
236
+ responseObject = new WeakRef(fromInnerResponse(response, 'immutable'))
232
237
 
233
238
  // 5. Resolve p with responseObject.
234
- p.resolve(responseObject)
239
+ p.resolve(responseObject.deref())
240
+ p = null
235
241
  }
236
242
 
237
243
  controller = fetching({
@@ -314,7 +320,10 @@ const markResourceTiming = performance.markResourceTiming
314
320
  // https://fetch.spec.whatwg.org/#abort-fetch
315
321
  function abortFetch (p, request, responseObject, error) {
316
322
  // 1. Reject promise with error.
317
- p.reject(error)
323
+ if (p) {
324
+ // We might have already resolved the promise at this stage
325
+ p.reject(error)
326
+ }
318
327
 
319
328
  // 2. If request’s body is not null and is readable, then cancel request’s
320
329
  // body with error.
@@ -435,8 +444,7 @@ function fetching ({
435
444
  // 9. If request’s origin is "client", then set request’s origin to request’s
436
445
  // client’s origin.
437
446
  if (request.origin === 'client') {
438
- // TODO: What if request.client is null?
439
- request.origin = request.client?.origin
447
+ request.origin = request.client.origin
440
448
  }
441
449
 
442
450
  // 10. If all of the following conditions are true:
@@ -1066,7 +1074,10 @@ function fetchFinale (fetchParams, response) {
1066
1074
  // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
1067
1075
  // process response given response, with fetchParams’s task destination.
1068
1076
  if (fetchParams.processResponse != null) {
1069
- queueMicrotask(() => fetchParams.processResponse(response))
1077
+ queueMicrotask(() => {
1078
+ fetchParams.processResponse(response)
1079
+ fetchParams.processResponse = null
1080
+ })
1070
1081
  }
1071
1082
 
1072
1083
  // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
@@ -1884,7 +1895,11 @@ async function httpNetworkFetch (
1884
1895
  // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
1885
1896
  // controller with reason, given reason.
1886
1897
  const cancelAlgorithm = (reason) => {
1887
- fetchParams.controller.abort(reason)
1898
+ // If the aborted fetch was already terminated, then we do not
1899
+ // need to do anything.
1900
+ if (!isCancelled(fetchParams)) {
1901
+ fetchParams.controller.abort(reason)
1902
+ }
1888
1903
  }
1889
1904
 
1890
1905
  // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
@@ -2102,20 +2117,16 @@ async function httpNetworkFetch (
2102
2117
 
2103
2118
  const headersList = new HeadersList()
2104
2119
 
2105
- // For H2, the rawHeaders are a plain JS object
2106
- // We distinguish between them and iterate accordingly
2107
- if (Array.isArray(rawHeaders)) {
2108
- for (let i = 0; i < rawHeaders.length; i += 2) {
2109
- headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2110
- }
2111
- const contentEncoding = headersList.get('content-encoding', true)
2112
- if (contentEncoding) {
2113
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2114
- // "All content-coding values are case-insensitive..."
2115
- codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2116
- }
2117
- location = headersList.get('location', true)
2120
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2121
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2122
+ }
2123
+ const contentEncoding = headersList.get('content-encoding', true)
2124
+ if (contentEncoding) {
2125
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2126
+ // "All content-coding values are case-insensitive..."
2127
+ codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2118
2128
  }
2129
+ location = headersList.get('location', true)
2119
2130
 
2120
2131
  this.body = new Readable({ read: resume })
2121
2132
 
@@ -2125,7 +2136,7 @@ async function httpNetworkFetch (
2125
2136
  redirectStatusSet.has(status)
2126
2137
 
2127
2138
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2128
- if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2139
+ if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2129
2140
  for (let i = 0; i < codings.length; ++i) {
2130
2141
  const coding = codings[i]
2131
2142
  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
@@ -477,9 +477,8 @@ class Request {
477
477
  // 4. If headers is a Headers object, then for each header in its header
478
478
  // list, append header’s name/header’s value to this’s headers.
479
479
  if (headers instanceof HeadersList) {
480
- for (const { 0: key, 1: val } of headers) {
481
- // Note: The header names are already in lowercase.
482
- headersList.append(key, val, true)
480
+ for (const { name, value } of headers.rawValues()) {
481
+ headersList.append(name, value, false)
483
482
  }
484
483
  // Note: Copy the `set-cookie` meta-data.
485
484
  headersList.cookies = headers.cookies
@@ -820,51 +819,50 @@ class Request {
820
819
 
821
820
  mixinBody(Request)
822
821
 
822
+ // https://fetch.spec.whatwg.org/#requests
823
823
  function makeRequest (init) {
824
- // https://fetch.spec.whatwg.org/#requests
825
- const request = {
826
- method: 'GET',
827
- localURLsOnly: false,
828
- unsafeRequest: false,
829
- body: null,
830
- client: null,
831
- reservedClient: null,
832
- replacesClientId: '',
833
- window: 'client',
834
- keepalive: false,
835
- serviceWorkers: 'all',
836
- initiator: '',
837
- destination: '',
838
- priority: null,
839
- origin: 'client',
840
- policyContainer: 'client',
841
- referrer: 'client',
842
- referrerPolicy: '',
843
- mode: 'no-cors',
844
- useCORSPreflightFlag: false,
845
- credentials: 'same-origin',
846
- useCredentials: false,
847
- cache: 'default',
848
- redirect: 'follow',
849
- integrity: '',
850
- cryptoGraphicsNonceMetadata: '',
851
- parserMetadata: '',
852
- reloadNavigation: false,
853
- historyNavigation: false,
854
- userActivation: false,
855
- taintedOrigin: false,
856
- redirectCount: 0,
857
- responseTainting: 'basic',
858
- preventNoCacheCacheControlHeaderModification: false,
859
- done: false,
860
- timingAllowFailed: false,
861
- ...init,
824
+ return {
825
+ method: init.method ?? 'GET',
826
+ localURLsOnly: init.localURLsOnly ?? false,
827
+ unsafeRequest: init.unsafeRequest ?? false,
828
+ body: init.body ?? null,
829
+ client: init.client ?? null,
830
+ reservedClient: init.reservedClient ?? null,
831
+ replacesClientId: init.replacesClientId ?? '',
832
+ window: init.window ?? 'client',
833
+ keepalive: init.keepalive ?? false,
834
+ serviceWorkers: init.serviceWorkers ?? 'all',
835
+ initiator: init.initiator ?? '',
836
+ destination: init.destination ?? '',
837
+ priority: init.priority ?? null,
838
+ origin: init.origin ?? 'client',
839
+ policyContainer: init.policyContainer ?? 'client',
840
+ referrer: init.referrer ?? 'client',
841
+ referrerPolicy: init.referrerPolicy ?? '',
842
+ mode: init.mode ?? 'no-cors',
843
+ useCORSPreflightFlag: init.useCORSPreflightFlag ?? false,
844
+ credentials: init.credentials ?? 'same-origin',
845
+ useCredentials: init.useCredentials ?? false,
846
+ cache: init.cache ?? 'default',
847
+ redirect: init.redirect ?? 'follow',
848
+ integrity: init.integrity ?? '',
849
+ cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? '',
850
+ parserMetadata: init.parserMetadata ?? '',
851
+ reloadNavigation: init.reloadNavigation ?? false,
852
+ historyNavigation: init.historyNavigation ?? false,
853
+ userActivation: init.userActivation ?? false,
854
+ taintedOrigin: init.taintedOrigin ?? false,
855
+ redirectCount: init.redirectCount ?? 0,
856
+ responseTainting: init.responseTainting ?? 'basic',
857
+ preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false,
858
+ done: init.done ?? false,
859
+ timingAllowFailed: init.timingAllowFailed ?? false,
860
+ urlList: init.urlList,
861
+ url: init.urlList[0],
862
862
  headersList: init.headersList
863
863
  ? new HeadersList(init.headersList)
864
864
  : new HeadersList()
865
865
  }
866
- request.url = request.urlList[0]
867
- return request
868
866
  }
869
867
 
870
868
  // https://fetch.spec.whatwg.org/#concept-request-clone
@@ -26,9 +26,23 @@ const { URLSerializer } = require('./data-url')
26
26
  const { kHeadersList, kConstruct } = require('../../core/symbols')
27
27
  const assert = require('node:assert')
28
28
  const { types } = require('node:util')
29
+ const { isDisturbed, isErrored } = require('node:stream')
29
30
 
30
31
  const textEncoder = new TextEncoder('utf-8')
31
32
 
33
+ const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
34
+ let registry
35
+
36
+ if (hasFinalizationRegistry) {
37
+ registry = new FinalizationRegistry((stream) => {
38
+ if (!stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
39
+ stream.cancel('Response object has been garbage collected').catch(noop)
40
+ }
41
+ })
42
+ }
43
+
44
+ function noop () {}
45
+
32
46
  // https://fetch.spec.whatwg.org/#response-class
33
47
  class Response {
34
48
  // Creates network error Response.
@@ -510,6 +524,11 @@ function fromInnerResponse (innerResponse, guard) {
510
524
  response[kHeaders] = new Headers(kConstruct)
511
525
  response[kHeaders][kHeadersList] = innerResponse.headersList
512
526
  response[kHeaders][kGuard] = guard
527
+
528
+ if (hasFinalizationRegistry && innerResponse.body?.stream) {
529
+ registry.register(response, innerResponse.body.stream)
530
+ }
531
+
513
532
  return response
514
533
  }
515
534
 
@@ -255,16 +255,23 @@ function appendFetchMetadata (httpRequest) {
255
255
 
256
256
  // https://fetch.spec.whatwg.org/#append-a-request-origin-header
257
257
  function appendRequestOriginHeader (request) {
258
- // 1. Let serializedOrigin be the result of byte-serializing a request origin with request.
258
+ // 1. Let serializedOrigin be the result of byte-serializing a request origin
259
+ // with request.
260
+ // TODO: implement "byte-serializing a request origin"
259
261
  let serializedOrigin = request.origin
260
262
 
261
- // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
262
- if (request.responseTainting === 'cors' || request.mode === 'websocket') {
263
- if (serializedOrigin) {
264
- request.headersList.append('origin', serializedOrigin, true)
265
- }
263
+ // "'client' is changed to an origin during fetching."
264
+ // This doesn't happen in undici (in most cases) because undici, by default,
265
+ // has no concept of origin.
266
+ if (serializedOrigin === 'client') {
267
+ return
268
+ }
266
269
 
270
+ // 2. If request’s response tainting is "cors" or request’s mode is "websocket",
271
+ // then append (`Origin`, serializedOrigin) to request’s header list.
267
272
  // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
273
+ if (request.responseTainting === 'cors' || request.mode === 'websocket') {
274
+ request.headersList.append('origin', serializedOrigin, true)
268
275
  } else if (request.method !== 'GET' && request.method !== 'HEAD') {
269
276
  // 1. Switch on request’s referrer policy:
270
277
  switch (request.referrerPolicy) {
@@ -275,13 +282,16 @@ function appendRequestOriginHeader (request) {
275
282
  case 'no-referrer-when-downgrade':
276
283
  case 'strict-origin':
277
284
  case 'strict-origin-when-cross-origin':
278
- // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
285
+ // If request’s origin is a tuple origin, its scheme is "https", and
286
+ // request’s current URL’s scheme is not "https", then set
287
+ // serializedOrigin to `null`.
279
288
  if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
280
289
  serializedOrigin = null
281
290
  }
282
291
  break
283
292
  case 'same-origin':
284
- // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`.
293
+ // If request’s origin is not same origin with request’s current URL’s
294
+ // origin, then set serializedOrigin to `null`.
285
295
  if (!sameOrigin(request, requestCurrentURL(request))) {
286
296
  serializedOrigin = null
287
297
  }
@@ -290,10 +300,8 @@ function appendRequestOriginHeader (request) {
290
300
  // Do nothing.
291
301
  }
292
302
 
293
- if (serializedOrigin) {
294
- // 2. Append (`Origin`, serializedOrigin) to request’s header list.
295
- request.headersList.append('origin', serializedOrigin, true)
296
- }
303
+ // 2. Append (`Origin`, serializedOrigin) to request’s header list.
304
+ request.headersList.append('origin', serializedOrigin, true)
297
305
  }
298
306
  }
299
307
 
@@ -250,6 +250,7 @@ webidl.sequenceConverter = function (converter) {
250
250
  /** @type {Generator} */
251
251
  const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.()
252
252
  const seq = []
253
+ let index = 0
253
254
 
254
255
  // 3. If method is undefined, throw a TypeError.
255
256
  if (
@@ -270,7 +271,7 @@ webidl.sequenceConverter = function (converter) {
270
271
  break
271
272
  }
272
273
 
273
- seq.push(converter(value, prefix, argument))
274
+ seq.push(converter(value, prefix, `${argument}[${index++}]`))
274
275
  }
275
276
 
276
277
  return seq
@@ -1,13 +1,14 @@
1
1
  'use strict'
2
2
 
3
- const { uid, states, sentCloseFrameState } = require('./constants')
3
+ const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
4
4
  const {
5
5
  kReadyState,
6
6
  kSentClose,
7
7
  kByteParser,
8
- kReceivedClose
8
+ kReceivedClose,
9
+ kResponse
9
10
  } = require('./symbols')
10
- const { fireEvent, failWebsocketConnection } = require('./util')
11
+ const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util')
11
12
  const { channels } = require('../../core/diagnostics')
12
13
  const { CloseEvent } = require('./events')
13
14
  const { makeRequest } = require('../fetch/request')
@@ -15,6 +16,7 @@ const { fetching } = require('../fetch/index')
15
16
  const { Headers } = require('../fetch/headers')
16
17
  const { getDecodeSplit } = require('../fetch/util')
17
18
  const { kHeadersList } = require('../../core/symbols')
19
+ const { WebsocketFrameSend } = require('./frame')
18
20
 
19
21
  /** @type {import('crypto')} */
20
22
  let crypto
@@ -33,7 +35,7 @@ try {
33
35
  * @param {(response: any) => void} onEstablish
34
36
  * @param {Partial<import('../../types/websocket').WebSocketInit>} options
35
37
  */
36
- function establishWebSocketConnection (url, protocols, ws, onEstablish, options) {
38
+ function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) {
37
39
  // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
38
40
  // scheme is "ws", and to "https" otherwise.
39
41
  const requestURL = url
@@ -46,6 +48,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options)
46
48
  // and redirect mode is "error".
47
49
  const request = makeRequest({
48
50
  urlList: [requestURL],
51
+ client,
49
52
  serviceWorkers: 'none',
50
53
  referrer: 'no-referrer',
51
54
  mode: 'websocket',
@@ -211,6 +214,72 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options)
211
214
  return controller
212
215
  }
213
216
 
217
+ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
218
+ if (isClosing(ws) || isClosed(ws)) {
219
+ // If this's ready state is CLOSING (2) or CLOSED (3)
220
+ // Do nothing.
221
+ } else if (!isEstablished(ws)) {
222
+ // If the WebSocket connection is not yet established
223
+ // Fail the WebSocket connection and set this's ready state
224
+ // to CLOSING (2).
225
+ failWebsocketConnection(ws, 'Connection was closed before it was established.')
226
+ ws[kReadyState] = states.CLOSING
227
+ } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) {
228
+ // If the WebSocket closing handshake has not yet been started
229
+ // Start the WebSocket closing handshake and set this's ready
230
+ // state to CLOSING (2).
231
+ // - If neither code nor reason is present, the WebSocket Close
232
+ // message must not have a body.
233
+ // - If code is present, then the status code to use in the
234
+ // WebSocket Close message must be the integer given by code.
235
+ // - If reason is also present, then reasonBytes must be
236
+ // provided in the Close message after the status code.
237
+
238
+ ws[kSentClose] = sentCloseFrameState.PROCESSING
239
+
240
+ const frame = new WebsocketFrameSend()
241
+
242
+ // If neither code nor reason is present, the WebSocket Close
243
+ // message must not have a body.
244
+
245
+ // If code is present, then the status code to use in the
246
+ // WebSocket Close message must be the integer given by code.
247
+ if (code !== undefined && reason === undefined) {
248
+ frame.frameData = Buffer.allocUnsafe(2)
249
+ frame.frameData.writeUInt16BE(code, 0)
250
+ } else if (code !== undefined && reason !== undefined) {
251
+ // If reason is also present, then reasonBytes must be
252
+ // provided in the Close message after the status code.
253
+ frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
254
+ frame.frameData.writeUInt16BE(code, 0)
255
+ // the body MAY contain UTF-8-encoded data with value /reason/
256
+ frame.frameData.write(reason, 2, 'utf-8')
257
+ } else {
258
+ frame.frameData = emptyBuffer
259
+ }
260
+
261
+ /** @type {import('stream').Duplex} */
262
+ const socket = ws[kResponse].socket
263
+
264
+ socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
265
+ if (!err) {
266
+ ws[kSentClose] = sentCloseFrameState.SENT
267
+ }
268
+ })
269
+
270
+ ws[kSentClose] = sentCloseFrameState.PROCESSING
271
+
272
+ // Upon either sending or receiving a Close control frame, it is said
273
+ // that _The WebSocket Closing Handshake is Started_ and that the
274
+ // WebSocket connection is in the CLOSING state.
275
+ ws[kReadyState] = states.CLOSING
276
+ } else {
277
+ // Otherwise
278
+ // Set this's ready state to CLOSING (2).
279
+ ws[kReadyState] = states.CLOSING
280
+ }
281
+ }
282
+
214
283
  /**
215
284
  * @param {Buffer} chunk
216
285
  */
@@ -237,10 +306,10 @@ function onSocketClose () {
237
306
 
238
307
  const result = ws[kByteParser].closingInfo
239
308
 
240
- if (result) {
309
+ if (result && !result.error) {
241
310
  code = result.code ?? 1005
242
311
  reason = result.reason
243
- } else if (ws[kSentClose] !== sentCloseFrameState.SENT) {
312
+ } else if (!ws[kReceivedClose]) {
244
313
  // If _The WebSocket
245
314
  // Connection is Closed_ and no Close control frame was received by the
246
315
  // endpoint (such as could occur if the underlying transport connection
@@ -293,5 +362,6 @@ function onSocketError (error) {
293
362
  }
294
363
 
295
364
  module.exports = {
296
- establishWebSocketConnection
365
+ establishWebSocketConnection,
366
+ closeWebSocketConnection
297
367
  }
@@ -2,13 +2,34 @@
2
2
 
3
3
  const { maxUnsigned16Bit } = require('./constants')
4
4
 
5
+ const BUFFER_SIZE = 16386
6
+
5
7
  /** @type {import('crypto')} */
6
8
  let crypto
9
+ let buffer = null
10
+ let bufIdx = BUFFER_SIZE
11
+
7
12
  try {
8
13
  crypto = require('node:crypto')
9
14
  /* c8 ignore next 3 */
10
15
  } catch {
16
+ crypto = {
17
+ // not full compatibility, but minimum.
18
+ randomFillSync: function randomFillSync (buffer, _offset, _size) {
19
+ for (let i = 0; i < buffer.length; ++i) {
20
+ buffer[i] = Math.random() * 255 | 0
21
+ }
22
+ return buffer
23
+ }
24
+ }
25
+ }
11
26
 
27
+ function generateMask () {
28
+ if (bufIdx === BUFFER_SIZE) {
29
+ bufIdx = 0
30
+ crypto.randomFillSync((buffer ??= Buffer.allocUnsafe(BUFFER_SIZE)), 0, BUFFER_SIZE)
31
+ }
32
+ return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
12
33
  }
13
34
 
14
35
  class WebsocketFrameSend {
@@ -17,11 +38,12 @@ class WebsocketFrameSend {
17
38
  */
18
39
  constructor (data) {
19
40
  this.frameData = data
20
- this.maskKey = crypto.randomBytes(4)
21
41
  }
22
42
 
23
43
  createFrame (opcode) {
24
- const bodyLength = this.frameData?.byteLength ?? 0
44
+ const frameData = this.frameData
45
+ const maskKey = generateMask()
46
+ const bodyLength = frameData?.byteLength ?? 0
25
47
 
26
48
  /** @type {number} */
27
49
  let payloadLength = bodyLength // 0-125
@@ -43,10 +65,10 @@ class WebsocketFrameSend {
43
65
  buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
44
66
 
45
67
  /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
46
- buffer[offset - 4] = this.maskKey[0]
47
- buffer[offset - 3] = this.maskKey[1]
48
- buffer[offset - 2] = this.maskKey[2]
49
- buffer[offset - 1] = this.maskKey[3]
68
+ buffer[offset - 4] = maskKey[0]
69
+ buffer[offset - 3] = maskKey[1]
70
+ buffer[offset - 2] = maskKey[2]
71
+ buffer[offset - 1] = maskKey[3]
50
72
 
51
73
  buffer[1] = payloadLength
52
74
 
@@ -61,8 +83,8 @@ class WebsocketFrameSend {
61
83
  buffer[1] |= 0x80 // MASK
62
84
 
63
85
  // mask body
64
- for (let i = 0; i < bodyLength; i++) {
65
- buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4]
86
+ for (let i = 0; i < bodyLength; ++i) {
87
+ buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
66
88
  }
67
89
 
68
90
  return buffer
@@ -6,6 +6,7 @@ const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbol
6
6
  const { channels } = require('../../core/diagnostics')
7
7
  const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util')
8
8
  const { WebsocketFrameSend } = require('./frame')
9
+ const { CloseEvent } = require('./events')
9
10
 
10
11
  // This code was influenced by ws released under the MIT license.
11
12
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -55,6 +56,12 @@ class ByteParser extends Writable {
55
56
 
56
57
  this.#info.fin = (buffer[0] & 0x80) !== 0
57
58
  this.#info.opcode = buffer[0] & 0x0F
59
+ this.#info.masked = (buffer[1] & 0x80) === 0x80
60
+
61
+ if (this.#info.masked) {
62
+ failWebsocketConnection(this.ws, 'Frame cannot be masked')
63
+ return callback()
64
+ }
58
65
 
59
66
  // If we receive a fragmented message, we use the type of the first
60
67
  // frame to parse the full message as binary/text, when it's terminated
@@ -102,6 +109,13 @@ class ByteParser extends Writable {
102
109
 
103
110
  this.#info.closeInfo = this.parseCloseBody(body)
104
111
 
112
+ if (this.#info.closeInfo.error) {
113
+ const { code, reason } = this.#info.closeInfo
114
+
115
+ callback(new CloseEvent('close', { wasClean: false, reason, code }))
116
+ return
117
+ }
118
+
105
119
  if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
106
120
  // If an endpoint receives a Close frame and did not previously send a
107
121
  // Close frame, the endpoint MUST send a Close frame in response. (When
@@ -198,7 +212,7 @@ class ByteParser extends Writable {
198
212
  const buffer = this.consume(8)
199
213
  const upper = buffer.readUInt32BE(0)
200
214
 
201
- // 2^31 is the maxinimum bytes an arraybuffer can contain
215
+ // 2^31 is the maximum bytes an arraybuffer can contain
202
216
  // on 32-bit systems. Although, on 64-bit systems, this is
203
217
  // 2^53-1 bytes.
204
218
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
@@ -239,7 +253,7 @@ class ByteParser extends Writable {
239
253
  }
240
254
  }
241
255
 
242
- if (this.#byteOffset === 0) {
256
+ if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) {
243
257
  callback()
244
258
  break
245
259
  }
@@ -300,6 +314,10 @@ class ByteParser extends Writable {
300
314
  code = data.readUInt16BE(0)
301
315
  }
302
316
 
317
+ if (code !== undefined && !isValidStatusCode(code)) {
318
+ return { code: 1002, reason: 'Invalid status code', error: true }
319
+ }
320
+
303
321
  // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
304
322
  /** @type {Buffer} */
305
323
  let reason = data.subarray(2)
@@ -309,17 +327,13 @@ class ByteParser extends Writable {
309
327
  reason = reason.subarray(3)
310
328
  }
311
329
 
312
- if (code !== undefined && !isValidStatusCode(code)) {
313
- return null
314
- }
315
-
316
330
  try {
317
331
  reason = utf8Decode(reason)
318
332
  } catch {
319
- return null
333
+ return { code: 1007, reason: 'Invalid UTF-8', error: true }
320
334
  }
321
335
 
322
- return { code, reason }
336
+ return { code, reason, error: false }
323
337
  }
324
338
 
325
339
  get closingInfo () {
@@ -104,7 +104,7 @@ function websocketMessageReceived (ws, type, data) {
104
104
  // -> type indicates that the data is Binary and binary type is "arraybuffer"
105
105
  // a new ArrayBuffer object, created in the relevant Realm of the
106
106
  // WebSocket object, whose contents are data
107
- dataForEvent = new Uint8Array(data).buffer
107
+ dataForEvent = toArrayBuffer(data)
108
108
  }
109
109
  }
110
110
 
@@ -117,6 +117,13 @@ function websocketMessageReceived (ws, type, data) {
117
117
  })
118
118
  }
119
119
 
120
+ function toArrayBuffer (buffer) {
121
+ if (buffer.byteLength === buffer.buffer.byteLength) {
122
+ return buffer.buffer
123
+ }
124
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
125
+ }
126
+
120
127
  /**
121
128
  * @see https://datatracker.ietf.org/doc/html/rfc6455
122
129
  * @see https://datatracker.ietf.org/doc/html/rfc2616
@@ -197,7 +204,8 @@ function failWebsocketConnection (ws, reason) {
197
204
  if (reason) {
198
205
  // TODO: process.nextTick
199
206
  fireEvent('error', ws, (type, init) => new ErrorEvent(type, init), {
200
- error: new Error(reason)
207
+ error: new Error(reason),
208
+ message: reason
201
209
  })
202
210
  }
203
211
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  const { webidl } = require('../fetch/webidl')
4
4
  const { URLSerializer } = require('../fetch/data-url')
5
- const { getGlobalOrigin } = require('../fetch/global')
6
- const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes, emptyBuffer } = require('./constants')
5
+ const { environmentSettingsObject } = require('../fetch/util')
6
+ const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes } = require('./constants')
7
7
  const {
8
8
  kWebSocketURL,
9
9
  kReadyState,
@@ -16,21 +16,22 @@ const {
16
16
  const {
17
17
  isConnecting,
18
18
  isEstablished,
19
- isClosed,
20
19
  isClosing,
21
20
  isValidSubprotocol,
22
- failWebsocketConnection,
23
21
  fireEvent
24
22
  } = require('./util')
25
- const { establishWebSocketConnection } = require('./connection')
23
+ const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection')
26
24
  const { WebsocketFrameSend } = require('./frame')
27
25
  const { ByteParser } = require('./receiver')
28
26
  const { kEnumerableProperty, isBlobLike } = require('../../core/util')
29
27
  const { getGlobalDispatcher } = require('../../global')
30
28
  const { types } = require('node:util')
29
+ const { ErrorEvent } = require('./events')
31
30
 
32
31
  let experimentalWarned = false
33
32
 
33
+ const FastBuffer = Buffer[Symbol.species]
34
+
34
35
  // https://websockets.spec.whatwg.org/#interface-definition
35
36
  class WebSocket extends EventTarget {
36
37
  #events = {
@@ -67,7 +68,7 @@ class WebSocket extends EventTarget {
67
68
  protocols = options.protocols
68
69
 
69
70
  // 1. Let baseURL be this's relevant settings object's API base URL.
70
- const baseURL = getGlobalOrigin()
71
+ const baseURL = environmentSettingsObject.settingsObject.baseUrl
71
72
 
72
73
  // 1. Let urlRecord be the result of applying the URL parser to url with baseURL.
73
74
  let urlRecord
@@ -123,6 +124,7 @@ class WebSocket extends EventTarget {
123
124
  this[kWebSocketURL] = new URL(urlRecord.href)
124
125
 
125
126
  // 11. Let client be this's relevant settings object.
127
+ const client = environmentSettingsObject.settingsObject
126
128
 
127
129
  // 12. Run this step in parallel:
128
130
 
@@ -131,6 +133,7 @@ class WebSocket extends EventTarget {
131
133
  this[kController] = establishWebSocketConnection(
132
134
  urlRecord,
133
135
  protocols,
136
+ client,
134
137
  this,
135
138
  (response) => this.#onConnectionEstablished(response),
136
139
  options
@@ -197,67 +200,7 @@ class WebSocket extends EventTarget {
197
200
  }
198
201
 
199
202
  // 3. Run the first matching steps from the following list:
200
- if (isClosing(this) || isClosed(this)) {
201
- // If this's ready state is CLOSING (2) or CLOSED (3)
202
- // Do nothing.
203
- } else if (!isEstablished(this)) {
204
- // If the WebSocket connection is not yet established
205
- // Fail the WebSocket connection and set this's ready state
206
- // to CLOSING (2).
207
- failWebsocketConnection(this, 'Connection was closed before it was established.')
208
- this[kReadyState] = WebSocket.CLOSING
209
- } else if (this[kSentClose] === sentCloseFrameState.NOT_SENT) {
210
- // If the WebSocket closing handshake has not yet been started
211
- // Start the WebSocket closing handshake and set this's ready
212
- // state to CLOSING (2).
213
- // - If neither code nor reason is present, the WebSocket Close
214
- // message must not have a body.
215
- // - If code is present, then the status code to use in the
216
- // WebSocket Close message must be the integer given by code.
217
- // - If reason is also present, then reasonBytes must be
218
- // provided in the Close message after the status code.
219
-
220
- this[kSentClose] = sentCloseFrameState.PROCESSING
221
-
222
- const frame = new WebsocketFrameSend()
223
-
224
- // If neither code nor reason is present, the WebSocket Close
225
- // message must not have a body.
226
-
227
- // If code is present, then the status code to use in the
228
- // WebSocket Close message must be the integer given by code.
229
- if (code !== undefined && reason === undefined) {
230
- frame.frameData = Buffer.allocUnsafe(2)
231
- frame.frameData.writeUInt16BE(code, 0)
232
- } else if (code !== undefined && reason !== undefined) {
233
- // If reason is also present, then reasonBytes must be
234
- // provided in the Close message after the status code.
235
- frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
236
- frame.frameData.writeUInt16BE(code, 0)
237
- // the body MAY contain UTF-8-encoded data with value /reason/
238
- frame.frameData.write(reason, 2, 'utf-8')
239
- } else {
240
- frame.frameData = emptyBuffer
241
- }
242
-
243
- /** @type {import('stream').Duplex} */
244
- const socket = this[kResponse].socket
245
-
246
- socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
247
- if (!err) {
248
- this[kSentClose] = sentCloseFrameState.SENT
249
- }
250
- })
251
-
252
- // Upon either sending or receiving a Close control frame, it is said
253
- // that _The WebSocket Closing Handshake is Started_ and that the
254
- // WebSocket connection is in the CLOSING state.
255
- this[kReadyState] = states.CLOSING
256
- } else {
257
- // Otherwise
258
- // Set this's ready state to CLOSING (2).
259
- this[kReadyState] = WebSocket.CLOSING
260
- }
203
+ closeWebSocketConnection(this, code, reason, reasonByteLength)
261
204
  }
262
205
 
263
206
  /**
@@ -323,7 +266,7 @@ class WebSocket extends EventTarget {
323
266
  // increase the bufferedAmount attribute by the length of the
324
267
  // ArrayBuffer in bytes.
325
268
 
326
- const value = Buffer.from(data)
269
+ const value = new FastBuffer(data)
327
270
  const frame = new WebsocketFrameSend(value)
328
271
  const buffer = frame.createFrame(opcodes.BINARY)
329
272
 
@@ -344,7 +287,7 @@ class WebSocket extends EventTarget {
344
287
  // not throw an exception must increase the bufferedAmount attribute
345
288
  // by the length of data’s buffer in bytes.
346
289
 
347
- const ab = Buffer.from(data, data.byteOffset, data.byteLength)
290
+ const ab = new FastBuffer(data, data.byteOffset, data.byteLength)
348
291
 
349
292
  const frame = new WebsocketFrameSend(ab)
350
293
  const buffer = frame.createFrame(opcodes.BINARY)
@@ -368,7 +311,7 @@ class WebSocket extends EventTarget {
368
311
  const frame = new WebsocketFrameSend()
369
312
 
370
313
  data.arrayBuffer().then((ab) => {
371
- const value = Buffer.from(ab)
314
+ const value = new FastBuffer(ab)
372
315
  frame.frameData = value
373
316
  const buffer = frame.createFrame(opcodes.BINARY)
374
317
 
@@ -521,9 +464,8 @@ class WebSocket extends EventTarget {
521
464
  this[kResponse] = response
522
465
 
523
466
  const parser = new ByteParser(this)
524
- parser.on('drain', function onParserDrain () {
525
- this.ws[kResponse].socket.resume()
526
- })
467
+ parser.on('drain', onParserDrain)
468
+ parser.on('error', onParserError.bind(this))
527
469
 
528
470
  response.socket.ws = this
529
471
  this[kByteParser] = parser
@@ -607,7 +549,7 @@ webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, arg
607
549
  return webidl.converters.DOMString(V, prefix, argument)
608
550
  }
609
551
 
610
- // This implements the propsal made in https://github.com/whatwg/websockets/issues/42
552
+ // This implements the proposal made in https://github.com/whatwg/websockets/issues/42
611
553
  webidl.converters.WebSocketInit = webidl.dictionaryConverter([
612
554
  {
613
555
  key: 'protocols',
@@ -647,6 +589,16 @@ webidl.converters.WebSocketSendData = function (V) {
647
589
  return webidl.converters.USVString(V)
648
590
  }
649
591
 
592
+ function onParserDrain () {
593
+ this.ws[kResponse].socket.resume()
594
+ }
595
+
596
+ function onParserError (err) {
597
+ fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
598
+
599
+ closeWebSocketConnection(this, err.code)
600
+ }
601
+
650
602
  module.exports = {
651
603
  WebSocket
652
604
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.15.0",
3
+ "version": "6.16.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": {
@@ -77,7 +77,7 @@
77
77
  "test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"",
78
78
  "test:fuzzing": "node test/fuzzing/fuzzing.test.js",
79
79
  "test:fetch": "npm run build:node && npm run test:fetch:nobuild",
80
- "test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
80
+ "test:fetch:nobuild": "borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
81
81
  "test:interceptors": "borp -p \"test/interceptors/*.js\"",
82
82
  "test:jest": "cross-env NODE_V8_COVERAGE= jest",
83
83
  "test:unit": "borp --expose-gc -p \"test/*.js\"",
@@ -105,7 +105,7 @@
105
105
  "@sinonjs/fake-timers": "^11.1.0",
106
106
  "@types/node": "^18.0.3",
107
107
  "abort-controller": "^3.0.0",
108
- "borp": "^0.12.0",
108
+ "borp": "^0.13.0",
109
109
  "c8": "^9.1.0",
110
110
  "cross-env": "^7.0.3",
111
111
  "dns-packet": "^5.4.0",
package/types/fetch.d.ts CHANGED
@@ -85,7 +85,7 @@ export declare class Headers implements SpecIterable<[string, string]> {
85
85
  readonly keys: () => SpecIterableIterator<string>
86
86
  readonly values: () => SpecIterableIterator<string>
87
87
  readonly entries: () => SpecIterableIterator<[string, string]>
88
- readonly [Symbol.iterator]: () => SpecIterator<[string, string]>
88
+ readonly [Symbol.iterator]: () => SpecIterableIterator<[string, string]>
89
89
  }
90
90
 
91
91
  export type RequestCache =
@@ -163,6 +163,7 @@ export declare class Request extends BodyMixin {
163
163
  readonly method: string
164
164
  readonly mode: RequestMode
165
165
  readonly redirect: RequestRedirect
166
+ readonly referrer: string
166
167
  readonly referrerPolicy: ReferrerPolicy
167
168
  readonly url: string
168
169
 
@@ -71,11 +71,11 @@ declare namespace MockInterceptor {
71
71
 
72
72
  export interface MockResponseCallbackOptions {
73
73
  path: string;
74
- origin: string;
75
74
  method: string;
76
- body?: BodyInit | Dispatcher.DispatchOptions['body'];
77
- headers: Headers | Record<string, string>;
78
- maxRedirections: number;
75
+ headers?: Headers | Record<string, string>;
76
+ origin?: string;
77
+ body?: BodyInit | Dispatcher.DispatchOptions['body'] | null;
78
+ maxRedirections?: number;
79
79
  }
80
80
 
81
81
  export type MockResponseDataHandler<TData extends object = object> = (
package/types/util.d.ts CHANGED
@@ -8,24 +8,11 @@ export namespace util {
8
8
  /**
9
9
  * Receives a header object and returns the parsed value.
10
10
  * @param headers Header object
11
+ * @param obj Object to specify a proxy object. Used to assign parsed values.
12
+ * @returns If `obj` is specified, it is equivalent to `obj`.
11
13
  */
12
14
  export function parseHeaders(
13
- headers:
14
- | Record<string, string | string[]>
15
- | (Buffer | string | (Buffer | string)[])[]
16
- ): Record<string, string | string[]>;
17
- /**
18
- * Receives a header object and returns the parsed value.
19
- * @param headers Header object
20
- * @param obj Object to specify a proxy object. Used to assign parsed values. But, if `headers` is an object, it is not used.
21
- * @returns If `headers` is an object, it is `headers`. Otherwise, if `obj` is specified, it is equivalent to `obj`.
22
- */
23
- export function parseHeaders<
24
- H extends
25
- | Record<string, string | string[]>
26
- | (Buffer | string | (Buffer | string)[])[]
27
- >(
28
- headers: H,
29
- obj?: H extends any[] ? Record<string, string | string[]> : never
15
+ headers: (Buffer | string | (Buffer | string)[])[],
16
+ obj?: Record<string, string | string[]>
30
17
  ): Record<string, string | string[]>;
31
18
  }