undici 7.16.0 → 7.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/README.md +47 -1
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/DiagnosticsChannel.md +57 -0
- package/docs/docs/api/Dispatcher.md +86 -0
- package/docs/docs/api/RoundRobinPool.md +145 -0
- package/docs/docs/api/WebSocket.md +21 -0
- package/docs/docs/best-practices/crawling.md +58 -0
- package/index.js +4 -1
- package/lib/api/api-upgrade.js +2 -1
- package/lib/core/connect.js +4 -1
- package/lib/core/diagnostics.js +28 -1
- package/lib/core/symbols.js +3 -0
- package/lib/core/util.js +29 -31
- package/lib/dispatcher/balanced-pool.js +10 -0
- package/lib/dispatcher/client-h1.js +0 -16
- package/lib/dispatcher/client-h2.js +153 -23
- package/lib/dispatcher/client.js +7 -2
- package/lib/dispatcher/dispatcher-base.js +11 -12
- package/lib/dispatcher/h2c-client.js +7 -78
- package/lib/dispatcher/pool-base.js +1 -1
- package/lib/dispatcher/proxy-agent.js +13 -2
- package/lib/dispatcher/round-robin-pool.js +137 -0
- package/lib/encoding/index.js +33 -0
- package/lib/handler/cache-handler.js +84 -27
- package/lib/handler/deduplication-handler.js +216 -0
- package/lib/handler/retry-handler.js +0 -2
- package/lib/interceptor/cache.js +35 -17
- package/lib/interceptor/decompress.js +2 -1
- package/lib/interceptor/deduplicate.js +109 -0
- package/lib/interceptor/dns.js +55 -13
- package/lib/mock/mock-utils.js +1 -2
- package/lib/mock/snapshot-agent.js +11 -5
- package/lib/mock/snapshot-recorder.js +12 -4
- package/lib/mock/snapshot-utils.js +4 -4
- package/lib/util/cache.js +29 -1
- package/lib/util/runtime-features.js +124 -0
- package/lib/web/cookies/parse.js +1 -1
- package/lib/web/fetch/body.js +29 -39
- package/lib/web/fetch/data-url.js +12 -160
- package/lib/web/fetch/formdata-parser.js +204 -127
- package/lib/web/fetch/index.js +9 -6
- package/lib/web/fetch/request.js +6 -0
- package/lib/web/fetch/response.js +2 -3
- package/lib/web/fetch/util.js +2 -65
- package/lib/web/infra/index.js +229 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
- package/lib/web/webidl/index.js +4 -2
- package/lib/web/websocket/connection.js +31 -21
- package/lib/web/websocket/frame.js +9 -15
- package/lib/web/websocket/stream/websocketstream.js +1 -1
- package/lib/web/websocket/util.js +2 -1
- package/package.json +5 -4
- package/types/agent.d.ts +1 -1
- package/types/api.d.ts +2 -2
- package/types/balanced-pool.d.ts +2 -1
- package/types/cache-interceptor.d.ts +1 -0
- package/types/client.d.ts +1 -1
- package/types/connector.d.ts +2 -2
- package/types/diagnostics-channel.d.ts +2 -2
- package/types/dispatcher.d.ts +12 -12
- package/types/fetch.d.ts +4 -4
- package/types/formdata.d.ts +1 -1
- package/types/h2c-client.d.ts +1 -1
- package/types/index.d.ts +9 -1
- package/types/interceptors.d.ts +36 -2
- package/types/pool.d.ts +1 -1
- package/types/readable.d.ts +2 -2
- package/types/round-robin-pool.d.ts +41 -0
- package/types/websocket.d.ts +9 -9
package/lib/core/util.js
CHANGED
|
@@ -615,14 +615,14 @@ function ReadableStreamFrom (iterable) {
|
|
|
615
615
|
pull (controller) {
|
|
616
616
|
return iterator.next().then(({ done, value }) => {
|
|
617
617
|
if (done) {
|
|
618
|
-
queueMicrotask(() => {
|
|
618
|
+
return queueMicrotask(() => {
|
|
619
619
|
controller.close()
|
|
620
620
|
controller.byobRequest?.respond(0)
|
|
621
621
|
})
|
|
622
622
|
} else {
|
|
623
623
|
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
|
624
624
|
if (buf.byteLength) {
|
|
625
|
-
controller.enqueue(new Uint8Array(buf))
|
|
625
|
+
return controller.enqueue(new Uint8Array(buf))
|
|
626
626
|
} else {
|
|
627
627
|
return this.pull(controller)
|
|
628
628
|
}
|
|
@@ -666,48 +666,46 @@ function addAbortListener (signal, listener) {
|
|
|
666
666
|
return () => signal.removeListener('abort', listener)
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
const validTokenChars = new Uint8Array([
|
|
670
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
|
|
671
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
|
|
672
|
+
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./)
|
|
673
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?)
|
|
674
|
+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O)
|
|
675
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_)
|
|
676
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o)
|
|
677
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~)
|
|
678
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
|
|
679
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159
|
|
680
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175
|
|
681
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191
|
|
682
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207
|
|
683
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223
|
|
684
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239
|
|
685
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255
|
|
686
|
+
])
|
|
687
|
+
|
|
669
688
|
/**
|
|
670
689
|
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
671
690
|
* @param {number} c
|
|
672
691
|
* @returns {boolean}
|
|
673
692
|
*/
|
|
674
693
|
function isTokenCharCode (c) {
|
|
675
|
-
|
|
676
|
-
case 0x22:
|
|
677
|
-
case 0x28:
|
|
678
|
-
case 0x29:
|
|
679
|
-
case 0x2c:
|
|
680
|
-
case 0x2f:
|
|
681
|
-
case 0x3a:
|
|
682
|
-
case 0x3b:
|
|
683
|
-
case 0x3c:
|
|
684
|
-
case 0x3d:
|
|
685
|
-
case 0x3e:
|
|
686
|
-
case 0x3f:
|
|
687
|
-
case 0x40:
|
|
688
|
-
case 0x5b:
|
|
689
|
-
case 0x5c:
|
|
690
|
-
case 0x5d:
|
|
691
|
-
case 0x7b:
|
|
692
|
-
case 0x7d:
|
|
693
|
-
// DQUOTE and "(),/:;<=>?@[\]{}"
|
|
694
|
-
return false
|
|
695
|
-
default:
|
|
696
|
-
// VCHAR %x21-7E
|
|
697
|
-
return c >= 0x21 && c <= 0x7e
|
|
698
|
-
}
|
|
694
|
+
return (validTokenChars[c] === 1)
|
|
699
695
|
}
|
|
700
696
|
|
|
697
|
+
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
|
|
698
|
+
|
|
701
699
|
/**
|
|
702
700
|
* @param {string} characters
|
|
703
701
|
* @returns {boolean}
|
|
704
702
|
*/
|
|
705
703
|
function isValidHTTPToken (characters) {
|
|
706
|
-
if (characters.length
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
for (let i = 0; i < characters.length; ++
|
|
710
|
-
if (
|
|
704
|
+
if (characters.length >= 12) return tokenRegExp.test(characters)
|
|
705
|
+
if (characters.length === 0) return false
|
|
706
|
+
|
|
707
|
+
for (let i = 0; i < characters.length; i++) {
|
|
708
|
+
if (validTokenChars[characters.charCodeAt(i)] !== 1) {
|
|
711
709
|
return false
|
|
712
710
|
}
|
|
713
711
|
}
|
|
@@ -140,6 +140,16 @@ class BalancedPool extends PoolBase {
|
|
|
140
140
|
return this
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
getUpstream (upstream) {
|
|
144
|
+
const upstreamOrigin = parseOrigin(upstream).origin
|
|
145
|
+
|
|
146
|
+
return this[kClients].find((pool) => (
|
|
147
|
+
pool[kUrl].origin === upstreamOrigin &&
|
|
148
|
+
pool.closed !== true &&
|
|
149
|
+
pool.destroyed !== true
|
|
150
|
+
))
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
get upstreams () {
|
|
144
154
|
return this[kClients]
|
|
145
155
|
.filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true)
|
|
@@ -77,12 +77,10 @@ function lazyllhttp () {
|
|
|
77
77
|
if (useWasmSIMD) {
|
|
78
78
|
try {
|
|
79
79
|
mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
|
|
80
|
-
/* istanbul ignore next */
|
|
81
80
|
} catch {
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
/* istanbul ignore next */
|
|
86
84
|
if (!mod) {
|
|
87
85
|
// We could check if the error was caused by the simd option not
|
|
88
86
|
// being enabled, but the occurring of this other error
|
|
@@ -100,7 +98,6 @@ function lazyllhttp () {
|
|
|
100
98
|
* @returns {number}
|
|
101
99
|
*/
|
|
102
100
|
wasm_on_url: (p, at, len) => {
|
|
103
|
-
/* istanbul ignore next */
|
|
104
101
|
return 0
|
|
105
102
|
},
|
|
106
103
|
/**
|
|
@@ -265,7 +262,6 @@ class Parser {
|
|
|
265
262
|
|
|
266
263
|
this.timeoutValue = delay
|
|
267
264
|
} else if (this.timeout) {
|
|
268
|
-
// istanbul ignore else: only for jest
|
|
269
265
|
if (this.timeout.refresh) {
|
|
270
266
|
this.timeout.refresh()
|
|
271
267
|
}
|
|
@@ -286,7 +282,6 @@ class Parser {
|
|
|
286
282
|
|
|
287
283
|
assert(this.timeoutType === TIMEOUT_BODY)
|
|
288
284
|
if (this.timeout) {
|
|
289
|
-
// istanbul ignore else: only for jest
|
|
290
285
|
if (this.timeout.refresh) {
|
|
291
286
|
this.timeout.refresh()
|
|
292
287
|
}
|
|
@@ -356,7 +351,6 @@ class Parser {
|
|
|
356
351
|
} else {
|
|
357
352
|
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
|
358
353
|
let message = ''
|
|
359
|
-
/* istanbul ignore else: difficult to make a test case for */
|
|
360
354
|
if (ptr) {
|
|
361
355
|
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
362
356
|
message =
|
|
@@ -402,7 +396,6 @@ class Parser {
|
|
|
402
396
|
onMessageBegin () {
|
|
403
397
|
const { socket, client } = this
|
|
404
398
|
|
|
405
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
406
399
|
if (socket.destroyed) {
|
|
407
400
|
return -1
|
|
408
401
|
}
|
|
@@ -531,14 +524,12 @@ class Parser {
|
|
|
531
524
|
onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
|
|
532
525
|
const { client, socket, headers, statusText } = this
|
|
533
526
|
|
|
534
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
535
527
|
if (socket.destroyed) {
|
|
536
528
|
return -1
|
|
537
529
|
}
|
|
538
530
|
|
|
539
531
|
const request = client[kQueue][client[kRunningIdx]]
|
|
540
532
|
|
|
541
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
542
533
|
if (!request) {
|
|
543
534
|
return -1
|
|
544
535
|
}
|
|
@@ -572,7 +563,6 @@ class Parser {
|
|
|
572
563
|
: client[kBodyTimeout]
|
|
573
564
|
this.setTimeout(bodyTimeout, TIMEOUT_BODY)
|
|
574
565
|
} else if (this.timeout) {
|
|
575
|
-
// istanbul ignore else: only for jest
|
|
576
566
|
if (this.timeout.refresh) {
|
|
577
567
|
this.timeout.refresh()
|
|
578
568
|
}
|
|
@@ -653,7 +643,6 @@ class Parser {
|
|
|
653
643
|
|
|
654
644
|
assert(this.timeoutType === TIMEOUT_BODY)
|
|
655
645
|
if (this.timeout) {
|
|
656
|
-
// istanbul ignore else: only for jest
|
|
657
646
|
if (this.timeout.refresh) {
|
|
658
647
|
this.timeout.refresh()
|
|
659
648
|
}
|
|
@@ -709,7 +698,6 @@ class Parser {
|
|
|
709
698
|
return 0
|
|
710
699
|
}
|
|
711
700
|
|
|
712
|
-
/* istanbul ignore next: should be handled by llhttp? */
|
|
713
701
|
if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
|
|
714
702
|
util.destroy(socket, new ResponseContentLengthMismatchError())
|
|
715
703
|
return -1
|
|
@@ -750,7 +738,6 @@ class Parser {
|
|
|
750
738
|
function onParserTimeout (parser) {
|
|
751
739
|
const { socket, timeoutType, client, paused } = parser.deref()
|
|
752
740
|
|
|
753
|
-
/* istanbul ignore else */
|
|
754
741
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
755
742
|
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
756
743
|
assert(!paused, 'cannot be paused while waiting for headers')
|
|
@@ -1157,7 +1144,6 @@ function writeH1 (client, request) {
|
|
|
1157
1144
|
channels.sendHeaders.publish({ request, headers: header, socket })
|
|
1158
1145
|
}
|
|
1159
1146
|
|
|
1160
|
-
/* istanbul ignore else: assertion */
|
|
1161
1147
|
if (!body || bodyLength === 0) {
|
|
1162
1148
|
writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload)
|
|
1163
1149
|
} else if (util.isBuffer(body)) {
|
|
@@ -1538,7 +1524,6 @@ class AsyncWriter {
|
|
|
1538
1524
|
|
|
1539
1525
|
if (!ret) {
|
|
1540
1526
|
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1541
|
-
// istanbul ignore else: only for jest
|
|
1542
1527
|
if (socket[kParser].timeout.refresh) {
|
|
1543
1528
|
socket[kParser].timeout.refresh()
|
|
1544
1529
|
}
|
|
@@ -1589,7 +1574,6 @@ class AsyncWriter {
|
|
|
1589
1574
|
}
|
|
1590
1575
|
|
|
1591
1576
|
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1592
|
-
// istanbul ignore else: only for jest
|
|
1593
1577
|
if (socket[kParser].timeout.refresh) {
|
|
1594
1578
|
socket[kParser].timeout.refresh()
|
|
1595
1579
|
}
|
|
@@ -7,7 +7,8 @@ const {
|
|
|
7
7
|
RequestContentLengthMismatchError,
|
|
8
8
|
RequestAbortedError,
|
|
9
9
|
SocketError,
|
|
10
|
-
InformationalError
|
|
10
|
+
InformationalError,
|
|
11
|
+
InvalidArgumentError
|
|
11
12
|
} = require('../core/errors.js')
|
|
12
13
|
const {
|
|
13
14
|
kUrl,
|
|
@@ -28,7 +29,10 @@ const {
|
|
|
28
29
|
kSize,
|
|
29
30
|
kHTTPContext,
|
|
30
31
|
kClosed,
|
|
31
|
-
kBodyTimeout
|
|
32
|
+
kBodyTimeout,
|
|
33
|
+
kEnableConnectProtocol,
|
|
34
|
+
kRemoteSettings,
|
|
35
|
+
kHTTP2Stream
|
|
32
36
|
} = require('../core/symbols.js')
|
|
33
37
|
const { channels } = require('../core/diagnostics.js')
|
|
34
38
|
|
|
@@ -53,7 +57,10 @@ const {
|
|
|
53
57
|
HTTP2_HEADER_SCHEME,
|
|
54
58
|
HTTP2_HEADER_CONTENT_LENGTH,
|
|
55
59
|
HTTP2_HEADER_EXPECT,
|
|
56
|
-
HTTP2_HEADER_STATUS
|
|
60
|
+
HTTP2_HEADER_STATUS,
|
|
61
|
+
HTTP2_HEADER_PROTOCOL,
|
|
62
|
+
NGHTTP2_REFUSED_STREAM,
|
|
63
|
+
NGHTTP2_CANCEL
|
|
57
64
|
}
|
|
58
65
|
} = http2
|
|
59
66
|
|
|
@@ -93,12 +100,21 @@ function connectH2 (client, socket) {
|
|
|
93
100
|
session[kClient] = client
|
|
94
101
|
session[kSocket] = socket
|
|
95
102
|
session[kHTTP2Session] = null
|
|
103
|
+
// We set it to true by default in a best-effort; however once connected to an H2 server
|
|
104
|
+
// we will check if extended CONNECT protocol is supported or not
|
|
105
|
+
// and set this value accordingly.
|
|
106
|
+
session[kEnableConnectProtocol] = false
|
|
107
|
+
// States whether or not we have received the remote settings from the server
|
|
108
|
+
session[kRemoteSettings] = false
|
|
96
109
|
|
|
97
110
|
util.addListener(session, 'error', onHttp2SessionError)
|
|
98
111
|
util.addListener(session, 'frameError', onHttp2FrameError)
|
|
99
112
|
util.addListener(session, 'end', onHttp2SessionEnd)
|
|
100
113
|
util.addListener(session, 'goaway', onHttp2SessionGoAway)
|
|
101
114
|
util.addListener(session, 'close', onHttp2SessionClose)
|
|
115
|
+
util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
|
|
116
|
+
// TODO (@metcoder95): implement SETTINGS support
|
|
117
|
+
// util.addListener(session, 'localSettings', onHttp2RemoteSettings)
|
|
102
118
|
|
|
103
119
|
session.unref()
|
|
104
120
|
|
|
@@ -115,12 +131,23 @@ function connectH2 (client, socket) {
|
|
|
115
131
|
return {
|
|
116
132
|
version: 'h2',
|
|
117
133
|
defaultPipelining: Infinity,
|
|
134
|
+
/**
|
|
135
|
+
* @param {import('../core/request.js')} request
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
118
138
|
write (request) {
|
|
119
139
|
return writeH2(client, request)
|
|
120
140
|
},
|
|
141
|
+
/**
|
|
142
|
+
* @returns {void}
|
|
143
|
+
*/
|
|
121
144
|
resume () {
|
|
122
145
|
resumeH2(client)
|
|
123
146
|
},
|
|
147
|
+
/**
|
|
148
|
+
* @param {Error | null} err
|
|
149
|
+
* @param {() => void} callback
|
|
150
|
+
*/
|
|
124
151
|
destroy (err, callback) {
|
|
125
152
|
if (socket[kClosed]) {
|
|
126
153
|
queueMicrotask(callback)
|
|
@@ -128,10 +155,43 @@ function connectH2 (client, socket) {
|
|
|
128
155
|
socket.destroy(err).on('close', callback)
|
|
129
156
|
}
|
|
130
157
|
},
|
|
158
|
+
/**
|
|
159
|
+
* @type {boolean}
|
|
160
|
+
*/
|
|
131
161
|
get destroyed () {
|
|
132
162
|
return socket.destroyed
|
|
133
163
|
},
|
|
134
|
-
|
|
164
|
+
/**
|
|
165
|
+
* @param {import('../core/request.js')} request
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
*/
|
|
168
|
+
busy (request) {
|
|
169
|
+
if (request != null) {
|
|
170
|
+
if (client[kRunning] > 0) {
|
|
171
|
+
// We are already processing requests
|
|
172
|
+
|
|
173
|
+
// Non-idempotent request cannot be retried.
|
|
174
|
+
// Ensure that no other requests are inflight and
|
|
175
|
+
// could cause failure.
|
|
176
|
+
if (request.idempotent === false) return true
|
|
177
|
+
// Don't dispatch an upgrade until all preceding requests have completed.
|
|
178
|
+
// Possibly, we do not have remote settings confirmed yet.
|
|
179
|
+
if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
|
|
180
|
+
// Request with stream or iterator body can error while other requests
|
|
181
|
+
// are inflight and indirectly error those as well.
|
|
182
|
+
// Ensure this doesn't happen by waiting for inflight
|
|
183
|
+
// to complete before dispatching.
|
|
184
|
+
|
|
185
|
+
// Request with stream or iterator body cannot be retried.
|
|
186
|
+
// Ensure that no other requests are inflight and
|
|
187
|
+
// could cause failure.
|
|
188
|
+
if (util.bodyLength(request.body) !== 0 &&
|
|
189
|
+
(util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true
|
|
190
|
+
} else {
|
|
191
|
+
return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
135
195
|
return false
|
|
136
196
|
}
|
|
137
197
|
}
|
|
@@ -151,6 +211,27 @@ function resumeH2 (client) {
|
|
|
151
211
|
}
|
|
152
212
|
}
|
|
153
213
|
|
|
214
|
+
function onHttp2RemoteSettings (settings) {
|
|
215
|
+
// Fallbacks are a safe bet, remote setting will always override
|
|
216
|
+
this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams]
|
|
217
|
+
/**
|
|
218
|
+
* From RFC-8441
|
|
219
|
+
* A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter
|
|
220
|
+
* with the value of 0 after previously sending a value of 1.
|
|
221
|
+
*/
|
|
222
|
+
// Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled
|
|
223
|
+
if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) {
|
|
224
|
+
const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441')
|
|
225
|
+
this[kSocket][kError] = err
|
|
226
|
+
this[kClient][kOnError](err)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol]
|
|
231
|
+
this[kRemoteSettings] = true
|
|
232
|
+
this[kClient][kResume]()
|
|
233
|
+
}
|
|
234
|
+
|
|
154
235
|
function onHttp2SessionError (err) {
|
|
155
236
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
156
237
|
|
|
@@ -282,8 +363,8 @@ function writeH2 (client, request) {
|
|
|
282
363
|
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
|
|
283
364
|
let { body } = request
|
|
284
365
|
|
|
285
|
-
if (upgrade) {
|
|
286
|
-
util.errorRequest(client, request, new
|
|
366
|
+
if (upgrade != null && upgrade !== 'websocket') {
|
|
367
|
+
util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
|
|
287
368
|
return false
|
|
288
369
|
}
|
|
289
370
|
|
|
@@ -364,26 +445,75 @@ function writeH2 (client, request) {
|
|
|
364
445
|
return false
|
|
365
446
|
}
|
|
366
447
|
|
|
367
|
-
if (method === 'CONNECT') {
|
|
448
|
+
if (upgrade || method === 'CONNECT') {
|
|
368
449
|
session.ref()
|
|
369
|
-
|
|
450
|
+
|
|
451
|
+
if (upgrade === 'websocket') {
|
|
452
|
+
// We cannot upgrade to websocket if extended CONNECT protocol is not supported
|
|
453
|
+
if (session[kEnableConnectProtocol] === false) {
|
|
454
|
+
util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
|
|
455
|
+
session.unref()
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// We force the method to CONNECT
|
|
460
|
+
// as per RFC-8441
|
|
461
|
+
// https://datatracker.ietf.org/doc/html/rfc8441#section-4
|
|
462
|
+
headers[HTTP2_HEADER_METHOD] = 'CONNECT'
|
|
463
|
+
headers[HTTP2_HEADER_PROTOCOL] = 'websocket'
|
|
464
|
+
// :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT
|
|
465
|
+
headers[HTTP2_HEADER_PATH] = path
|
|
466
|
+
|
|
467
|
+
if (protocol === 'ws:' || protocol === 'wss:') {
|
|
468
|
+
headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https'
|
|
469
|
+
} else {
|
|
470
|
+
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
stream = session.request(headers, { endStream: false, signal })
|
|
474
|
+
stream[kHTTP2Stream] = true
|
|
475
|
+
|
|
476
|
+
stream.once('response', (headers, _flags) => {
|
|
477
|
+
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
478
|
+
|
|
479
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
480
|
+
|
|
481
|
+
++session[kOpenStreams]
|
|
482
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
stream.on('error', () => {
|
|
486
|
+
if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
|
|
487
|
+
// NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
|
|
488
|
+
// We do not treat those as errors as the server might
|
|
489
|
+
// not support websockets and refuse the stream
|
|
490
|
+
abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
stream.once('close', () => {
|
|
495
|
+
session[kOpenStreams] -= 1
|
|
496
|
+
if (session[kOpenStreams] === 0) session.unref()
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
stream.setTimeout(requestTimeout)
|
|
500
|
+
return true
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// TODO: consolidate once we support CONNECT properly
|
|
504
|
+
// NOTE: We are already connected, streams are pending, first request
|
|
370
505
|
// will create a new stream. We trigger a request to create the stream and wait until
|
|
371
506
|
// `ready` event is triggered
|
|
372
507
|
// We disabled endStream to allow the user to write to the stream
|
|
373
508
|
stream = session.request(headers, { endStream: false, signal })
|
|
509
|
+
stream[kHTTP2Stream] = true
|
|
510
|
+
stream.on('response', headers => {
|
|
511
|
+
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
374
512
|
|
|
375
|
-
|
|
376
|
-
request.onUpgrade(null, null, stream)
|
|
513
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
377
514
|
++session[kOpenStreams]
|
|
378
515
|
client[kQueue][client[kRunningIdx]++] = null
|
|
379
|
-
}
|
|
380
|
-
stream.once('ready', () => {
|
|
381
|
-
request.onUpgrade(null, null, stream)
|
|
382
|
-
++session[kOpenStreams]
|
|
383
|
-
client[kQueue][client[kRunningIdx]++] = null
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
|
|
516
|
+
})
|
|
387
517
|
stream.once('close', () => {
|
|
388
518
|
session[kOpenStreams] -= 1
|
|
389
519
|
if (session[kOpenStreams] === 0) session.unref()
|
|
@@ -395,7 +525,6 @@ function writeH2 (client, request) {
|
|
|
395
525
|
|
|
396
526
|
// https://tools.ietf.org/html/rfc7540#section-8.3
|
|
397
527
|
// :path and :scheme headers must be omitted when sending CONNECT
|
|
398
|
-
|
|
399
528
|
headers[HTTP2_HEADER_PATH] = path
|
|
400
529
|
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
|
|
401
530
|
|
|
@@ -435,12 +564,12 @@ function writeH2 (client, request) {
|
|
|
435
564
|
contentLength = request.contentLength
|
|
436
565
|
}
|
|
437
566
|
|
|
438
|
-
if (
|
|
567
|
+
if (!expectsPayload) {
|
|
439
568
|
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
440
569
|
// A user agent SHOULD NOT send a Content-Length header field when
|
|
441
570
|
// the request message does not contain a payload body and the method
|
|
442
571
|
// semantics do not anticipate such a body.
|
|
443
|
-
|
|
572
|
+
// And for methods that don't expect a payload, omit Content-Length.
|
|
444
573
|
contentLength = null
|
|
445
574
|
}
|
|
446
575
|
|
|
@@ -456,7 +585,7 @@ function writeH2 (client, request) {
|
|
|
456
585
|
}
|
|
457
586
|
|
|
458
587
|
if (contentLength != null) {
|
|
459
|
-
assert(body, 'no body must not have content length')
|
|
588
|
+
assert(body || contentLength === 0, 'no body must not have content length')
|
|
460
589
|
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
|
|
461
590
|
}
|
|
462
591
|
|
|
@@ -475,6 +604,7 @@ function writeH2 (client, request) {
|
|
|
475
604
|
if (expectContinue) {
|
|
476
605
|
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
|
477
606
|
stream = session.request(headers, { endStream: shouldEndStream, signal })
|
|
607
|
+
stream[kHTTP2Stream] = true
|
|
478
608
|
|
|
479
609
|
stream.once('continue', writeBodyH2)
|
|
480
610
|
} else {
|
|
@@ -482,6 +612,7 @@ function writeH2 (client, request) {
|
|
|
482
612
|
endStream: shouldEndStream,
|
|
483
613
|
signal
|
|
484
614
|
})
|
|
615
|
+
stream[kHTTP2Stream] = true
|
|
485
616
|
|
|
486
617
|
writeBodyH2()
|
|
487
618
|
}
|
|
@@ -590,7 +721,6 @@ function writeH2 (client, request) {
|
|
|
590
721
|
return true
|
|
591
722
|
|
|
592
723
|
function writeBodyH2 () {
|
|
593
|
-
/* istanbul ignore else: assertion */
|
|
594
724
|
if (!body || contentLength === 0) {
|
|
595
725
|
writeBuffer(
|
|
596
726
|
abort,
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -107,7 +107,8 @@ class Client extends DispatcherBase {
|
|
|
107
107
|
autoSelectFamilyAttemptTimeout,
|
|
108
108
|
// h2
|
|
109
109
|
maxConcurrentStreams,
|
|
110
|
-
allowH2
|
|
110
|
+
allowH2,
|
|
111
|
+
useH2c
|
|
111
112
|
} = {}) {
|
|
112
113
|
if (keepAlive !== undefined) {
|
|
113
114
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -199,6 +200,10 @@ class Client extends DispatcherBase {
|
|
|
199
200
|
throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
if (useH2c != null && typeof useH2c !== 'boolean') {
|
|
204
|
+
throw new InvalidArgumentError('useH2c must be a valid boolean value')
|
|
205
|
+
}
|
|
206
|
+
|
|
202
207
|
super()
|
|
203
208
|
|
|
204
209
|
if (typeof connect !== 'function') {
|
|
@@ -206,6 +211,7 @@ class Client extends DispatcherBase {
|
|
|
206
211
|
...tls,
|
|
207
212
|
maxCachedSessions,
|
|
208
213
|
allowH2,
|
|
214
|
+
useH2c,
|
|
209
215
|
socketPath,
|
|
210
216
|
timeout: connectTimeout,
|
|
211
217
|
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
@@ -289,7 +295,6 @@ class Client extends DispatcherBase {
|
|
|
289
295
|
)
|
|
290
296
|
}
|
|
291
297
|
|
|
292
|
-
/* istanbul ignore: only used for test */
|
|
293
298
|
[kConnect] (cb) {
|
|
294
299
|
connect(this)
|
|
295
300
|
this.once('connect', cb)
|
|
@@ -16,14 +16,14 @@ class DispatcherBase extends Dispatcher {
|
|
|
16
16
|
/** @type {boolean} */
|
|
17
17
|
[kDestroyed] = false;
|
|
18
18
|
|
|
19
|
-
/** @type {Array|null} */
|
|
19
|
+
/** @type {Array<Function|null} */
|
|
20
20
|
[kOnDestroyed] = null;
|
|
21
21
|
|
|
22
22
|
/** @type {boolean} */
|
|
23
23
|
[kClosed] = false;
|
|
24
24
|
|
|
25
|
-
/** @type {Array} */
|
|
26
|
-
[kOnClosed] =
|
|
25
|
+
/** @type {Array<Function>|null} */
|
|
26
|
+
[kOnClosed] = null
|
|
27
27
|
|
|
28
28
|
/** @returns {boolean} */
|
|
29
29
|
get destroyed () {
|
|
@@ -49,7 +49,8 @@ class DispatcherBase extends Dispatcher {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (this[kDestroyed]) {
|
|
52
|
-
|
|
52
|
+
const err = new ClientDestroyedError()
|
|
53
|
+
queueMicrotask(() => callback(err, null))
|
|
53
54
|
return
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -63,6 +64,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
this[kClosed] = true
|
|
67
|
+
this[kOnClosed] ??= []
|
|
66
68
|
this[kOnClosed].push(callback)
|
|
67
69
|
|
|
68
70
|
const onClosed = () => {
|
|
@@ -76,9 +78,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
76
78
|
// Should not error.
|
|
77
79
|
this[kClose]()
|
|
78
80
|
.then(() => this.destroy())
|
|
79
|
-
.then(() =>
|
|
80
|
-
queueMicrotask(onClosed)
|
|
81
|
-
})
|
|
81
|
+
.then(() => queueMicrotask(onClosed))
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
destroy (err, callback) {
|
|
@@ -90,7 +90,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
90
90
|
if (callback === undefined) {
|
|
91
91
|
return new Promise((resolve, reject) => {
|
|
92
92
|
this.destroy(err, (err, data) => {
|
|
93
|
-
return err ?
|
|
93
|
+
return err ? reject(err) : resolve(data)
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
96
|
}
|
|
@@ -113,7 +113,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
this[kDestroyed] = true
|
|
116
|
-
this[kOnDestroyed]
|
|
116
|
+
this[kOnDestroyed] ??= []
|
|
117
117
|
this[kOnDestroyed].push(callback)
|
|
118
118
|
|
|
119
119
|
const onDestroyed = () => {
|
|
@@ -125,9 +125,8 @@ class DispatcherBase extends Dispatcher {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Should not error.
|
|
128
|
-
this[kDestroy](err)
|
|
129
|
-
queueMicrotask(onDestroyed)
|
|
130
|
-
})
|
|
128
|
+
this[kDestroy](err)
|
|
129
|
+
.then(() => queueMicrotask(onDestroyed))
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
dispatch (opts, handler) {
|