undici 7.15.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 +48 -2
- package/docs/docs/api/Agent.md +1 -0
- 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/Errors.md +0 -1
- 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-fetch.js +2 -2
- package/index.js +8 -9
- package/lib/api/api-request.js +22 -8
- package/lib/api/api-upgrade.js +2 -1
- package/lib/api/readable.js +7 -5
- package/lib/core/connect.js +4 -1
- package/lib/core/diagnostics.js +28 -1
- package/lib/core/errors.js +217 -13
- package/lib/core/request.js +5 -1
- package/lib/core/symbols.js +3 -0
- package/lib/core/util.js +61 -41
- package/lib/dispatcher/agent.js +19 -7
- package/lib/dispatcher/balanced-pool.js +10 -0
- package/lib/dispatcher/client-h1.js +18 -23
- package/lib/dispatcher/client-h2.js +166 -26
- package/lib/dispatcher/client.js +64 -59
- package/lib/dispatcher/dispatcher-base.js +20 -16
- package/lib/dispatcher/env-http-proxy-agent.js +12 -16
- package/lib/dispatcher/fixed-queue.js +15 -39
- package/lib/dispatcher/h2c-client.js +7 -78
- package/lib/dispatcher/pool-base.js +60 -43
- package/lib/dispatcher/pool.js +2 -2
- package/lib/dispatcher/proxy-agent.js +27 -11
- package/lib/dispatcher/round-robin-pool.js +137 -0
- package/lib/encoding/index.js +33 -0
- package/lib/global.js +19 -1
- 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 +94 -15
- 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-agent.js +4 -4
- package/lib/mock/mock-errors.js +10 -0
- package/lib/mock/mock-utils.js +13 -12
- 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/date.js +534 -140
- package/lib/util/runtime-features.js +124 -0
- package/lib/web/cookies/index.js +1 -1
- package/lib/web/cookies/parse.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +2 -2
- package/lib/web/eventsource/eventsource.js +34 -29
- package/lib/web/eventsource/util.js +1 -9
- package/lib/web/fetch/body.js +45 -61
- package/lib/web/fetch/data-url.js +12 -160
- package/lib/web/fetch/formdata-parser.js +204 -127
- package/lib/web/fetch/index.js +21 -19
- package/lib/web/fetch/request.js +6 -0
- package/lib/web/fetch/response.js +4 -7
- package/lib/web/fetch/util.js +10 -79
- package/lib/web/infra/index.js +229 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
- package/lib/web/webidl/index.js +207 -44
- package/lib/web/websocket/connection.js +33 -22
- package/lib/web/websocket/events.js +1 -1
- package/lib/web/websocket/frame.js +9 -15
- package/lib/web/websocket/stream/websocketerror.js +22 -1
- package/lib/web/websocket/stream/websocketstream.js +17 -8
- package/lib/web/websocket/util.js +2 -1
- package/lib/web/websocket/websocket.js +32 -42
- package/package.json +9 -7
- package/types/agent.d.ts +2 -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/errors.d.ts +5 -15
- 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/webidl.d.ts +82 -21
- package/types/websocket.d.ts +9 -9
package/lib/dispatcher/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { InvalidArgumentError } = require('../core/errors')
|
|
3
|
+
const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
|
|
4
4
|
const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
5
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
6
6
|
const Pool = require('./pool')
|
|
@@ -13,6 +13,7 @@ const kOnConnectionError = Symbol('onConnectionError')
|
|
|
13
13
|
const kOnDrain = Symbol('onDrain')
|
|
14
14
|
const kFactory = Symbol('factory')
|
|
15
15
|
const kOptions = Symbol('options')
|
|
16
|
+
const kOrigins = Symbol('origins')
|
|
16
17
|
|
|
17
18
|
function defaultFactory (origin, opts) {
|
|
18
19
|
return opts && opts.connections === 1
|
|
@@ -21,7 +22,7 @@ function defaultFactory (origin, opts) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
class Agent extends DispatcherBase {
|
|
24
|
-
constructor ({ factory = defaultFactory, connect, ...options } = {}) {
|
|
25
|
+
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
|
|
25
26
|
if (typeof factory !== 'function') {
|
|
26
27
|
throw new InvalidArgumentError('factory must be a function.')
|
|
27
28
|
}
|
|
@@ -30,15 +31,20 @@ class Agent extends DispatcherBase {
|
|
|
30
31
|
throw new InvalidArgumentError('connect must be a function or an object')
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) {
|
|
35
|
+
throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
super()
|
|
34
39
|
|
|
35
40
|
if (connect && typeof connect !== 'function') {
|
|
36
41
|
connect = { ...connect }
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
this[kOptions] = { ...util.deepClone(options), connect }
|
|
44
|
+
this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
|
|
40
45
|
this[kFactory] = factory
|
|
41
46
|
this[kClients] = new Map()
|
|
47
|
+
this[kOrigins] = new Set()
|
|
42
48
|
|
|
43
49
|
this[kOnDrain] = (origin, targets) => {
|
|
44
50
|
this.emit('drain', origin, [this, ...targets])
|
|
@@ -73,6 +79,10 @@ class Agent extends DispatcherBase {
|
|
|
73
79
|
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) {
|
|
83
|
+
throw new MaxOriginsReachedError()
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
const result = this[kClients].get(key)
|
|
77
87
|
let dispatcher = result && result.dispatcher
|
|
78
88
|
if (!dispatcher) {
|
|
@@ -84,6 +94,7 @@ class Agent extends DispatcherBase {
|
|
|
84
94
|
this[kClients].delete(key)
|
|
85
95
|
result.dispatcher.close()
|
|
86
96
|
}
|
|
97
|
+
this[kOrigins].delete(key)
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
dispatcher = this[kFactory](opts.origin, this[kOptions])
|
|
@@ -105,29 +116,30 @@ class Agent extends DispatcherBase {
|
|
|
105
116
|
})
|
|
106
117
|
|
|
107
118
|
this[kClients].set(key, { count: 0, dispatcher })
|
|
119
|
+
this[kOrigins].add(key)
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
return dispatcher.dispatch(opts, handler)
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
[kClose] () {
|
|
114
126
|
const closePromises = []
|
|
115
127
|
for (const { dispatcher } of this[kClients].values()) {
|
|
116
128
|
closePromises.push(dispatcher.close())
|
|
117
129
|
}
|
|
118
130
|
this[kClients].clear()
|
|
119
131
|
|
|
120
|
-
|
|
132
|
+
return Promise.all(closePromises)
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
|
|
135
|
+
[kDestroy] (err) {
|
|
124
136
|
const destroyPromises = []
|
|
125
137
|
for (const { dispatcher } of this[kClients].values()) {
|
|
126
138
|
destroyPromises.push(dispatcher.destroy(err))
|
|
127
139
|
}
|
|
128
140
|
this[kClients].clear()
|
|
129
141
|
|
|
130
|
-
|
|
142
|
+
return Promise.all(destroyPromises)
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
get stats () {
|
|
@@ -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)
|
|
@@ -64,11 +64,24 @@ function lazyllhttp () {
|
|
|
64
64
|
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
|
|
65
65
|
|
|
66
66
|
let mod
|
|
67
|
-
try {
|
|
68
|
-
mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
|
|
69
|
-
} catch {
|
|
70
|
-
/* istanbul ignore next */
|
|
71
67
|
|
|
68
|
+
// We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
|
|
69
|
+
let useWasmSIMD = process.arch !== 'ppc64'
|
|
70
|
+
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
|
|
71
|
+
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
|
|
72
|
+
useWasmSIMD = true
|
|
73
|
+
} else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
|
|
74
|
+
useWasmSIMD = false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (useWasmSIMD) {
|
|
78
|
+
try {
|
|
79
|
+
mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!mod) {
|
|
72
85
|
// We could check if the error was caused by the simd option not
|
|
73
86
|
// being enabled, but the occurring of this other error
|
|
74
87
|
// * https://github.com/emscripten-core/emscripten/issues/11495
|
|
@@ -85,7 +98,6 @@ function lazyllhttp () {
|
|
|
85
98
|
* @returns {number}
|
|
86
99
|
*/
|
|
87
100
|
wasm_on_url: (p, at, len) => {
|
|
88
|
-
/* istanbul ignore next */
|
|
89
101
|
return 0
|
|
90
102
|
},
|
|
91
103
|
/**
|
|
@@ -250,7 +262,6 @@ class Parser {
|
|
|
250
262
|
|
|
251
263
|
this.timeoutValue = delay
|
|
252
264
|
} else if (this.timeout) {
|
|
253
|
-
// istanbul ignore else: only for jest
|
|
254
265
|
if (this.timeout.refresh) {
|
|
255
266
|
this.timeout.refresh()
|
|
256
267
|
}
|
|
@@ -271,7 +282,6 @@ class Parser {
|
|
|
271
282
|
|
|
272
283
|
assert(this.timeoutType === TIMEOUT_BODY)
|
|
273
284
|
if (this.timeout) {
|
|
274
|
-
// istanbul ignore else: only for jest
|
|
275
285
|
if (this.timeout.refresh) {
|
|
276
286
|
this.timeout.refresh()
|
|
277
287
|
}
|
|
@@ -325,10 +335,6 @@ class Parser {
|
|
|
325
335
|
currentBufferRef = chunk
|
|
326
336
|
currentParser = this
|
|
327
337
|
ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length)
|
|
328
|
-
/* eslint-disable-next-line no-useless-catch */
|
|
329
|
-
} catch (err) {
|
|
330
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
331
|
-
throw err
|
|
332
338
|
} finally {
|
|
333
339
|
currentParser = null
|
|
334
340
|
currentBufferRef = null
|
|
@@ -345,7 +351,6 @@ class Parser {
|
|
|
345
351
|
} else {
|
|
346
352
|
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
|
347
353
|
let message = ''
|
|
348
|
-
/* istanbul ignore else: difficult to make a test case for */
|
|
349
354
|
if (ptr) {
|
|
350
355
|
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
351
356
|
message =
|
|
@@ -391,7 +396,6 @@ class Parser {
|
|
|
391
396
|
onMessageBegin () {
|
|
392
397
|
const { socket, client } = this
|
|
393
398
|
|
|
394
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
395
399
|
if (socket.destroyed) {
|
|
396
400
|
return -1
|
|
397
401
|
}
|
|
@@ -520,14 +524,12 @@ class Parser {
|
|
|
520
524
|
onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
|
|
521
525
|
const { client, socket, headers, statusText } = this
|
|
522
526
|
|
|
523
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
524
527
|
if (socket.destroyed) {
|
|
525
528
|
return -1
|
|
526
529
|
}
|
|
527
530
|
|
|
528
531
|
const request = client[kQueue][client[kRunningIdx]]
|
|
529
532
|
|
|
530
|
-
/* istanbul ignore next: difficult to make a test case for */
|
|
531
533
|
if (!request) {
|
|
532
534
|
return -1
|
|
533
535
|
}
|
|
@@ -561,7 +563,6 @@ class Parser {
|
|
|
561
563
|
: client[kBodyTimeout]
|
|
562
564
|
this.setTimeout(bodyTimeout, TIMEOUT_BODY)
|
|
563
565
|
} else if (this.timeout) {
|
|
564
|
-
// istanbul ignore else: only for jest
|
|
565
566
|
if (this.timeout.refresh) {
|
|
566
567
|
this.timeout.refresh()
|
|
567
568
|
}
|
|
@@ -642,7 +643,6 @@ class Parser {
|
|
|
642
643
|
|
|
643
644
|
assert(this.timeoutType === TIMEOUT_BODY)
|
|
644
645
|
if (this.timeout) {
|
|
645
|
-
// istanbul ignore else: only for jest
|
|
646
646
|
if (this.timeout.refresh) {
|
|
647
647
|
this.timeout.refresh()
|
|
648
648
|
}
|
|
@@ -698,7 +698,6 @@ class Parser {
|
|
|
698
698
|
return 0
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
/* istanbul ignore next: should be handled by llhttp? */
|
|
702
701
|
if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
|
|
703
702
|
util.destroy(socket, new ResponseContentLengthMismatchError())
|
|
704
703
|
return -1
|
|
@@ -739,7 +738,6 @@ class Parser {
|
|
|
739
738
|
function onParserTimeout (parser) {
|
|
740
739
|
const { socket, timeoutType, client, paused } = parser.deref()
|
|
741
740
|
|
|
742
|
-
/* istanbul ignore else */
|
|
743
741
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
744
742
|
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
745
743
|
assert(!paused, 'cannot be paused while waiting for headers')
|
|
@@ -760,7 +758,7 @@ function onParserTimeout (parser) {
|
|
|
760
758
|
* @param {import('net').Socket} socket
|
|
761
759
|
* @returns
|
|
762
760
|
*/
|
|
763
|
-
|
|
761
|
+
function connectH1 (client, socket) {
|
|
764
762
|
client[kSocket] = socket
|
|
765
763
|
|
|
766
764
|
if (!llhttpInstance) {
|
|
@@ -1146,7 +1144,6 @@ function writeH1 (client, request) {
|
|
|
1146
1144
|
channels.sendHeaders.publish({ request, headers: header, socket })
|
|
1147
1145
|
}
|
|
1148
1146
|
|
|
1149
|
-
/* istanbul ignore else: assertion */
|
|
1150
1147
|
if (!body || bodyLength === 0) {
|
|
1151
1148
|
writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload)
|
|
1152
1149
|
} else if (util.isBuffer(body)) {
|
|
@@ -1527,7 +1524,6 @@ class AsyncWriter {
|
|
|
1527
1524
|
|
|
1528
1525
|
if (!ret) {
|
|
1529
1526
|
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1530
|
-
// istanbul ignore else: only for jest
|
|
1531
1527
|
if (socket[kParser].timeout.refresh) {
|
|
1532
1528
|
socket[kParser].timeout.refresh()
|
|
1533
1529
|
}
|
|
@@ -1578,7 +1574,6 @@ class AsyncWriter {
|
|
|
1578
1574
|
}
|
|
1579
1575
|
|
|
1580
1576
|
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1581
|
-
// istanbul ignore else: only for jest
|
|
1582
1577
|
if (socket[kParser].timeout.refresh) {
|
|
1583
1578
|
socket[kParser].timeout.refresh()
|
|
1584
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
|
|
|
@@ -77,7 +84,7 @@ function parseH2Headers (headers) {
|
|
|
77
84
|
return result
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
function connectH2 (client, socket) {
|
|
81
88
|
client[kSocket] = socket
|
|
82
89
|
|
|
83
90
|
const session = http2.connect(client[kUrl], {
|
|
@@ -93,12 +100,21 @@ async 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 @@ async 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 @@ async 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
|
|
|
@@ -279,11 +360,11 @@ function shouldSendContentLength (method) {
|
|
|
279
360
|
function writeH2 (client, request) {
|
|
280
361
|
const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
|
|
281
362
|
const session = client[kHTTP2Session]
|
|
282
|
-
const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
|
|
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
|
|
|
@@ -292,6 +373,16 @@ function writeH2 (client, request) {
|
|
|
292
373
|
const key = reqHeaders[n + 0]
|
|
293
374
|
const val = reqHeaders[n + 1]
|
|
294
375
|
|
|
376
|
+
if (key === 'cookie') {
|
|
377
|
+
if (headers[key] != null) {
|
|
378
|
+
headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
|
|
379
|
+
} else {
|
|
380
|
+
headers[key] = val
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
|
|
295
386
|
if (Array.isArray(val)) {
|
|
296
387
|
for (let i = 0; i < val.length; i++) {
|
|
297
388
|
if (headers[key]) {
|
|
@@ -354,26 +445,75 @@ function writeH2 (client, request) {
|
|
|
354
445
|
return false
|
|
355
446
|
}
|
|
356
447
|
|
|
357
|
-
if (method === 'CONNECT') {
|
|
448
|
+
if (upgrade || method === 'CONNECT') {
|
|
358
449
|
session.ref()
|
|
359
|
-
|
|
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
|
|
360
505
|
// will create a new stream. We trigger a request to create the stream and wait until
|
|
361
506
|
// `ready` event is triggered
|
|
362
507
|
// We disabled endStream to allow the user to write to the stream
|
|
363
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
|
|
364
512
|
|
|
365
|
-
|
|
366
|
-
request.onUpgrade(null, null, stream)
|
|
513
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
367
514
|
++session[kOpenStreams]
|
|
368
515
|
client[kQueue][client[kRunningIdx]++] = null
|
|
369
|
-
}
|
|
370
|
-
stream.once('ready', () => {
|
|
371
|
-
request.onUpgrade(null, null, stream)
|
|
372
|
-
++session[kOpenStreams]
|
|
373
|
-
client[kQueue][client[kRunningIdx]++] = null
|
|
374
|
-
})
|
|
375
|
-
}
|
|
376
|
-
|
|
516
|
+
})
|
|
377
517
|
stream.once('close', () => {
|
|
378
518
|
session[kOpenStreams] -= 1
|
|
379
519
|
if (session[kOpenStreams] === 0) session.unref()
|
|
@@ -385,9 +525,8 @@ function writeH2 (client, request) {
|
|
|
385
525
|
|
|
386
526
|
// https://tools.ietf.org/html/rfc7540#section-8.3
|
|
387
527
|
// :path and :scheme headers must be omitted when sending CONNECT
|
|
388
|
-
|
|
389
528
|
headers[HTTP2_HEADER_PATH] = path
|
|
390
|
-
headers[HTTP2_HEADER_SCHEME] = 'https'
|
|
529
|
+
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
|
|
391
530
|
|
|
392
531
|
// https://tools.ietf.org/html/rfc7231#section-4.3.1
|
|
393
532
|
// https://tools.ietf.org/html/rfc7231#section-4.3.2
|
|
@@ -425,12 +564,12 @@ function writeH2 (client, request) {
|
|
|
425
564
|
contentLength = request.contentLength
|
|
426
565
|
}
|
|
427
566
|
|
|
428
|
-
if (
|
|
567
|
+
if (!expectsPayload) {
|
|
429
568
|
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
430
569
|
// A user agent SHOULD NOT send a Content-Length header field when
|
|
431
570
|
// the request message does not contain a payload body and the method
|
|
432
571
|
// semantics do not anticipate such a body.
|
|
433
|
-
|
|
572
|
+
// And for methods that don't expect a payload, omit Content-Length.
|
|
434
573
|
contentLength = null
|
|
435
574
|
}
|
|
436
575
|
|
|
@@ -446,7 +585,7 @@ function writeH2 (client, request) {
|
|
|
446
585
|
}
|
|
447
586
|
|
|
448
587
|
if (contentLength != null) {
|
|
449
|
-
assert(body, 'no body must not have content length')
|
|
588
|
+
assert(body || contentLength === 0, 'no body must not have content length')
|
|
450
589
|
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
|
|
451
590
|
}
|
|
452
591
|
|
|
@@ -465,6 +604,7 @@ function writeH2 (client, request) {
|
|
|
465
604
|
if (expectContinue) {
|
|
466
605
|
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
|
467
606
|
stream = session.request(headers, { endStream: shouldEndStream, signal })
|
|
607
|
+
stream[kHTTP2Stream] = true
|
|
468
608
|
|
|
469
609
|
stream.once('continue', writeBodyH2)
|
|
470
610
|
} else {
|
|
@@ -472,6 +612,7 @@ function writeH2 (client, request) {
|
|
|
472
612
|
endStream: shouldEndStream,
|
|
473
613
|
signal
|
|
474
614
|
})
|
|
615
|
+
stream[kHTTP2Stream] = true
|
|
475
616
|
|
|
476
617
|
writeBodyH2()
|
|
477
618
|
}
|
|
@@ -580,7 +721,6 @@ function writeH2 (client, request) {
|
|
|
580
721
|
return true
|
|
581
722
|
|
|
582
723
|
function writeBodyH2 () {
|
|
583
|
-
/* istanbul ignore else: assertion */
|
|
584
724
|
if (!body || contentLength === 0) {
|
|
585
725
|
writeBuffer(
|
|
586
726
|
abort,
|