undici 6.16.1 → 6.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Dispatcher.md +32 -0
- package/index-fetch.js +1 -1
- package/index.js +3 -2
- package/lib/core/symbols.js +0 -1
- package/lib/dispatcher/client-h1.js +1 -1
- package/lib/interceptor/dump.js +123 -0
- package/lib/web/cookies/util.js +5 -3
- package/lib/web/fetch/body.js +9 -0
- package/lib/web/fetch/headers.js +59 -31
- package/lib/web/fetch/request.js +11 -11
- package/lib/web/fetch/response.js +8 -8
- package/lib/web/fetch/symbols.js +0 -1
- package/lib/web/websocket/connection.js +4 -9
- package/lib/web/websocket/receiver.js +172 -141
- package/lib/web/websocket/util.js +29 -1
- package/lib/web/websocket/websocket.js +14 -4
- package/package.json +4 -2
- package/types/dispatcher.d.ts +3 -3
- package/types/index.d.ts +5 -0
- package/types/interceptors.d.ts +8 -2
|
@@ -952,6 +952,38 @@ const client = new Client("http://example.com").compose(
|
|
|
952
952
|
);
|
|
953
953
|
```
|
|
954
954
|
|
|
955
|
+
##### `dump`
|
|
956
|
+
|
|
957
|
+
The `dump` interceptor enables you to dump the response body from a request upon a given limit.
|
|
958
|
+
|
|
959
|
+
**Options**
|
|
960
|
+
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: `1048576`.
|
|
961
|
+
|
|
962
|
+
> The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis.
|
|
963
|
+
|
|
964
|
+
**Example - Basic Dump Interceptor**
|
|
965
|
+
|
|
966
|
+
```js
|
|
967
|
+
const { Client, interceptors } = require("undici");
|
|
968
|
+
const { dump } = interceptors;
|
|
969
|
+
|
|
970
|
+
const client = new Client("http://example.com").compose(
|
|
971
|
+
dump({
|
|
972
|
+
maxSize: 1024,
|
|
973
|
+
})
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
// or
|
|
977
|
+
client.dispatch(
|
|
978
|
+
{
|
|
979
|
+
path: "/",
|
|
980
|
+
method: "GET",
|
|
981
|
+
dumpMaxSize: 1024,
|
|
982
|
+
},
|
|
983
|
+
handler
|
|
984
|
+
);
|
|
985
|
+
```
|
|
986
|
+
|
|
955
987
|
## Instance Events
|
|
956
988
|
|
|
957
989
|
### Event: `'connect'`
|
package/index-fetch.js
CHANGED
|
@@ -7,7 +7,7 @@ const fetchImpl = require('./lib/web/fetch').fetch
|
|
|
7
7
|
module.exports.fetch = function fetch (resource, init = undefined) {
|
|
8
8
|
return fetchImpl(resource, init).catch((err) => {
|
|
9
9
|
if (err && typeof err === 'object') {
|
|
10
|
-
Error.captureStackTrace(err
|
|
10
|
+
Error.captureStackTrace(err)
|
|
11
11
|
}
|
|
12
12
|
throw err
|
|
13
13
|
})
|
package/index.js
CHANGED
|
@@ -40,7 +40,8 @@ module.exports.RedirectHandler = RedirectHandler
|
|
|
40
40
|
module.exports.createRedirectInterceptor = createRedirectInterceptor
|
|
41
41
|
module.exports.interceptors = {
|
|
42
42
|
redirect: require('./lib/interceptor/redirect'),
|
|
43
|
-
retry: require('./lib/interceptor/retry')
|
|
43
|
+
retry: require('./lib/interceptor/retry'),
|
|
44
|
+
dump: require('./lib/interceptor/dump')
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
module.exports.buildConnector = buildConnector
|
|
@@ -108,7 +109,7 @@ module.exports.fetch = async function fetch (init, options = undefined) {
|
|
|
108
109
|
return await fetchImpl(init, options)
|
|
109
110
|
} catch (err) {
|
|
110
111
|
if (err && typeof err === 'object') {
|
|
111
|
-
Error.captureStackTrace(err
|
|
112
|
+
Error.captureStackTrace(err)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
throw err
|
package/lib/core/symbols.js
CHANGED
|
@@ -8,7 +8,6 @@ module.exports = {
|
|
|
8
8
|
kQueue: Symbol('queue'),
|
|
9
9
|
kConnect: Symbol('connect'),
|
|
10
10
|
kConnecting: Symbol('connecting'),
|
|
11
|
-
kHeadersList: Symbol('headers list'),
|
|
12
11
|
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
|
|
13
12
|
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
|
|
14
13
|
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
|
|
@@ -1101,7 +1101,7 @@ function writeStream ({ abort, body, client, request, socket, contentLength, hea
|
|
|
1101
1101
|
}
|
|
1102
1102
|
}
|
|
1103
1103
|
|
|
1104
|
-
|
|
1104
|
+
function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1105
1105
|
try {
|
|
1106
1106
|
if (!body) {
|
|
1107
1107
|
if (contentLength === 0) {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const util = require('../core/util')
|
|
4
|
+
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
|
|
5
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
6
|
+
|
|
7
|
+
class DumpHandler extends DecoratorHandler {
|
|
8
|
+
#maxSize = 1024 * 1024
|
|
9
|
+
#abort = null
|
|
10
|
+
#dumped = false
|
|
11
|
+
#aborted = false
|
|
12
|
+
#size = 0
|
|
13
|
+
#reason = null
|
|
14
|
+
#handler = null
|
|
15
|
+
|
|
16
|
+
constructor ({ maxSize }, handler) {
|
|
17
|
+
super(handler)
|
|
18
|
+
|
|
19
|
+
if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) {
|
|
20
|
+
throw new InvalidArgumentError('maxSize must be a number greater than 0')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.#maxSize = maxSize ?? this.#maxSize
|
|
24
|
+
this.#handler = handler
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onConnect (abort) {
|
|
28
|
+
this.#abort = abort
|
|
29
|
+
|
|
30
|
+
this.#handler.onConnect(this.#customAbort.bind(this))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#customAbort (reason) {
|
|
34
|
+
this.#aborted = true
|
|
35
|
+
this.#reason = reason
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// TODO: will require adjustment after new hooks are out
|
|
39
|
+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
|
40
|
+
const headers = util.parseHeaders(rawHeaders)
|
|
41
|
+
const contentLength = headers['content-length']
|
|
42
|
+
|
|
43
|
+
if (contentLength != null && contentLength > this.#maxSize) {
|
|
44
|
+
throw new RequestAbortedError(
|
|
45
|
+
`Response size (${contentLength}) larger than maxSize (${
|
|
46
|
+
this.#maxSize
|
|
47
|
+
})`
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.#aborted) {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.#handler.onHeaders(
|
|
56
|
+
statusCode,
|
|
57
|
+
rawHeaders,
|
|
58
|
+
resume,
|
|
59
|
+
statusMessage
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onError (err) {
|
|
64
|
+
if (this.#dumped) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
err = this.#reason ?? err
|
|
69
|
+
|
|
70
|
+
this.#handler.onError(err)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onData (chunk) {
|
|
74
|
+
this.#size = this.#size + chunk.length
|
|
75
|
+
|
|
76
|
+
if (this.#size >= this.#maxSize) {
|
|
77
|
+
this.#dumped = true
|
|
78
|
+
|
|
79
|
+
if (this.#aborted) {
|
|
80
|
+
this.#handler.onError(this.#reason)
|
|
81
|
+
} else {
|
|
82
|
+
this.#handler.onComplete([])
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onComplete (trailers) {
|
|
90
|
+
if (this.#dumped) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (this.#aborted) {
|
|
95
|
+
this.#handler.onError(this.reason)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.#handler.onComplete(trailers)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createDumpInterceptor (
|
|
104
|
+
{ maxSize: defaultMaxSize } = {
|
|
105
|
+
maxSize: 1024 * 1024
|
|
106
|
+
}
|
|
107
|
+
) {
|
|
108
|
+
return dispatch => {
|
|
109
|
+
return function Intercept (opts, handler) {
|
|
110
|
+
const { dumpMaxSize = defaultMaxSize } =
|
|
111
|
+
opts
|
|
112
|
+
|
|
113
|
+
const dumpHandler = new DumpHandler(
|
|
114
|
+
{ maxSize: dumpMaxSize },
|
|
115
|
+
handler
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return dispatch(opts, dumpHandler)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = createDumpInterceptor
|
package/lib/web/cookies/util.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const {
|
|
4
|
+
const { getHeadersList: internalGetHeadersList } = require('../fetch/headers')
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {string} value
|
|
@@ -278,8 +278,10 @@ function stringify (cookie) {
|
|
|
278
278
|
let kHeadersListNode
|
|
279
279
|
|
|
280
280
|
function getHeadersList (headers) {
|
|
281
|
-
|
|
282
|
-
return headers
|
|
281
|
+
try {
|
|
282
|
+
return internalGetHeadersList(headers)
|
|
283
|
+
} catch {
|
|
284
|
+
// fall-through
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
if (!kHeadersListNode) {
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -385,6 +385,15 @@ function bodyMixinMethods (instance) {
|
|
|
385
385
|
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
|
|
386
386
|
)
|
|
387
387
|
}, instance, false)
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
bytes () {
|
|
391
|
+
// The bytes() method steps are to return the result of running consume body
|
|
392
|
+
// with this and the following step given a byte sequence bytes: return the
|
|
393
|
+
// result of creating a Uint8Array from bytes in this’s relevant realm.
|
|
394
|
+
return consumeBody(this, (bytes) => {
|
|
395
|
+
return new Uint8Array(bytes.buffer, 0, bytes.byteLength)
|
|
396
|
+
}, instance, true)
|
|
388
397
|
}
|
|
389
398
|
}
|
|
390
399
|
|
package/lib/web/fetch/headers.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
const { kGuard } = require('./symbols')
|
|
5
|
+
const { kConstruct } = require('../../core/symbols')
|
|
7
6
|
const { kEnumerableProperty } = require('../../core/util')
|
|
8
7
|
const {
|
|
9
8
|
iteratorMixin,
|
|
@@ -103,19 +102,18 @@ function appendHeader (headers, name, value) {
|
|
|
103
102
|
// 3. If headers’s guard is "immutable", then throw a TypeError.
|
|
104
103
|
// 4. Otherwise, if headers’s guard is "request" and name is a
|
|
105
104
|
// forbidden header name, return.
|
|
105
|
+
// 5. Otherwise, if headers’s guard is "request-no-cors":
|
|
106
|
+
// TODO
|
|
106
107
|
// Note: undici does not implement forbidden header names
|
|
107
|
-
if (headers
|
|
108
|
+
if (getHeadersGuard(headers) === 'immutable') {
|
|
108
109
|
throw new TypeError('immutable')
|
|
109
|
-
} else if (headers[kGuard] === 'request-no-cors') {
|
|
110
|
-
// 5. Otherwise, if headers’s guard is "request-no-cors":
|
|
111
|
-
// TODO
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
// 6. Otherwise, if headers’s guard is "response" and name is a
|
|
115
113
|
// forbidden response-header name, return.
|
|
116
114
|
|
|
117
115
|
// 7. Append (name, value) to headers’s header list.
|
|
118
|
-
return headers
|
|
116
|
+
return getHeadersList(headers).append(name, value, false)
|
|
119
117
|
|
|
120
118
|
// 8. If headers’s guard is "request-no-cors", then remove
|
|
121
119
|
// privileged no-CORS request headers from headers
|
|
@@ -357,16 +355,20 @@ class HeadersList {
|
|
|
357
355
|
|
|
358
356
|
// https://fetch.spec.whatwg.org/#headers-class
|
|
359
357
|
class Headers {
|
|
358
|
+
#guard
|
|
359
|
+
#headersList
|
|
360
|
+
|
|
360
361
|
constructor (init = undefined) {
|
|
361
362
|
if (init === kConstruct) {
|
|
362
363
|
return
|
|
363
364
|
}
|
|
364
|
-
|
|
365
|
+
|
|
366
|
+
this.#headersList = new HeadersList()
|
|
365
367
|
|
|
366
368
|
// The new Headers(init) constructor steps are:
|
|
367
369
|
|
|
368
370
|
// 1. Set this’s guard to "none".
|
|
369
|
-
this
|
|
371
|
+
this.#guard = 'none'
|
|
370
372
|
|
|
371
373
|
// 2. If init is given, then fill this with init.
|
|
372
374
|
if (init !== undefined) {
|
|
@@ -416,22 +418,20 @@ class Headers {
|
|
|
416
418
|
// 5. Otherwise, if this’s guard is "response" and name is
|
|
417
419
|
// a forbidden response-header name, return.
|
|
418
420
|
// Note: undici does not implement forbidden header names
|
|
419
|
-
if (this
|
|
421
|
+
if (this.#guard === 'immutable') {
|
|
420
422
|
throw new TypeError('immutable')
|
|
421
|
-
} else if (this[kGuard] === 'request-no-cors') {
|
|
422
|
-
// TODO
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
// 6. If this’s header list does not contain name, then
|
|
426
426
|
// return.
|
|
427
|
-
if (!this
|
|
427
|
+
if (!this.#headersList.contains(name, false)) {
|
|
428
428
|
return
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
// 7. Delete name from this’s header list.
|
|
432
432
|
// 8. If this’s guard is "request-no-cors", then remove
|
|
433
433
|
// privileged no-CORS request headers from this.
|
|
434
|
-
this
|
|
434
|
+
this.#headersList.delete(name, false)
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
// https://fetch.spec.whatwg.org/#dom-headers-get
|
|
@@ -454,7 +454,7 @@ class Headers {
|
|
|
454
454
|
|
|
455
455
|
// 2. Return the result of getting name from this’s header
|
|
456
456
|
// list.
|
|
457
|
-
return this
|
|
457
|
+
return this.#headersList.get(name, false)
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
// https://fetch.spec.whatwg.org/#dom-headers-has
|
|
@@ -477,7 +477,7 @@ class Headers {
|
|
|
477
477
|
|
|
478
478
|
// 2. Return true if this’s header list contains name;
|
|
479
479
|
// otherwise false.
|
|
480
|
-
return this
|
|
480
|
+
return this.#headersList.contains(name, false)
|
|
481
481
|
}
|
|
482
482
|
|
|
483
483
|
// https://fetch.spec.whatwg.org/#dom-headers-set
|
|
@@ -518,16 +518,14 @@ class Headers {
|
|
|
518
518
|
// 6. Otherwise, if this’s guard is "response" and name is a
|
|
519
519
|
// forbidden response-header name, return.
|
|
520
520
|
// Note: undici does not implement forbidden header names
|
|
521
|
-
if (this
|
|
521
|
+
if (this.#guard === 'immutable') {
|
|
522
522
|
throw new TypeError('immutable')
|
|
523
|
-
} else if (this[kGuard] === 'request-no-cors') {
|
|
524
|
-
// TODO
|
|
525
523
|
}
|
|
526
524
|
|
|
527
525
|
// 7. Set (name, value) in this’s header list.
|
|
528
526
|
// 8. If this’s guard is "request-no-cors", then remove
|
|
529
527
|
// privileged no-CORS request headers from this
|
|
530
|
-
this
|
|
528
|
+
this.#headersList.set(name, value, false)
|
|
531
529
|
}
|
|
532
530
|
|
|
533
531
|
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
|
|
@@ -538,7 +536,7 @@ class Headers {
|
|
|
538
536
|
// 2. Return the values of all headers in this’s header list whose name is
|
|
539
537
|
// a byte-case-insensitive match for `Set-Cookie`, in order.
|
|
540
538
|
|
|
541
|
-
const list = this
|
|
539
|
+
const list = this.#headersList.cookies
|
|
542
540
|
|
|
543
541
|
if (list) {
|
|
544
542
|
return [...list]
|
|
@@ -549,8 +547,8 @@ class Headers {
|
|
|
549
547
|
|
|
550
548
|
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
|
|
551
549
|
get [kHeadersSortedMap] () {
|
|
552
|
-
if (this[
|
|
553
|
-
return this[
|
|
550
|
+
if (this.#headersList[kHeadersSortedMap]) {
|
|
551
|
+
return this.#headersList[kHeadersSortedMap]
|
|
554
552
|
}
|
|
555
553
|
|
|
556
554
|
// 1. Let headers be an empty list of headers with the key being the name
|
|
@@ -559,14 +557,14 @@ class Headers {
|
|
|
559
557
|
|
|
560
558
|
// 2. Let names be the result of convert header names to a sorted-lowercase
|
|
561
559
|
// set with all the names of the headers in list.
|
|
562
|
-
const names = this
|
|
560
|
+
const names = this.#headersList.toSortedArray()
|
|
563
561
|
|
|
564
|
-
const cookies = this
|
|
562
|
+
const cookies = this.#headersList.cookies
|
|
565
563
|
|
|
566
564
|
// fast-path
|
|
567
565
|
if (cookies === null || cookies.length === 1) {
|
|
568
566
|
// Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
|
|
569
|
-
return (this[
|
|
567
|
+
return (this.#headersList[kHeadersSortedMap] = names)
|
|
570
568
|
}
|
|
571
569
|
|
|
572
570
|
// 3. For each name of names:
|
|
@@ -596,16 +594,38 @@ class Headers {
|
|
|
596
594
|
}
|
|
597
595
|
|
|
598
596
|
// 4. Return headers.
|
|
599
|
-
return (this[
|
|
597
|
+
return (this.#headersList[kHeadersSortedMap] = headers)
|
|
600
598
|
}
|
|
601
599
|
|
|
602
600
|
[util.inspect.custom] (depth, options) {
|
|
603
601
|
options.depth ??= depth
|
|
604
602
|
|
|
605
|
-
return `Headers ${util.formatWithOptions(options, this
|
|
603
|
+
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
static getHeadersGuard (o) {
|
|
607
|
+
return o.#guard
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
static setHeadersGuard (o, guard) {
|
|
611
|
+
o.#guard = guard
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
static getHeadersList (o) {
|
|
615
|
+
return o.#headersList
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
static setHeadersList (o, list) {
|
|
619
|
+
o.#headersList = list
|
|
606
620
|
}
|
|
607
621
|
}
|
|
608
622
|
|
|
623
|
+
const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
|
|
624
|
+
Reflect.deleteProperty(Headers, 'getHeadersGuard')
|
|
625
|
+
Reflect.deleteProperty(Headers, 'setHeadersGuard')
|
|
626
|
+
Reflect.deleteProperty(Headers, 'getHeadersList')
|
|
627
|
+
Reflect.deleteProperty(Headers, 'setHeadersList')
|
|
628
|
+
|
|
609
629
|
Object.defineProperty(Headers.prototype, util.inspect.custom, {
|
|
610
630
|
enumerable: false
|
|
611
631
|
})
|
|
@@ -631,8 +651,12 @@ webidl.converters.HeadersInit = function (V, prefix, argument) {
|
|
|
631
651
|
|
|
632
652
|
// A work-around to ensure we send the properly-cased Headers when V is a Headers object.
|
|
633
653
|
// Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
|
|
634
|
-
if (!util.types.isProxy(V) &&
|
|
635
|
-
|
|
654
|
+
if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
|
|
655
|
+
try {
|
|
656
|
+
return getHeadersList(V).entriesList
|
|
657
|
+
} catch {
|
|
658
|
+
// fall-through
|
|
659
|
+
}
|
|
636
660
|
}
|
|
637
661
|
|
|
638
662
|
if (typeof iterator === 'function') {
|
|
@@ -654,5 +678,9 @@ module.exports = {
|
|
|
654
678
|
// for test.
|
|
655
679
|
compareHeaderName,
|
|
656
680
|
Headers,
|
|
657
|
-
HeadersList
|
|
681
|
+
HeadersList,
|
|
682
|
+
getHeadersGuard,
|
|
683
|
+
setHeadersGuard,
|
|
684
|
+
setHeadersList,
|
|
685
|
+
getHeadersList
|
|
658
686
|
}
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
5
|
const { extractBody, mixinBody, cloneBody } = require('./body')
|
|
6
|
-
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
|
|
6
|
+
const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
|
|
7
7
|
const { FinalizationRegistry } = require('./dispatcher-weakref')()
|
|
8
8
|
const util = require('../../core/util')
|
|
9
9
|
const nodeUtil = require('node:util')
|
|
@@ -25,10 +25,10 @@ const {
|
|
|
25
25
|
requestDuplex
|
|
26
26
|
} = require('./constants')
|
|
27
27
|
const { kEnumerableProperty } = util
|
|
28
|
-
const { kHeaders, kSignal, kState,
|
|
28
|
+
const { kHeaders, kSignal, kState, kDispatcher } = require('./symbols')
|
|
29
29
|
const { webidl } = require('./webidl')
|
|
30
30
|
const { URLSerializer } = require('./data-url')
|
|
31
|
-
const {
|
|
31
|
+
const { kConstruct } = require('../../core/symbols')
|
|
32
32
|
const assert = require('node:assert')
|
|
33
33
|
const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('node:events')
|
|
34
34
|
|
|
@@ -445,8 +445,8 @@ class Request {
|
|
|
445
445
|
// Realm, whose header list is request’s header list and guard is
|
|
446
446
|
// "request".
|
|
447
447
|
this[kHeaders] = new Headers(kConstruct)
|
|
448
|
-
this[kHeaders]
|
|
449
|
-
this[kHeaders]
|
|
448
|
+
setHeadersList(this[kHeaders], request.headersList)
|
|
449
|
+
setHeadersGuard(this[kHeaders], 'request')
|
|
450
450
|
|
|
451
451
|
// 31. If this’s request’s mode is "no-cors", then:
|
|
452
452
|
if (mode === 'no-cors') {
|
|
@@ -459,13 +459,13 @@ class Request {
|
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
// 2. Set this’s headers’s guard to "request-no-cors".
|
|
462
|
-
this[kHeaders]
|
|
462
|
+
setHeadersGuard(this[kHeaders], 'request-no-cors')
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
// 32. If init is not empty, then:
|
|
466
466
|
if (initHasKey) {
|
|
467
467
|
/** @type {HeadersList} */
|
|
468
|
-
const headersList = this[kHeaders]
|
|
468
|
+
const headersList = getHeadersList(this[kHeaders])
|
|
469
469
|
// 1. Let headers be a copy of this’s headers and its associated header
|
|
470
470
|
// list.
|
|
471
471
|
// 2. If init["headers"] exists, then set headers to init["headers"].
|
|
@@ -519,7 +519,7 @@ class Request {
|
|
|
519
519
|
// 3, If Content-Type is non-null and this’s headers’s header list does
|
|
520
520
|
// not contain `Content-Type`, then append `Content-Type`/Content-Type to
|
|
521
521
|
// this’s headers.
|
|
522
|
-
if (contentType && !this[kHeaders]
|
|
522
|
+
if (contentType && !getHeadersList(this[kHeaders]).contains('content-type', true)) {
|
|
523
523
|
this[kHeaders].append('content-type', contentType)
|
|
524
524
|
}
|
|
525
525
|
}
|
|
@@ -785,7 +785,7 @@ class Request {
|
|
|
785
785
|
}
|
|
786
786
|
|
|
787
787
|
// 4. Return clonedRequestObject.
|
|
788
|
-
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders]
|
|
788
|
+
return fromInnerRequest(clonedRequest, ac.signal, getHeadersGuard(this[kHeaders]))
|
|
789
789
|
}
|
|
790
790
|
|
|
791
791
|
[nodeUtil.inspect.custom] (depth, options) {
|
|
@@ -894,8 +894,8 @@ function fromInnerRequest (innerRequest, signal, guard) {
|
|
|
894
894
|
request[kState] = innerRequest
|
|
895
895
|
request[kSignal] = signal
|
|
896
896
|
request[kHeaders] = new Headers(kConstruct)
|
|
897
|
-
request[kHeaders]
|
|
898
|
-
request[kHeaders]
|
|
897
|
+
setHeadersList(request[kHeaders], innerRequest.headersList)
|
|
898
|
+
setHeadersGuard(request[kHeaders], guard)
|
|
899
899
|
return request
|
|
900
900
|
}
|
|
901
901
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { Headers, HeadersList, fill } = require('./headers')
|
|
3
|
+
const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
|
|
4
4
|
const { extractBody, cloneBody, mixinBody } = require('./body')
|
|
5
5
|
const util = require('../../core/util')
|
|
6
6
|
const nodeUtil = require('node:util')
|
|
@@ -19,11 +19,11 @@ const {
|
|
|
19
19
|
redirectStatusSet,
|
|
20
20
|
nullBodyStatus
|
|
21
21
|
} = require('./constants')
|
|
22
|
-
const { kState, kHeaders
|
|
22
|
+
const { kState, kHeaders } = require('./symbols')
|
|
23
23
|
const { webidl } = require('./webidl')
|
|
24
24
|
const { FormData } = require('./formdata')
|
|
25
25
|
const { URLSerializer } = require('./data-url')
|
|
26
|
-
const {
|
|
26
|
+
const { kConstruct } = require('../../core/symbols')
|
|
27
27
|
const assert = require('node:assert')
|
|
28
28
|
const { types } = require('node:util')
|
|
29
29
|
const { isDisturbed, isErrored } = require('node:stream')
|
|
@@ -141,8 +141,8 @@ class Response {
|
|
|
141
141
|
// Realm, whose header list is this’s response’s header list and guard
|
|
142
142
|
// is "response".
|
|
143
143
|
this[kHeaders] = new Headers(kConstruct)
|
|
144
|
-
this[kHeaders]
|
|
145
|
-
this[kHeaders]
|
|
144
|
+
setHeadersGuard(this[kHeaders], 'response')
|
|
145
|
+
setHeadersList(this[kHeaders], this[kState].headersList)
|
|
146
146
|
|
|
147
147
|
// 3. Let bodyWithType be null.
|
|
148
148
|
let bodyWithType = null
|
|
@@ -255,7 +255,7 @@ class Response {
|
|
|
255
255
|
|
|
256
256
|
// 3. Return the result of creating a Response object, given
|
|
257
257
|
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
|
|
258
|
-
return fromInnerResponse(clonedResponse, this[kHeaders]
|
|
258
|
+
return fromInnerResponse(clonedResponse, getHeadersGuard(this[kHeaders]))
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
[nodeUtil.inspect.custom] (depth, options) {
|
|
@@ -522,8 +522,8 @@ function fromInnerResponse (innerResponse, guard) {
|
|
|
522
522
|
const response = new Response(kConstruct)
|
|
523
523
|
response[kState] = innerResponse
|
|
524
524
|
response[kHeaders] = new Headers(kConstruct)
|
|
525
|
-
response[kHeaders]
|
|
526
|
-
response[kHeaders]
|
|
525
|
+
setHeadersList(response[kHeaders], innerResponse.headersList)
|
|
526
|
+
setHeadersGuard(response[kHeaders], guard)
|
|
527
527
|
|
|
528
528
|
if (hasFinalizationRegistry && innerResponse.body?.stream) {
|
|
529
529
|
registry.register(response, innerResponse.body.stream)
|
package/lib/web/fetch/symbols.js
CHANGED
|
@@ -13,9 +13,8 @@ const { channels } = require('../../core/diagnostics')
|
|
|
13
13
|
const { CloseEvent } = require('./events')
|
|
14
14
|
const { makeRequest } = require('../fetch/request')
|
|
15
15
|
const { fetching } = require('../fetch/index')
|
|
16
|
-
const { Headers } = require('../fetch/headers')
|
|
16
|
+
const { Headers, getHeadersList } = require('../fetch/headers')
|
|
17
17
|
const { getDecodeSplit } = require('../fetch/util')
|
|
18
|
-
const { kHeadersList } = require('../../core/symbols')
|
|
19
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
20
19
|
|
|
21
20
|
/** @type {import('crypto')} */
|
|
@@ -59,7 +58,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
59
58
|
|
|
60
59
|
// Note: undici extension, allow setting custom headers.
|
|
61
60
|
if (options.headers) {
|
|
62
|
-
const headersList = new Headers(options.headers)
|
|
61
|
+
const headersList = getHeadersList(new Headers(options.headers))
|
|
63
62
|
|
|
64
63
|
request.headersList = headersList
|
|
65
64
|
}
|
|
@@ -261,13 +260,9 @@ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
|
|
|
261
260
|
/** @type {import('stream').Duplex} */
|
|
262
261
|
const socket = ws[kResponse].socket
|
|
263
262
|
|
|
264
|
-
socket.write(frame.createFrame(opcodes.CLOSE)
|
|
265
|
-
if (!err) {
|
|
266
|
-
ws[kSentClose] = sentCloseFrameState.SENT
|
|
267
|
-
}
|
|
268
|
-
})
|
|
263
|
+
socket.write(frame.createFrame(opcodes.CLOSE))
|
|
269
264
|
|
|
270
|
-
ws[kSentClose] = sentCloseFrameState.
|
|
265
|
+
ws[kSentClose] = sentCloseFrameState.SENT
|
|
271
266
|
|
|
272
267
|
// Upon either sending or receiving a Close control frame, it is said
|
|
273
268
|
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Writable } = require('node:stream')
|
|
4
|
+
const assert = require('node:assert')
|
|
4
5
|
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
|
|
5
6
|
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
|
|
6
7
|
const { channels } = require('../../core/diagnostics')
|
|
7
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
isValidStatusCode,
|
|
10
|
+
isValidOpcode,
|
|
11
|
+
failWebsocketConnection,
|
|
12
|
+
websocketMessageReceived,
|
|
13
|
+
utf8Decode,
|
|
14
|
+
isControlFrame,
|
|
15
|
+
isTextBinaryFrame,
|
|
16
|
+
isContinuationFrame
|
|
17
|
+
} = require('./util')
|
|
8
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
9
|
-
const {
|
|
19
|
+
const { closeWebSocketConnection } = require('./connection')
|
|
10
20
|
|
|
11
21
|
// This code was influenced by ws released under the MIT license.
|
|
12
22
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
@@ -16,6 +26,7 @@ const { CloseEvent } = require('./events')
|
|
|
16
26
|
class ByteParser extends Writable {
|
|
17
27
|
#buffers = []
|
|
18
28
|
#byteOffset = 0
|
|
29
|
+
#loop = false
|
|
19
30
|
|
|
20
31
|
#state = parserStates.INFO
|
|
21
32
|
|
|
@@ -35,6 +46,7 @@ class ByteParser extends Writable {
|
|
|
35
46
|
_write (chunk, _, callback) {
|
|
36
47
|
this.#buffers.push(chunk)
|
|
37
48
|
this.#byteOffset += chunk.length
|
|
49
|
+
this.#loop = true
|
|
38
50
|
|
|
39
51
|
this.run(callback)
|
|
40
52
|
}
|
|
@@ -45,7 +57,7 @@ class ByteParser extends Writable {
|
|
|
45
57
|
* or not enough bytes are buffered to parse.
|
|
46
58
|
*/
|
|
47
59
|
run (callback) {
|
|
48
|
-
while (
|
|
60
|
+
while (this.#loop) {
|
|
49
61
|
if (this.#state === parserStates.INFO) {
|
|
50
62
|
// If there aren't enough bytes to parse the payload length, etc.
|
|
51
63
|
if (this.#byteOffset < 2) {
|
|
@@ -53,148 +65,85 @@ class ByteParser extends Writable {
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
const buffer = this.consume(2)
|
|
68
|
+
const fin = (buffer[0] & 0x80) !== 0
|
|
69
|
+
const opcode = buffer[0] & 0x0F
|
|
70
|
+
const masked = (buffer[1] & 0x80) === 0x80
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.#info.masked = (buffer[1] & 0x80) === 0x80
|
|
72
|
+
const fragmented = !fin && opcode !== opcodes.CONTINUATION
|
|
73
|
+
const payloadLength = buffer[1] & 0x7F
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
const rsv1 = buffer[0] & 0x40
|
|
76
|
+
const rsv2 = buffer[0] & 0x20
|
|
77
|
+
const rsv3 = buffer[0] & 0x10
|
|
78
|
+
|
|
79
|
+
if (!isValidOpcode(opcode)) {
|
|
80
|
+
failWebsocketConnection(this.ws, 'Invalid opcode received')
|
|
63
81
|
return callback()
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
if (masked) {
|
|
85
|
+
failWebsocketConnection(this.ws, 'Frame cannot be masked')
|
|
86
|
+
return callback()
|
|
87
|
+
}
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
// MUST be 0 unless an extension is negotiated that defines meanings
|
|
90
|
+
// for non-zero values. If a nonzero value is received and none of
|
|
91
|
+
// the negotiated extensions defines the meaning of such a nonzero
|
|
92
|
+
// value, the receiving endpoint MUST _Fail the WebSocket
|
|
93
|
+
// Connection_.
|
|
94
|
+
if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) {
|
|
95
|
+
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
71
98
|
|
|
72
|
-
if (
|
|
99
|
+
if (fragmented && !isTextBinaryFrame(opcode)) {
|
|
73
100
|
// Only text and binary frames can be fragmented
|
|
74
101
|
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
|
|
75
102
|
return
|
|
76
103
|
}
|
|
77
104
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
this
|
|
82
|
-
|
|
83
|
-
} else if (payloadLength === 126) {
|
|
84
|
-
this.#state = parserStates.PAYLOADLENGTH_16
|
|
85
|
-
} else if (payloadLength === 127) {
|
|
86
|
-
this.#state = parserStates.PAYLOADLENGTH_64
|
|
105
|
+
// If we are already parsing a text/binary frame and do not receive either
|
|
106
|
+
// a continuation frame or close frame, fail the connection.
|
|
107
|
+
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
|
|
108
|
+
failWebsocketConnection(this.ws, 'Expected continuation frame')
|
|
109
|
+
return
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
if (this.#info.fragmented &&
|
|
112
|
+
if (this.#info.fragmented && fragmented) {
|
|
90
113
|
// A fragmented frame can't be fragmented itself
|
|
91
114
|
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
|
|
92
115
|
return
|
|
93
|
-
}
|
|
94
|
-
(this.#info.opcode === opcodes.PING ||
|
|
95
|
-
this.#info.opcode === opcodes.PONG ||
|
|
96
|
-
this.#info.opcode === opcodes.CLOSE) &&
|
|
97
|
-
payloadLength > 125
|
|
98
|
-
) {
|
|
99
|
-
// Control frames can have a payload length of 125 bytes MAX
|
|
100
|
-
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
|
|
101
|
-
return
|
|
102
|
-
} else if (this.#info.opcode === opcodes.CLOSE) {
|
|
103
|
-
if (payloadLength === 1) {
|
|
104
|
-
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const body = this.consume(payloadLength)
|
|
109
|
-
|
|
110
|
-
this.#info.closeInfo = this.parseCloseBody(body)
|
|
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
|
-
|
|
119
|
-
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
|
|
120
|
-
// If an endpoint receives a Close frame and did not previously send a
|
|
121
|
-
// Close frame, the endpoint MUST send a Close frame in response. (When
|
|
122
|
-
// sending a Close frame in response, the endpoint typically echos the
|
|
123
|
-
// status code it received.)
|
|
124
|
-
let body = emptyBuffer
|
|
125
|
-
if (this.#info.closeInfo.code) {
|
|
126
|
-
body = Buffer.allocUnsafe(2)
|
|
127
|
-
body.writeUInt16BE(this.#info.closeInfo.code, 0)
|
|
128
|
-
}
|
|
129
|
-
const closeFrame = new WebsocketFrameSend(body)
|
|
130
|
-
|
|
131
|
-
this.ws[kResponse].socket.write(
|
|
132
|
-
closeFrame.createFrame(opcodes.CLOSE),
|
|
133
|
-
(err) => {
|
|
134
|
-
if (!err) {
|
|
135
|
-
this.ws[kSentClose] = sentCloseFrameState.SENT
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Upon either sending or receiving a Close control frame, it is said
|
|
142
|
-
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
143
|
-
// WebSocket connection is in the CLOSING state.
|
|
144
|
-
this.ws[kReadyState] = states.CLOSING
|
|
145
|
-
this.ws[kReceivedClose] = true
|
|
146
|
-
|
|
147
|
-
this.end()
|
|
116
|
+
}
|
|
148
117
|
|
|
118
|
+
// "All control frames MUST have a payload length of 125 bytes or less
|
|
119
|
+
// and MUST NOT be fragmented."
|
|
120
|
+
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
|
|
121
|
+
failWebsocketConnection(this.ws, 'Control frame either too large or fragmented')
|
|
149
122
|
return
|
|
150
|
-
}
|
|
151
|
-
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
|
152
|
-
// response, unless it already received a Close frame.
|
|
153
|
-
// A Pong frame sent in response to a Ping frame must have identical
|
|
154
|
-
// "Application data"
|
|
155
|
-
|
|
156
|
-
const body = this.consume(payloadLength)
|
|
157
|
-
|
|
158
|
-
if (!this.ws[kReceivedClose]) {
|
|
159
|
-
const frame = new WebsocketFrameSend(body)
|
|
160
|
-
|
|
161
|
-
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
|
|
162
|
-
|
|
163
|
-
if (channels.ping.hasSubscribers) {
|
|
164
|
-
channels.ping.publish({
|
|
165
|
-
payload: body
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
this.#state = parserStates.INFO
|
|
171
|
-
|
|
172
|
-
if (this.#byteOffset > 0) {
|
|
173
|
-
continue
|
|
174
|
-
} else {
|
|
175
|
-
callback()
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
} else if (this.#info.opcode === opcodes.PONG) {
|
|
179
|
-
// A Pong frame MAY be sent unsolicited. This serves as a
|
|
180
|
-
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
|
181
|
-
// not expected.
|
|
123
|
+
}
|
|
182
124
|
|
|
183
|
-
|
|
125
|
+
if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
|
|
126
|
+
failWebsocketConnection(this.ws, 'Unexpected continuation frame')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
184
129
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
130
|
+
if (payloadLength <= 125) {
|
|
131
|
+
this.#info.payloadLength = payloadLength
|
|
132
|
+
this.#state = parserStates.READ_DATA
|
|
133
|
+
} else if (payloadLength === 126) {
|
|
134
|
+
this.#state = parserStates.PAYLOADLENGTH_16
|
|
135
|
+
} else if (payloadLength === 127) {
|
|
136
|
+
this.#state = parserStates.PAYLOADLENGTH_64
|
|
137
|
+
}
|
|
190
138
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else {
|
|
194
|
-
callback()
|
|
195
|
-
return
|
|
196
|
-
}
|
|
139
|
+
if (isTextBinaryFrame(opcode)) {
|
|
140
|
+
this.#info.binaryType = opcode
|
|
197
141
|
}
|
|
142
|
+
|
|
143
|
+
this.#info.opcode = opcode
|
|
144
|
+
this.#info.masked = masked
|
|
145
|
+
this.#info.fin = fin
|
|
146
|
+
this.#info.fragmented = fragmented
|
|
198
147
|
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
|
|
199
148
|
if (this.#byteOffset < 2) {
|
|
200
149
|
return callback()
|
|
@@ -229,33 +178,28 @@ class ByteParser extends Writable {
|
|
|
229
178
|
this.#state = parserStates.READ_DATA
|
|
230
179
|
} else if (this.#state === parserStates.READ_DATA) {
|
|
231
180
|
if (this.#byteOffset < this.#info.payloadLength) {
|
|
232
|
-
// If there is still more data in this chunk that needs to be read
|
|
233
181
|
return callback()
|
|
234
|
-
}
|
|
235
|
-
// If the server sent multiple frames in a single chunk
|
|
182
|
+
}
|
|
236
183
|
|
|
237
|
-
|
|
184
|
+
const body = this.consume(this.#info.payloadLength)
|
|
238
185
|
|
|
186
|
+
if (isControlFrame(this.#info.opcode)) {
|
|
187
|
+
this.#loop = this.parseControlFrame(body)
|
|
188
|
+
} else {
|
|
239
189
|
this.#fragments.push(body)
|
|
240
190
|
|
|
241
|
-
// If the frame is
|
|
242
|
-
// a
|
|
243
|
-
|
|
191
|
+
// If the frame is not fragmented, a message has been received.
|
|
192
|
+
// If the frame is fragmented, it will terminate with a fin bit set
|
|
193
|
+
// and an opcode of 0 (continuation), therefore we handle that when
|
|
194
|
+
// parsing continuation frames, not here.
|
|
195
|
+
if (!this.#info.fragmented && this.#info.fin) {
|
|
244
196
|
const fullMessage = Buffer.concat(this.#fragments)
|
|
245
|
-
|
|
246
|
-
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
|
|
247
|
-
|
|
248
|
-
this.#info = {}
|
|
197
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
|
249
198
|
this.#fragments.length = 0
|
|
250
199
|
}
|
|
251
|
-
|
|
252
|
-
this.#state = parserStates.INFO
|
|
253
200
|
}
|
|
254
|
-
}
|
|
255
201
|
|
|
256
|
-
|
|
257
|
-
callback()
|
|
258
|
-
break
|
|
202
|
+
this.#state = parserStates.INFO
|
|
259
203
|
}
|
|
260
204
|
}
|
|
261
205
|
}
|
|
@@ -263,11 +207,11 @@ class ByteParser extends Writable {
|
|
|
263
207
|
/**
|
|
264
208
|
* Take n bytes from the buffered Buffers
|
|
265
209
|
* @param {number} n
|
|
266
|
-
* @returns {Buffer
|
|
210
|
+
* @returns {Buffer}
|
|
267
211
|
*/
|
|
268
212
|
consume (n) {
|
|
269
213
|
if (n > this.#byteOffset) {
|
|
270
|
-
|
|
214
|
+
throw new Error('Called consume() before buffers satiated.')
|
|
271
215
|
} else if (n === 0) {
|
|
272
216
|
return emptyBuffer
|
|
273
217
|
}
|
|
@@ -303,6 +247,8 @@ class ByteParser extends Writable {
|
|
|
303
247
|
}
|
|
304
248
|
|
|
305
249
|
parseCloseBody (data) {
|
|
250
|
+
assert(data.length !== 1)
|
|
251
|
+
|
|
306
252
|
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
|
|
307
253
|
/** @type {number|undefined} */
|
|
308
254
|
let code
|
|
@@ -336,6 +282,91 @@ class ByteParser extends Writable {
|
|
|
336
282
|
return { code, reason, error: false }
|
|
337
283
|
}
|
|
338
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Parses control frames.
|
|
287
|
+
* @param {Buffer} body
|
|
288
|
+
*/
|
|
289
|
+
parseControlFrame (body) {
|
|
290
|
+
const { opcode, payloadLength } = this.#info
|
|
291
|
+
|
|
292
|
+
if (opcode === opcodes.CLOSE) {
|
|
293
|
+
if (payloadLength === 1) {
|
|
294
|
+
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.#info.closeInfo = this.parseCloseBody(body)
|
|
299
|
+
|
|
300
|
+
if (this.#info.closeInfo.error) {
|
|
301
|
+
const { code, reason } = this.#info.closeInfo
|
|
302
|
+
|
|
303
|
+
closeWebSocketConnection(this.ws, code, reason, reason.length)
|
|
304
|
+
failWebsocketConnection(this.ws, reason)
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
|
|
309
|
+
// If an endpoint receives a Close frame and did not previously send a
|
|
310
|
+
// Close frame, the endpoint MUST send a Close frame in response. (When
|
|
311
|
+
// sending a Close frame in response, the endpoint typically echos the
|
|
312
|
+
// status code it received.)
|
|
313
|
+
let body = emptyBuffer
|
|
314
|
+
if (this.#info.closeInfo.code) {
|
|
315
|
+
body = Buffer.allocUnsafe(2)
|
|
316
|
+
body.writeUInt16BE(this.#info.closeInfo.code, 0)
|
|
317
|
+
}
|
|
318
|
+
const closeFrame = new WebsocketFrameSend(body)
|
|
319
|
+
|
|
320
|
+
this.ws[kResponse].socket.write(
|
|
321
|
+
closeFrame.createFrame(opcodes.CLOSE),
|
|
322
|
+
(err) => {
|
|
323
|
+
if (!err) {
|
|
324
|
+
this.ws[kSentClose] = sentCloseFrameState.SENT
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Upon either sending or receiving a Close control frame, it is said
|
|
331
|
+
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
332
|
+
// WebSocket connection is in the CLOSING state.
|
|
333
|
+
this.ws[kReadyState] = states.CLOSING
|
|
334
|
+
this.ws[kReceivedClose] = true
|
|
335
|
+
|
|
336
|
+
this.end()
|
|
337
|
+
return false
|
|
338
|
+
} else if (opcode === opcodes.PING) {
|
|
339
|
+
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
|
340
|
+
// response, unless it already received a Close frame.
|
|
341
|
+
// A Pong frame sent in response to a Ping frame must have identical
|
|
342
|
+
// "Application data"
|
|
343
|
+
|
|
344
|
+
if (!this.ws[kReceivedClose]) {
|
|
345
|
+
const frame = new WebsocketFrameSend(body)
|
|
346
|
+
|
|
347
|
+
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
|
|
348
|
+
|
|
349
|
+
if (channels.ping.hasSubscribers) {
|
|
350
|
+
channels.ping.publish({
|
|
351
|
+
payload: body
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (opcode === opcodes.PONG) {
|
|
356
|
+
// A Pong frame MAY be sent unsolicited. This serves as a
|
|
357
|
+
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
|
358
|
+
// not expected.
|
|
359
|
+
|
|
360
|
+
if (channels.pong.hasSubscribers) {
|
|
361
|
+
channels.pong.publish({
|
|
362
|
+
payload: body
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
|
|
339
370
|
get closingInfo () {
|
|
340
371
|
return this.#info.closeInfo
|
|
341
372
|
}
|
|
@@ -210,6 +210,30 @@ function failWebsocketConnection (ws, reason) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
|
|
215
|
+
* @param {number} opcode
|
|
216
|
+
*/
|
|
217
|
+
function isControlFrame (opcode) {
|
|
218
|
+
return (
|
|
219
|
+
opcode === opcodes.CLOSE ||
|
|
220
|
+
opcode === opcodes.PING ||
|
|
221
|
+
opcode === opcodes.PONG
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isContinuationFrame (opcode) {
|
|
226
|
+
return opcode === opcodes.CONTINUATION
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isTextBinaryFrame (opcode) {
|
|
230
|
+
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isValidOpcode (opcode) {
|
|
234
|
+
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
|
214
238
|
const hasIntl = typeof process.versions.icu === 'string'
|
|
215
239
|
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
|
|
@@ -237,5 +261,9 @@ module.exports = {
|
|
|
237
261
|
isValidStatusCode,
|
|
238
262
|
failWebsocketConnection,
|
|
239
263
|
websocketMessageReceived,
|
|
240
|
-
utf8Decode
|
|
264
|
+
utf8Decode,
|
|
265
|
+
isControlFrame,
|
|
266
|
+
isContinuationFrame,
|
|
267
|
+
isTextBinaryFrame,
|
|
268
|
+
isValidOpcode
|
|
241
269
|
}
|
|
@@ -26,7 +26,7 @@ const { ByteParser } = require('./receiver')
|
|
|
26
26
|
const { kEnumerableProperty, isBlobLike } = require('../../core/util')
|
|
27
27
|
const { getGlobalDispatcher } = require('../../global')
|
|
28
28
|
const { types } = require('node:util')
|
|
29
|
-
const { ErrorEvent } = require('./events')
|
|
29
|
+
const { ErrorEvent, CloseEvent } = require('./events')
|
|
30
30
|
|
|
31
31
|
let experimentalWarned = false
|
|
32
32
|
|
|
@@ -287,7 +287,7 @@ class WebSocket extends EventTarget {
|
|
|
287
287
|
// not throw an exception must increase the bufferedAmount attribute
|
|
288
288
|
// by the length of data’s buffer in bytes.
|
|
289
289
|
|
|
290
|
-
const ab = new FastBuffer(data, data.byteOffset, data.byteLength)
|
|
290
|
+
const ab = new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
|
|
291
291
|
|
|
292
292
|
const frame = new WebsocketFrameSend(ab)
|
|
293
293
|
const buffer = frame.createFrame(opcodes.BINARY)
|
|
@@ -594,9 +594,19 @@ function onParserDrain () {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
function onParserError (err) {
|
|
597
|
-
|
|
597
|
+
let message
|
|
598
|
+
let code
|
|
599
|
+
|
|
600
|
+
if (err instanceof CloseEvent) {
|
|
601
|
+
message = err.reason
|
|
602
|
+
code = err.code
|
|
603
|
+
} else {
|
|
604
|
+
message = err.message
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))
|
|
598
608
|
|
|
599
|
-
closeWebSocketConnection(this,
|
|
609
|
+
closeWebSocketConnection(this, code)
|
|
600
610
|
}
|
|
601
611
|
|
|
602
612
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.17.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": {
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
|
|
89
89
|
"test:webidl": "borp -p \"test/webidl/*.js\"",
|
|
90
90
|
"test:websocket": "borp -p \"test/websocket/*.js\"",
|
|
91
|
+
"test:websocket:autobahn": "node test/autobahn/client.js",
|
|
92
|
+
"test:websocket:autobahn:report": "node test/autobahn/report.js",
|
|
91
93
|
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
92
94
|
"test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
93
95
|
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
|
|
@@ -135,7 +137,7 @@
|
|
|
135
137
|
"ignore": [
|
|
136
138
|
"lib/llhttp/constants.js",
|
|
137
139
|
"lib/llhttp/utils.js",
|
|
138
|
-
"test/wpt
|
|
140
|
+
"test/fixtures/wpt"
|
|
139
141
|
]
|
|
140
142
|
},
|
|
141
143
|
"tsd": {
|
package/types/dispatcher.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ declare class Dispatcher extends EventEmitter {
|
|
|
19
19
|
connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
|
|
20
20
|
connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
|
|
21
21
|
/** Compose a chain of dispatchers */
|
|
22
|
-
compose(dispatchers: Dispatcher.
|
|
23
|
-
compose(...dispatchers: Dispatcher.
|
|
22
|
+
compose(dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
|
|
23
|
+
compose(...dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
|
|
24
24
|
/** Performs an HTTP request. */
|
|
25
25
|
request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>;
|
|
26
26
|
request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void;
|
|
@@ -97,7 +97,7 @@ declare class Dispatcher extends EventEmitter {
|
|
|
97
97
|
|
|
98
98
|
declare namespace Dispatcher {
|
|
99
99
|
export interface ComposedDispatcher extends Dispatcher {}
|
|
100
|
-
export type
|
|
100
|
+
export type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch'];
|
|
101
101
|
export interface DispatchOptions {
|
|
102
102
|
origin?: string | URL;
|
|
103
103
|
path: string;
|
package/types/index.d.ts
CHANGED
|
@@ -66,4 +66,9 @@ declare namespace Undici {
|
|
|
66
66
|
var File: typeof import('./file').File;
|
|
67
67
|
var FileReader: typeof import('./filereader').FileReader;
|
|
68
68
|
var caches: typeof import('./cache').caches;
|
|
69
|
+
var interceptors: {
|
|
70
|
+
dump: typeof import('./interceptors').dump;
|
|
71
|
+
retry: typeof import('./interceptors').retry;
|
|
72
|
+
redirect: typeof import('./interceptors').redirect;
|
|
73
|
+
}
|
|
69
74
|
}
|
package/types/interceptors.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import Dispatcher from "./dispatcher";
|
|
2
|
+
import RetryHandler from "./retry-handler";
|
|
2
3
|
|
|
3
|
-
type
|
|
4
|
+
export type DumpInterceptorOpts = { maxSize?: number }
|
|
5
|
+
export type RetryInterceptorOpts = RetryHandler.RetryOptions
|
|
6
|
+
export type RedirectInterceptorOpts = { maxRedirections?: number }
|
|
4
7
|
|
|
5
|
-
export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.
|
|
8
|
+
export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
|
|
9
|
+
export declare function dump(opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
|
|
10
|
+
export declare function retry(opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
|
|
11
|
+
export declare function redirect(opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
|