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.
- package/docs/docs/api/Util.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/lib/api/api-request.js +49 -17
- package/lib/api/readable.js +6 -2
- package/lib/core/connect.js +1 -1
- package/lib/dispatcher/client-h2.js +2 -0
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/headers.js +5 -1
- package/lib/web/fetch/index.js +36 -25
- package/lib/web/fetch/request.js +41 -43
- package/lib/web/fetch/response.js +19 -0
- package/lib/web/fetch/util.js +20 -12
- package/lib/web/fetch/webidl.js +2 -1
- package/lib/web/websocket/connection.js +77 -7
- package/lib/web/websocket/frame.js +30 -8
- package/lib/web/websocket/receiver.js +22 -8
- package/lib/web/websocket/util.js +10 -2
- package/lib/web/websocket/websocket.js +26 -74
- package/package.json +3 -3
- package/types/fetch.d.ts +2 -1
- package/types/mock-interceptor.d.ts +4 -4
- package/types/util.d.ts +4 -17
package/docs/docs/api/Util.md
CHANGED
|
@@ -8,11 +8,11 @@ Receives a header object and returns the parsed value.
|
|
|
8
8
|
|
|
9
9
|
Arguments:
|
|
10
10
|
|
|
11
|
-
- **headers** `
|
|
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 **
|
|
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 [
|
|
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
|
package/lib/api/api-request.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
package/lib/api/readable.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/core/connect.js
CHANGED
|
@@ -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
|
|
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 = /^[!#$%&'
|
|
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 =
|
|
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 */
|
package/lib/web/fetch/headers.js
CHANGED
|
@@ -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
|
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -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 {
|
|
481
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
|
package/lib/web/fetch/util.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
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
|
|
package/lib/web/fetch/webidl.js
CHANGED
|
@@ -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[
|
|
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
|
|
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] =
|
|
47
|
-
buffer[offset - 3] =
|
|
48
|
-
buffer[offset - 2] =
|
|
49
|
-
buffer[offset - 1] =
|
|
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] =
|
|
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
|
|
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
|
|
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 =
|
|
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 {
|
|
6
|
-
const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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',
|
|
525
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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]: () =>
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
}
|