undici 6.15.0 → 6.16.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.
@@ -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) {
@@ -56,6 +55,9 @@ class RequestHandler extends AsyncResource {
56
55
  this.onInfo = onInfo || null
57
56
  this.throwOnError = throwOnError
58
57
  this.highWaterMark = highWaterMark
58
+ this.signal = signal
59
+ this.reason = null
60
+ this.removeAbortListener = null
59
61
 
60
62
  if (util.isStream(body)) {
61
63
  body.on('error', (err) => {
@@ -63,7 +65,26 @@ class RequestHandler extends AsyncResource {
63
65
  })
64
66
  }
65
67
 
66
- addSignal(this, signal)
68
+ if (this.signal) {
69
+ if (this.signal.aborted) {
70
+ this.reason = this.signal.reason ?? new RequestAbortedError()
71
+ } else {
72
+ this.removeAbortListener = util.addAbortListener(this.signal, () => {
73
+ this.reason = this.signal.reason ?? new RequestAbortedError()
74
+ if (this.res) {
75
+ util.destroy(this.res, this.reason)
76
+ } else if (this.abort) {
77
+ this.abort(this.reason)
78
+ }
79
+
80
+ if (this.removeAbortListener) {
81
+ this.res?.off('close', this.removeAbortListener)
82
+ this.removeAbortListener()
83
+ this.removeAbortListener = null
84
+ }
85
+ })
86
+ }
87
+ }
67
88
  }
68
89
 
69
90
  onConnect (abort, context) {
@@ -93,14 +114,18 @@ class RequestHandler extends AsyncResource {
93
114
  const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
94
115
  const contentType = parsedHeaders['content-type']
95
116
  const contentLength = parsedHeaders['content-length']
96
- const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
117
+ const res = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
118
+
119
+ if (this.removeAbortListener) {
120
+ res.on('close', this.removeAbortListener)
121
+ }
97
122
 
98
123
  this.callback = null
99
- this.res = body
124
+ this.res = res
100
125
  if (callback !== null) {
101
126
  if (this.throwOnError && statusCode >= 400) {
102
127
  this.runInAsyncScope(getResolveErrorBodyCallback, null,
103
- { callback, body, contentType, statusCode, statusMessage, headers }
128
+ { callback, body: res, contentType, statusCode, statusMessage, headers }
104
129
  )
105
130
  } else {
106
131
  this.runInAsyncScope(callback, null, null, {
@@ -108,7 +133,7 @@ class RequestHandler extends AsyncResource {
108
133
  headers,
109
134
  trailers: this.trailers,
110
135
  opaque,
111
- body,
136
+ body: res,
112
137
  context
113
138
  })
114
139
  }
@@ -116,25 +141,17 @@ class RequestHandler extends AsyncResource {
116
141
  }
117
142
 
118
143
  onData (chunk) {
119
- const { res } = this
120
- return res.push(chunk)
144
+ return this.res.push(chunk)
121
145
  }
122
146
 
123
147
  onComplete (trailers) {
124
- const { res } = this
125
-
126
- removeSignal(this)
127
-
128
148
  util.parseHeaders(trailers, this.trailers)
129
-
130
- res.push(null)
149
+ this.res.push(null)
131
150
  }
132
151
 
133
152
  onError (err) {
134
153
  const { res, callback, body, opaque } = this
135
154
 
136
- removeSignal(this)
137
-
138
155
  if (callback) {
139
156
  // TODO: Does this need queueMicrotask?
140
157
  this.callback = null
@@ -155,6 +172,12 @@ class RequestHandler extends AsyncResource {
155
172
  this.body = null
156
173
  util.destroy(body, err)
157
174
  }
175
+
176
+ if (this.removeAbortListener) {
177
+ res?.off('close', this.removeAbortListener)
178
+ this.removeAbortListener()
179
+ this.removeAbortListener = null
180
+ }
158
181
  }
159
182
  }
160
183
 
@@ -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) {
@@ -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.
@@ -1066,7 +1075,10 @@ function fetchFinale (fetchParams, response) {
1066
1075
  // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
1067
1076
  // process response given response, with fetchParams’s task destination.
1068
1077
  if (fetchParams.processResponse != null) {
1069
- queueMicrotask(() => fetchParams.processResponse(response))
1078
+ queueMicrotask(() => {
1079
+ fetchParams.processResponse(response)
1080
+ fetchParams.processResponse = null
1081
+ })
1070
1082
  }
1071
1083
 
1072
1084
  // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
@@ -1884,7 +1896,11 @@ async function httpNetworkFetch (
1884
1896
  // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
1885
1897
  // controller with reason, given reason.
1886
1898
  const cancelAlgorithm = (reason) => {
1887
- fetchParams.controller.abort(reason)
1899
+ // If the aborted fetch was already terminated, then we do not
1900
+ // need to do anything.
1901
+ if (!isCancelled(fetchParams)) {
1902
+ fetchParams.controller.abort(reason)
1903
+ }
1888
1904
  }
1889
1905
 
1890
1906
  // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
@@ -2102,20 +2118,16 @@ async function httpNetworkFetch (
2102
2118
 
2103
2119
  const headersList = new HeadersList()
2104
2120
 
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)
2121
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2122
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2123
+ }
2124
+ const contentEncoding = headersList.get('content-encoding', true)
2125
+ if (contentEncoding) {
2126
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2127
+ // "All content-coding values are case-insensitive..."
2128
+ codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2118
2129
  }
2130
+ location = headersList.get('location', true)
2119
2131
 
2120
2132
  this.body = new Readable({ read: resume })
2121
2133
 
@@ -2125,7 +2137,7 @@ async function httpNetworkFetch (
2125
2137
  redirectStatusSet.has(status)
2126
2138
 
2127
2139
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2128
- if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2140
+ if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2129
2141
  for (let i = 0; i < codings.length; ++i) {
2130
2142
  const coding = codings[i]
2131
2143
  // 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
 
@@ -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
@@ -211,6 +213,72 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options)
211
213
  return controller
212
214
  }
213
215
 
216
+ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
217
+ if (isClosing(ws) || isClosed(ws)) {
218
+ // If this's ready state is CLOSING (2) or CLOSED (3)
219
+ // Do nothing.
220
+ } else if (!isEstablished(ws)) {
221
+ // If the WebSocket connection is not yet established
222
+ // Fail the WebSocket connection and set this's ready state
223
+ // to CLOSING (2).
224
+ failWebsocketConnection(ws, 'Connection was closed before it was established.')
225
+ ws[kReadyState] = states.CLOSING
226
+ } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) {
227
+ // If the WebSocket closing handshake has not yet been started
228
+ // Start the WebSocket closing handshake and set this's ready
229
+ // state to CLOSING (2).
230
+ // - If neither code nor reason is present, the WebSocket Close
231
+ // message must not have a body.
232
+ // - If code is present, then the status code to use in the
233
+ // WebSocket Close message must be the integer given by code.
234
+ // - If reason is also present, then reasonBytes must be
235
+ // provided in the Close message after the status code.
236
+
237
+ ws[kSentClose] = sentCloseFrameState.PROCESSING
238
+
239
+ const frame = new WebsocketFrameSend()
240
+
241
+ // If neither code nor reason is present, the WebSocket Close
242
+ // message must not have a body.
243
+
244
+ // If code is present, then the status code to use in the
245
+ // WebSocket Close message must be the integer given by code.
246
+ if (code !== undefined && reason === undefined) {
247
+ frame.frameData = Buffer.allocUnsafe(2)
248
+ frame.frameData.writeUInt16BE(code, 0)
249
+ } else if (code !== undefined && reason !== undefined) {
250
+ // If reason is also present, then reasonBytes must be
251
+ // provided in the Close message after the status code.
252
+ frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
253
+ frame.frameData.writeUInt16BE(code, 0)
254
+ // the body MAY contain UTF-8-encoded data with value /reason/
255
+ frame.frameData.write(reason, 2, 'utf-8')
256
+ } else {
257
+ frame.frameData = emptyBuffer
258
+ }
259
+
260
+ /** @type {import('stream').Duplex} */
261
+ const socket = ws[kResponse].socket
262
+
263
+ socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
264
+ if (!err) {
265
+ ws[kSentClose] = sentCloseFrameState.SENT
266
+ }
267
+ })
268
+
269
+ ws[kSentClose] = sentCloseFrameState.PROCESSING
270
+
271
+ // Upon either sending or receiving a Close control frame, it is said
272
+ // that _The WebSocket Closing Handshake is Started_ and that the
273
+ // WebSocket connection is in the CLOSING state.
274
+ ws[kReadyState] = states.CLOSING
275
+ } else {
276
+ // Otherwise
277
+ // Set this's ready state to CLOSING (2).
278
+ ws[kReadyState] = states.CLOSING
279
+ }
280
+ }
281
+
214
282
  /**
215
283
  * @param {Buffer} chunk
216
284
  */
@@ -237,10 +305,10 @@ function onSocketClose () {
237
305
 
238
306
  const result = ws[kByteParser].closingInfo
239
307
 
240
- if (result) {
308
+ if (result && !result.error) {
241
309
  code = result.code ?? 1005
242
310
  reason = result.reason
243
- } else if (ws[kSentClose] !== sentCloseFrameState.SENT) {
311
+ } else if (!ws[kReceivedClose]) {
244
312
  // If _The WebSocket
245
313
  // Connection is Closed_ and no Close control frame was received by the
246
314
  // endpoint (such as could occur if the underlying transport connection
@@ -293,5 +361,6 @@ function onSocketError (error) {
293
361
  }
294
362
 
295
363
  module.exports = {
296
- establishWebSocketConnection
364
+ establishWebSocketConnection,
365
+ closeWebSocketConnection
297
366
  }
@@ -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
@@ -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
  }
@@ -310,16 +324,16 @@ class ByteParser extends Writable {
310
324
  }
311
325
 
312
326
  if (code !== undefined && !isValidStatusCode(code)) {
313
- return null
327
+ return { code: 1002, reason: 'Invalid status code', error: true }
314
328
  }
315
329
 
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
@@ -197,67 +198,7 @@ class WebSocket extends EventTarget {
197
198
  }
198
199
 
199
200
  // 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
- }
201
+ closeWebSocketConnection(this, code, reason, reasonByteLength)
261
202
  }
262
203
 
263
204
  /**
@@ -323,7 +264,7 @@ class WebSocket extends EventTarget {
323
264
  // increase the bufferedAmount attribute by the length of the
324
265
  // ArrayBuffer in bytes.
325
266
 
326
- const value = Buffer.from(data)
267
+ const value = new FastBuffer(data)
327
268
  const frame = new WebsocketFrameSend(value)
328
269
  const buffer = frame.createFrame(opcodes.BINARY)
329
270
 
@@ -344,7 +285,7 @@ class WebSocket extends EventTarget {
344
285
  // not throw an exception must increase the bufferedAmount attribute
345
286
  // by the length of data’s buffer in bytes.
346
287
 
347
- const ab = Buffer.from(data, data.byteOffset, data.byteLength)
288
+ const ab = new FastBuffer(data, data.byteOffset, data.byteLength)
348
289
 
349
290
  const frame = new WebsocketFrameSend(ab)
350
291
  const buffer = frame.createFrame(opcodes.BINARY)
@@ -368,7 +309,7 @@ class WebSocket extends EventTarget {
368
309
  const frame = new WebsocketFrameSend()
369
310
 
370
311
  data.arrayBuffer().then((ab) => {
371
- const value = Buffer.from(ab)
312
+ const value = new FastBuffer(ab)
372
313
  frame.frameData = value
373
314
  const buffer = frame.createFrame(opcodes.BINARY)
374
315
 
@@ -521,9 +462,8 @@ class WebSocket extends EventTarget {
521
462
  this[kResponse] = response
522
463
 
523
464
  const parser = new ByteParser(this)
524
- parser.on('drain', function onParserDrain () {
525
- this.ws[kResponse].socket.resume()
526
- })
465
+ parser.on('drain', onParserDrain)
466
+ parser.on('error', onParserError.bind(this))
527
467
 
528
468
  response.socket.ws = this
529
469
  this[kByteParser] = parser
@@ -647,6 +587,16 @@ webidl.converters.WebSocketSendData = function (V) {
647
587
  return webidl.converters.USVString(V)
648
588
  }
649
589
 
590
+ function onParserDrain () {
591
+ this.ws[kResponse].socket.resume()
592
+ }
593
+
594
+ function onParserError (err) {
595
+ fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
596
+
597
+ closeWebSocketConnection(this, err.code)
598
+ }
599
+
650
600
  module.exports = {
651
601
  WebSocket
652
602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.15.0",
3
+ "version": "6.16.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": {
@@ -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
  }