undici 8.4.1 → 8.5.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/Client.md +1 -0
- package/docs/docs/api/Cookies.md +27 -0
- package/docs/docs/api/Dispatcher.md +10 -1
- package/docs/docs/api/Socks5ProxyAgent.md +1 -0
- package/lib/dispatcher/client-h1.js +69 -1
- package/lib/dispatcher/client-h2.js +84 -12
- package/lib/dispatcher/client.js +6 -2
- package/lib/dispatcher/dispatcher-base.js +1 -0
- package/lib/dispatcher/proxy-agent.js +2 -1
- package/lib/dispatcher/socks5-proxy-agent.js +4 -2
- package/lib/util/cache.js +8 -2
- package/lib/web/cookies/parse.js +17 -25
- package/lib/web/eventsource/eventsource.js +7 -18
- package/lib/web/eventsource/util.js +32 -1
- package/lib/web/fetch/body.js +43 -0
- package/lib/web/fetch/request.js +1 -0
- package/lib/web/websocket/receiver.js +20 -3
- package/lib/web/websocket/stream/websocketstream.js +8 -1
- package/lib/web/websocket/websocket.js +3 -1
- package/package.json +1 -1
- package/types/client.d.ts +5 -0
- package/types/fetch.d.ts +1 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -25,6 +25,7 @@ Returns: `Client`
|
|
|
25
25
|
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
|
26
26
|
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
27
27
|
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
28
|
+
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
|
28
29
|
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
|
29
30
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. This option has no effect once HTTP/2 is negotiated — see `maxConcurrentStreams` for the h2 dispatch ceiling.
|
|
30
31
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null` - Configures how undici establishes TCP/TLS connections. Accepts two forms:
|
package/docs/docs/api/Cookies.md
CHANGED
|
@@ -80,6 +80,33 @@ Arguments:
|
|
|
80
80
|
|
|
81
81
|
Returns: `Cookie[]`
|
|
82
82
|
|
|
83
|
+
## `parseCookie(cookie)`
|
|
84
|
+
|
|
85
|
+
Parses a single `Set-Cookie` header value into a `Cookie` object.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { parseCookie } from 'undici'
|
|
89
|
+
|
|
90
|
+
console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax'))
|
|
91
|
+
// {
|
|
92
|
+
// name: 'undici',
|
|
93
|
+
// value: 'getSetCookies',
|
|
94
|
+
// secure: true,
|
|
95
|
+
// sameSite: 'Lax'
|
|
96
|
+
// }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Notes:
|
|
100
|
+
|
|
101
|
+
* The cookie value is returned as it appears in the header. Percent-encoded sequences such as `%20` or `%0D%0A` are **not** decoded.
|
|
102
|
+
* `sameSite` is only set for exact case-insensitive matches of `Strict`, `Lax`, or `None`.
|
|
103
|
+
|
|
104
|
+
Arguments:
|
|
105
|
+
|
|
106
|
+
* **cookie** `string`
|
|
107
|
+
|
|
108
|
+
Returns: `Cookie | null`
|
|
109
|
+
|
|
83
110
|
## `setCookie(headers, cookie)`
|
|
84
111
|
|
|
85
112
|
Appends a cookie to the `Set-Cookie` header.
|
|
@@ -1376,14 +1376,23 @@ When using the array header format (`string[]`), Undici processes only indexed e
|
|
|
1376
1376
|
|
|
1377
1377
|
Response headers will derive a `host` from the `url` of the [Client](/docs/docs/api/Client.md#class-client) instance if no `host` header was previously specified.
|
|
1378
1378
|
|
|
1379
|
+
### Request header validation
|
|
1380
|
+
|
|
1381
|
+
Request headers that are managed by the HTTP connection are handled differently from ordinary headers:
|
|
1382
|
+
|
|
1383
|
+
* `transfer-encoding`, `keep-alive`, and `upgrade` cannot be set through `options.headers`; Undici throws an `InvalidArgumentError`.
|
|
1384
|
+
* `expect` is not supported; Undici throws a `NotSupportedError`.
|
|
1385
|
+
* `connection` must be a string containing comma-separated valid HTTP tokens. Undici rejects malformed tokens with `InvalidArgumentError: invalid connection header` and uses the `close` token to request connection reset behavior.
|
|
1386
|
+
* `host` and `content-length` are tracked separately from the raw header list. Duplicate `host` or `content-length` values are rejected, and `content-length` must contain only decimal digits.
|
|
1387
|
+
|
|
1379
1388
|
### Example 1 - Object
|
|
1380
1389
|
|
|
1381
1390
|
```js
|
|
1382
1391
|
{
|
|
1383
1392
|
'content-length': '123',
|
|
1384
1393
|
'content-type': 'text/plain',
|
|
1385
|
-
connection: 'keep-alive',
|
|
1386
1394
|
host: 'mysite.com',
|
|
1395
|
+
'accept-language': 'en',
|
|
1387
1396
|
accept: '*/*'
|
|
1388
1397
|
}
|
|
1389
1398
|
```
|
|
@@ -22,6 +22,7 @@ Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
|
|
|
22
22
|
* **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL.
|
|
23
23
|
* **connect** `Function` (optional) - Custom connector function for the proxy connection.
|
|
24
24
|
* **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS).
|
|
25
|
+
* **requestTls** `BuildOptions` (optional) - TLS options applied to the HTTPS connection to the target server through the SOCKS5 tunnel. Use this to configure `ca`, `cert`, `key`, `rejectUnauthorized`, `servername`, etc. for the target HTTPS endpoint.
|
|
25
26
|
|
|
26
27
|
Examples:
|
|
27
28
|
|
|
@@ -57,6 +57,9 @@ const constants = require('../llhttp/constants.js')
|
|
|
57
57
|
const EMPTY_BUF = Buffer.alloc(0)
|
|
58
58
|
const FastBuffer = Buffer[Symbol.species]
|
|
59
59
|
const removeAllListeners = util.removeAllListeners
|
|
60
|
+
const kIdleSocketValidation = Symbol('kIdleSocketValidation')
|
|
61
|
+
const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
|
|
62
|
+
const kSocketUsed = Symbol('kSocketUsed')
|
|
60
63
|
|
|
61
64
|
let extractBody
|
|
62
65
|
|
|
@@ -449,6 +452,11 @@ class Parser {
|
|
|
449
452
|
return -1
|
|
450
453
|
}
|
|
451
454
|
|
|
455
|
+
if (client[kRunning] === 0) {
|
|
456
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
457
|
+
return -1
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
const request = client[kQueue][client[kRunningIdx]]
|
|
453
461
|
if (!request) {
|
|
454
462
|
return -1
|
|
@@ -584,6 +592,11 @@ class Parser {
|
|
|
584
592
|
return -1
|
|
585
593
|
}
|
|
586
594
|
|
|
595
|
+
if (client[kRunning] === 0) {
|
|
596
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
597
|
+
return -1
|
|
598
|
+
}
|
|
599
|
+
|
|
587
600
|
const request = client[kQueue][client[kRunningIdx]]
|
|
588
601
|
|
|
589
602
|
if (!request) {
|
|
@@ -762,6 +775,7 @@ class Parser {
|
|
|
762
775
|
request.onResponseEnd(headers)
|
|
763
776
|
|
|
764
777
|
client[kQueue][client[kRunningIdx]++] = null
|
|
778
|
+
socket[kSocketUsed] = client[kPending] === 0
|
|
765
779
|
|
|
766
780
|
if (socket[kWriting]) {
|
|
767
781
|
assert(client[kRunning] === 0)
|
|
@@ -838,6 +852,9 @@ function connectH1 (client, socket) {
|
|
|
838
852
|
socket[kWriting] = false
|
|
839
853
|
socket[kReset] = false
|
|
840
854
|
socket[kBlocking] = false
|
|
855
|
+
socket[kIdleSocketValidation] = 0
|
|
856
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
857
|
+
socket[kSocketUsed] = false
|
|
841
858
|
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
|
842
859
|
|
|
843
860
|
util.addListener(socket, 'error', onHttpSocketError)
|
|
@@ -880,7 +897,7 @@ function connectH1 (client, socket) {
|
|
|
880
897
|
* @returns {boolean}
|
|
881
898
|
*/
|
|
882
899
|
busy (request) {
|
|
883
|
-
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
|
|
900
|
+
if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
|
|
884
901
|
return true
|
|
885
902
|
}
|
|
886
903
|
|
|
@@ -960,6 +977,8 @@ function onHttpSocketEnd () {
|
|
|
960
977
|
function onHttpSocketClose () {
|
|
961
978
|
const parser = this[kParser]
|
|
962
979
|
|
|
980
|
+
clearIdleSocketValidation(this)
|
|
981
|
+
|
|
963
982
|
if (parser) {
|
|
964
983
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
965
984
|
this[kError] = parser.finish() || this[kError]
|
|
@@ -1006,6 +1025,28 @@ function onSocketClose () {
|
|
|
1006
1025
|
this[kClosed] = true
|
|
1007
1026
|
}
|
|
1008
1027
|
|
|
1028
|
+
function clearIdleSocketValidation (socket) {
|
|
1029
|
+
if (socket[kIdleSocketValidationTimeout]) {
|
|
1030
|
+
clearTimeout(socket[kIdleSocketValidationTimeout])
|
|
1031
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
socket[kIdleSocketValidation] = 0
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function scheduleIdleSocketValidation (client, socket) {
|
|
1038
|
+
socket[kIdleSocketValidation] = 1
|
|
1039
|
+
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
|
1040
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
1041
|
+
socket[kIdleSocketValidation] = 2
|
|
1042
|
+
|
|
1043
|
+
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1044
|
+
client[kResume]()
|
|
1045
|
+
}
|
|
1046
|
+
}, 0)
|
|
1047
|
+
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1009
1050
|
/**
|
|
1010
1051
|
* @param {import('./client.js')} client
|
|
1011
1052
|
*/
|
|
@@ -1023,6 +1064,32 @@ function resumeH1 (client) {
|
|
|
1023
1064
|
socket[kNoRef] = false
|
|
1024
1065
|
}
|
|
1025
1066
|
|
|
1067
|
+
if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
|
|
1068
|
+
if (socket[kIdleSocketValidation] === 0) {
|
|
1069
|
+
scheduleIdleSocketValidation(client, socket)
|
|
1070
|
+
socket[kParser].readMore()
|
|
1071
|
+
if (socket.destroyed) {
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
return
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (socket[kIdleSocketValidation] === 1) {
|
|
1078
|
+
socket[kParser].readMore()
|
|
1079
|
+
if (socket.destroyed) {
|
|
1080
|
+
return
|
|
1081
|
+
}
|
|
1082
|
+
return
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (client[kRunning] === 0) {
|
|
1087
|
+
socket[kParser].readMore()
|
|
1088
|
+
if (socket.destroyed) {
|
|
1089
|
+
return
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1026
1093
|
if (client[kSize] === 0) {
|
|
1027
1094
|
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
|
1028
1095
|
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
|
@@ -1121,6 +1188,7 @@ function writeH1 (client, request) {
|
|
|
1121
1188
|
}
|
|
1122
1189
|
|
|
1123
1190
|
const socket = client[kSocket]
|
|
1191
|
+
clearIdleSocketValidation(socket)
|
|
1124
1192
|
|
|
1125
1193
|
/**
|
|
1126
1194
|
* @param {Error} [err]
|
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
kSize,
|
|
36
36
|
kHTTPContext,
|
|
37
37
|
kClosed,
|
|
38
|
+
kKeepAliveDefaultTimeout,
|
|
38
39
|
kHeadersTimeout,
|
|
39
40
|
kBodyTimeout,
|
|
40
41
|
kEnableConnectProtocol,
|
|
@@ -152,6 +153,21 @@ function requeueUnsentRequest (client, request) {
|
|
|
152
153
|
client[kQueue].splice(client[kPendingIdx] + 1, 0, request)
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
function completeRequest (client, request, resetPendingIdx = false) {
|
|
157
|
+
const index = client[kQueue].indexOf(request, client[kRunningIdx])
|
|
158
|
+
|
|
159
|
+
if (index === -1 || index >= client[kPendingIdx]) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
client[kQueue].splice(index, 1)
|
|
164
|
+
client[kPendingIdx]--
|
|
165
|
+
|
|
166
|
+
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
|
167
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
function canRetryRequestAfterGoAway (request) {
|
|
156
172
|
const { body } = request
|
|
157
173
|
|
|
@@ -191,6 +207,7 @@ function connectH2 (client, socket) {
|
|
|
191
207
|
session[kClient] = client
|
|
192
208
|
session[kSocket] = socket
|
|
193
209
|
session[kHTTP2SessionState] = {
|
|
210
|
+
idleTimeout: null,
|
|
194
211
|
ping: {
|
|
195
212
|
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
|
|
196
213
|
}
|
|
@@ -279,10 +296,10 @@ function connectH2 (client, socket) {
|
|
|
279
296
|
if (client[kRunning] > 0) {
|
|
280
297
|
// We are already processing requests
|
|
281
298
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
299
|
+
// Unlike HTTP/1.1 pipelining, HTTP/2 multiplexes requests on
|
|
300
|
+
// independent streams, so non-idempotent requests can be dispatched
|
|
301
|
+
// concurrently. Retry eligibility is handled by stream/session error
|
|
302
|
+
// handling instead of by serializing all non-idempotent requests.
|
|
286
303
|
// Don't dispatch an upgrade until all preceding requests have completed.
|
|
287
304
|
// Possibly, we do not have remote settings confirmed yet.
|
|
288
305
|
if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
|
|
@@ -308,18 +325,68 @@ function connectH2 (client, socket) {
|
|
|
308
325
|
|
|
309
326
|
function resumeH2 (client) {
|
|
310
327
|
const socket = client[kSocket]
|
|
328
|
+
const session = client[kHTTP2Session]
|
|
311
329
|
|
|
312
330
|
if (socket?.destroyed === false) {
|
|
313
331
|
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
|
|
314
332
|
socket.unref()
|
|
315
|
-
|
|
333
|
+
session.unref()
|
|
316
334
|
} else {
|
|
317
335
|
socket.ref()
|
|
318
|
-
|
|
336
|
+
session.ref()
|
|
319
337
|
}
|
|
338
|
+
|
|
339
|
+
if (client[kSize] === 0 && session[kOpenStreams] === 0) {
|
|
340
|
+
setHttp2IdleTimeout(session)
|
|
341
|
+
} else {
|
|
342
|
+
clearHttp2IdleTimeout(session)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearHttp2IdleTimeout (session) {
|
|
348
|
+
const state = session[kHTTP2SessionState]
|
|
349
|
+
|
|
350
|
+
if (state?.idleTimeout != null) {
|
|
351
|
+
clearTimeout(state.idleTimeout)
|
|
352
|
+
state.idleTimeout = null
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function setHttp2IdleTimeout (session) {
|
|
357
|
+
const client = session[kClient]
|
|
358
|
+
|
|
359
|
+
if (client[kHTTP2Session] !== session || session.closed || session.destroyed) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (session[kOpenStreams] !== 0 || client[kSize] !== 0) {
|
|
364
|
+
clearHttp2IdleTimeout(session)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const state = session[kHTTP2SessionState]
|
|
369
|
+
if (state.idleTimeout == null) {
|
|
370
|
+
state.idleTimeout = setTimeout(onHttp2SessionIdleTimeout, client[kKeepAliveDefaultTimeout], session).unref()
|
|
320
371
|
}
|
|
321
372
|
}
|
|
322
373
|
|
|
374
|
+
function onHttp2SessionIdleTimeout (session) {
|
|
375
|
+
const client = session[kClient]
|
|
376
|
+
const socket = session[kSocket]
|
|
377
|
+
const state = session[kHTTP2SessionState]
|
|
378
|
+
|
|
379
|
+
state.idleTimeout = null
|
|
380
|
+
|
|
381
|
+
if (client[kHTTP2Session] !== session || session[kOpenStreams] !== 0 || client[kSize] !== 0 || session.closed || session.destroyed) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const err = new InformationalError('socket idle timeout')
|
|
386
|
+
socket[kError] = err
|
|
387
|
+
util.destroy(socket, err)
|
|
388
|
+
}
|
|
389
|
+
|
|
323
390
|
function applyConnectionWindowSize (connectionWindowSize) {
|
|
324
391
|
try {
|
|
325
392
|
if (typeof this.setLocalWindowSize === 'function') {
|
|
@@ -445,6 +512,8 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
445
512
|
client[kHTTP2Session] = null
|
|
446
513
|
}
|
|
447
514
|
|
|
515
|
+
clearHttp2IdleTimeout(this)
|
|
516
|
+
|
|
448
517
|
if (!this.closed && !this.destroyed) {
|
|
449
518
|
this.close()
|
|
450
519
|
}
|
|
@@ -467,6 +536,8 @@ function onHttp2SessionClose () {
|
|
|
467
536
|
client[kHTTP2Session] = null
|
|
468
537
|
}
|
|
469
538
|
|
|
539
|
+
clearHttp2IdleTimeout(this)
|
|
540
|
+
|
|
470
541
|
if (state.ping.interval != null) {
|
|
471
542
|
clearInterval(state.ping.interval)
|
|
472
543
|
state.ping.interval = null
|
|
@@ -479,7 +550,9 @@ function onHttp2SessionClose () {
|
|
|
479
550
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
480
551
|
for (let i = 0; i < requests.length; i++) {
|
|
481
552
|
const request = requests[i]
|
|
482
|
-
|
|
553
|
+
if (request != null) {
|
|
554
|
+
util.errorRequest(client, request, err)
|
|
555
|
+
}
|
|
483
556
|
}
|
|
484
557
|
}
|
|
485
558
|
}
|
|
@@ -542,6 +615,7 @@ function closeStreamSession (stream) {
|
|
|
542
615
|
session[kOpenStreams] -= 1
|
|
543
616
|
if (session[kOpenStreams] === 0) {
|
|
544
617
|
session.unref()
|
|
618
|
+
setHttp2IdleTimeout(session)
|
|
545
619
|
}
|
|
546
620
|
}
|
|
547
621
|
|
|
@@ -711,6 +785,7 @@ function setupUpgradeStream (stream, state) {
|
|
|
711
785
|
stream.on('timeout', onUpgradeStreamTimeout)
|
|
712
786
|
stream.once('close', onUpgradeStreamClose)
|
|
713
787
|
|
|
788
|
+
clearHttp2IdleTimeout(session)
|
|
714
789
|
++session[kOpenStreams]
|
|
715
790
|
stream.setTimeout(headersTimeout)
|
|
716
791
|
}
|
|
@@ -742,11 +817,7 @@ function writeH2 (client, request) {
|
|
|
742
817
|
}
|
|
743
818
|
|
|
744
819
|
requestFinalized = true
|
|
745
|
-
client
|
|
746
|
-
|
|
747
|
-
if (resetPendingIdx) {
|
|
748
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
749
|
-
}
|
|
820
|
+
completeRequest(client, request, resetPendingIdx)
|
|
750
821
|
|
|
751
822
|
client[kResume]()
|
|
752
823
|
}
|
|
@@ -983,6 +1054,7 @@ function writeH2 (client, request) {
|
|
|
983
1054
|
state.stream = stream
|
|
984
1055
|
|
|
985
1056
|
// Increment counter as we have new streams open
|
|
1057
|
+
clearHttp2IdleTimeout(session)
|
|
986
1058
|
++session[kOpenStreams]
|
|
987
1059
|
stream.setTimeout(headersTimeout)
|
|
988
1060
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -395,7 +395,9 @@ class Client extends DispatcherBase {
|
|
|
395
395
|
const requests = this[kQueue].splice(this[kPendingIdx])
|
|
396
396
|
for (let i = 0; i < requests.length; i++) {
|
|
397
397
|
const request = requests[i]
|
|
398
|
-
|
|
398
|
+
if (request != null) {
|
|
399
|
+
util.errorRequest(this, request, err)
|
|
400
|
+
}
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
const callback = () => {
|
|
@@ -434,7 +436,9 @@ function onError (client, err) {
|
|
|
434
436
|
|
|
435
437
|
for (let i = 0; i < requests.length; i++) {
|
|
436
438
|
const request = requests[i]
|
|
437
|
-
|
|
439
|
+
if (request != null) {
|
|
440
|
+
util.errorRequest(client, request, err)
|
|
441
|
+
}
|
|
438
442
|
}
|
|
439
443
|
assert(client[kSize] === 0)
|
|
440
444
|
}
|
|
@@ -147,7 +147,8 @@ class ProxyAgent extends DispatcherBase {
|
|
|
147
147
|
factory: agentFactory,
|
|
148
148
|
username: opts.username || username,
|
|
149
149
|
password: opts.password || password,
|
|
150
|
-
proxyTls: opts.proxyTls
|
|
150
|
+
proxyTls: opts.proxyTls,
|
|
151
|
+
requestTls: opts.requestTls
|
|
151
152
|
})
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -19,6 +19,7 @@ const kProxyAuth = Symbol('proxy auth')
|
|
|
19
19
|
const kProxyProtocol = Symbol('proxy protocol')
|
|
20
20
|
const kPools = Symbol('pools')
|
|
21
21
|
const kConnector = Symbol('connector')
|
|
22
|
+
const kRequestTls = Symbol('request tls settings')
|
|
22
23
|
|
|
23
24
|
// Static flag to ensure warning is only emitted once per process
|
|
24
25
|
let experimentalWarningEmitted = false
|
|
@@ -53,6 +54,7 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
53
54
|
this[kProxyUrl] = url
|
|
54
55
|
this[kProxyHeaders] = options.headers || {}
|
|
55
56
|
this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:'
|
|
57
|
+
this[kRequestTls] = options.requestTls
|
|
56
58
|
|
|
57
59
|
// Extract auth from URL or options
|
|
58
60
|
this[kProxyAuth] = {
|
|
@@ -205,9 +207,9 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
205
207
|
}
|
|
206
208
|
debug('upgrading to TLS')
|
|
207
209
|
finalSocket = tls.connect({
|
|
210
|
+
...this[kRequestTls],
|
|
208
211
|
socket,
|
|
209
|
-
servername: targetHost
|
|
210
|
-
...connectOpts.tls || {}
|
|
212
|
+
servername: this[kRequestTls]?.servername || targetHost
|
|
211
213
|
})
|
|
212
214
|
|
|
213
215
|
const tlsReady = Promise.withResolvers()
|
package/lib/util/cache.js
CHANGED
|
@@ -228,6 +228,10 @@ function parseCacheControlHeader (header) {
|
|
|
228
228
|
headers[headers.length - 1] = lastHeader
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
for (let j = 0; j < headers.length; j++) {
|
|
232
|
+
headers[j] = headers[j].trim()
|
|
233
|
+
}
|
|
234
|
+
|
|
231
235
|
if (key in output) {
|
|
232
236
|
output[key] = output[key].concat(headers)
|
|
233
237
|
} else {
|
|
@@ -236,10 +240,12 @@ function parseCacheControlHeader (header) {
|
|
|
236
240
|
}
|
|
237
241
|
} else {
|
|
238
242
|
// Something like `no-cache="some-header"`
|
|
243
|
+
const fieldName = value.trim()
|
|
244
|
+
|
|
239
245
|
if (key in output) {
|
|
240
|
-
output[key] = output[key].concat(
|
|
246
|
+
output[key] = output[key].concat(fieldName)
|
|
241
247
|
} else {
|
|
242
|
-
output[key] = [
|
|
248
|
+
output[key] = [fieldName]
|
|
243
249
|
}
|
|
244
250
|
}
|
|
245
251
|
|
package/lib/web/cookies/parse.js
CHANGED
|
@@ -4,7 +4,6 @@ const { collectASequenceOfCodePointsFast } = require('../infra')
|
|
|
4
4
|
const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
|
|
5
5
|
const { isCTLExcludingHtab } = require('./util')
|
|
6
6
|
const assert = require('node:assert')
|
|
7
|
-
const { unescape: qsUnescape } = require('node:querystring')
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* @description Parses the field-value attributes of a set-cookie header string.
|
|
@@ -82,7 +81,7 @@ function parseSetCookie (header) {
|
|
|
82
81
|
// store arbitrary data in a cookie-value SHOULD encode that data, for
|
|
83
82
|
// example, using Base64 [RFC4648].
|
|
84
83
|
return {
|
|
85
|
-
name, value
|
|
84
|
+
name, value, ...parseUnparsedAttributes(unparsedAttributes)
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
|
|
@@ -280,32 +279,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
|
|
|
280
279
|
// If the attribute-name case-insensitively matches the string
|
|
281
280
|
// "SameSite", the user agent MUST process the cookie-av as follows:
|
|
282
281
|
|
|
283
|
-
// 1. Let enforcement be "Default".
|
|
284
|
-
let enforcement = 'Default'
|
|
285
|
-
|
|
286
282
|
const attributeValueLowercase = attributeValue.toLowerCase()
|
|
287
|
-
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
|
288
|
-
// "None", set enforcement to "None".
|
|
289
|
-
if (attributeValueLowercase.includes('none')) {
|
|
290
|
-
enforcement = 'None'
|
|
291
|
-
}
|
|
292
283
|
|
|
293
|
-
//
|
|
294
|
-
// "
|
|
295
|
-
|
|
296
|
-
|
|
284
|
+
// 1. If cookie-av's attribute-value is a case-insensitive match for
|
|
285
|
+
// "None", append an attribute to the cookie-attribute-list with an
|
|
286
|
+
// attribute-name of "SameSite" and an attribute-value of "None".
|
|
287
|
+
if (attributeValueLowercase === 'none') {
|
|
288
|
+
cookieAttributeList.sameSite = 'None'
|
|
289
|
+
} else if (attributeValueLowercase === 'strict') {
|
|
290
|
+
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
|
291
|
+
// "Strict", append an attribute to the cookie-attribute-list with
|
|
292
|
+
// an attribute-name of "SameSite" and an attribute-value of
|
|
293
|
+
// "Strict".
|
|
294
|
+
cookieAttributeList.sameSite = 'Strict'
|
|
295
|
+
} else if (attributeValueLowercase === 'lax') {
|
|
296
|
+
// 3. If cookie-av's attribute-value is a case-insensitive match for
|
|
297
|
+
// "Lax", append an attribute to the cookie-attribute-list with an
|
|
298
|
+
// attribute-name of "SameSite" and an attribute-value of "Lax".
|
|
299
|
+
cookieAttributeList.sameSite = 'Lax'
|
|
297
300
|
}
|
|
298
|
-
|
|
299
|
-
// 4. If cookie-av's attribute-value is a case-insensitive match for
|
|
300
|
-
// "Lax", set enforcement to "Lax".
|
|
301
|
-
if (attributeValueLowercase.includes('lax')) {
|
|
302
|
-
enforcement = 'Lax'
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// 5. Append an attribute to the cookie-attribute-list with an
|
|
306
|
-
// attribute-name of "SameSite" and an attribute-value of
|
|
307
|
-
// enforcement.
|
|
308
|
-
cookieAttributeList.sameSite = enforcement
|
|
309
301
|
} else {
|
|
310
302
|
cookieAttributeList.unparsed ??= []
|
|
311
303
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { pipeline } = require('node:stream')
|
|
4
4
|
const { fetching } = require('../fetch')
|
|
5
|
-
const { makeRequest } = require('../fetch/request')
|
|
6
5
|
const { webidl } = require('../webidl')
|
|
7
6
|
const { EventSourceStream } = require('./eventsource-stream')
|
|
8
7
|
const { parseMIMEType } = require('../fetch/data-url')
|
|
@@ -10,6 +9,7 @@ const { createFastMessageEvent } = require('../websocket/events')
|
|
|
10
9
|
const { isNetworkError } = require('../fetch/response')
|
|
11
10
|
const { kEnumerableProperty } = require('../../core/util')
|
|
12
11
|
const { environmentSettingsObject } = require('../fetch/util')
|
|
12
|
+
const { createPotentialCORSRequest } = require('./util')
|
|
13
13
|
|
|
14
14
|
let experimentalWarned = false
|
|
15
15
|
|
|
@@ -160,33 +160,22 @@ class EventSource extends EventTarget {
|
|
|
160
160
|
|
|
161
161
|
// 8. Let request be the result of creating a potential-CORS request given
|
|
162
162
|
// urlRecord, the empty string, and corsAttributeState.
|
|
163
|
-
const
|
|
164
|
-
redirect: 'follow',
|
|
165
|
-
keepalive: true,
|
|
166
|
-
// @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
167
|
-
mode: 'cors',
|
|
168
|
-
credentials: corsAttributeState === 'anonymous'
|
|
169
|
-
? 'same-origin'
|
|
170
|
-
: 'omit',
|
|
171
|
-
referrer: 'no-referrer'
|
|
172
|
-
}
|
|
163
|
+
const request = createPotentialCORSRequest(urlRecord, '', corsAttributeState)
|
|
173
164
|
|
|
174
165
|
// 9. Set request's client to settings.
|
|
175
|
-
|
|
166
|
+
request.client = environmentSettingsObject.settingsObject
|
|
176
167
|
|
|
177
168
|
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
|
|
178
|
-
|
|
169
|
+
request.headersList.set('Accept', 'text/event-stream')
|
|
179
170
|
|
|
180
171
|
// 11. Set request's cache mode to "no-store".
|
|
181
|
-
|
|
172
|
+
request.cache = 'no-store'
|
|
182
173
|
|
|
183
174
|
// 12. Set request's initiator type to "other".
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
initRequest.urlList = [new URL(this.#url)]
|
|
175
|
+
request.initiator = 'other'
|
|
187
176
|
|
|
188
177
|
// 13. Set ev's request to request.
|
|
189
|
-
this.#request =
|
|
178
|
+
this.#request = request
|
|
190
179
|
|
|
191
180
|
this.#connect()
|
|
192
181
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { makeRequest } = require('../fetch/request')
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Checks if the given value is a valid LastEventId.
|
|
5
7
|
* @param {string} value
|
|
@@ -23,7 +25,36 @@ function isASCIINumber (value) {
|
|
|
23
25
|
return true
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function createPotentialCORSRequest (url, destination, corsAttributeState, sameOriginFallback) {
|
|
29
|
+
// 1. Let mode be "no-cors" if corsAttributeState is No CORS, and "cors" otherwise.
|
|
30
|
+
let mode = corsAttributeState === 'no cors' ? 'no-cors' : 'cors'
|
|
31
|
+
|
|
32
|
+
// 2. If same-origin fallback flag is set and mode is "no-cors", set mode to "same-origin".
|
|
33
|
+
if (sameOriginFallback && mode === 'no-cors') {
|
|
34
|
+
mode = 'same-origin'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Let credentialsMode be "include".
|
|
38
|
+
let credentialsMode = 'include'
|
|
39
|
+
|
|
40
|
+
// 4. If corsAttributeState is Anonymous, set credentialsMode to "same-origin".
|
|
41
|
+
if (corsAttributeState === 'anonymous') {
|
|
42
|
+
credentialsMode = 'same-origin'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 5. Return a new request whose URL is url, destination is destination, mode is mode,
|
|
46
|
+
// credentials mode is credentialsMode, and whose use-URL-credentials flag is set.
|
|
47
|
+
return makeRequest({
|
|
48
|
+
urlList: [url],
|
|
49
|
+
destination,
|
|
50
|
+
mode,
|
|
51
|
+
credentials: credentialsMode,
|
|
52
|
+
useCredentials: true
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
module.exports = {
|
|
27
57
|
isValidLastEventId,
|
|
28
|
-
isASCIINumber
|
|
58
|
+
isASCIINumber,
|
|
59
|
+
createPotentialCORSRequest
|
|
29
60
|
}
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -392,6 +392,49 @@ function bodyMixinMethods (instance, getInternalState) {
|
|
|
392
392
|
return consumeBody(this, (bytes) => {
|
|
393
393
|
return new Uint8Array(bytes)
|
|
394
394
|
}, instance, getInternalState)
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
textStream () {
|
|
398
|
+
const this_ = getInternalState(this)
|
|
399
|
+
|
|
400
|
+
// 1. If this is unusable, then throw a TypeError.
|
|
401
|
+
if (bodyUnusable(this_)) {
|
|
402
|
+
throw new TypeError('Body is unusable: Body has already been read')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 2. If this’s body is null:
|
|
406
|
+
if (this_.body == null) {
|
|
407
|
+
// 2.1. Let emptyStream be a new ReadableStream in this’s relevant realm.
|
|
408
|
+
// 2.2. Set up emptyStream.
|
|
409
|
+
/** @type {ReadableStreamDefaultController<any>} */
|
|
410
|
+
let controller
|
|
411
|
+
const emptyStream = new ReadableStream({
|
|
412
|
+
start: (c) => {
|
|
413
|
+
controller = c
|
|
414
|
+
},
|
|
415
|
+
pull: () => Promise.resolve(),
|
|
416
|
+
cancel: () => Promise.resolve()
|
|
417
|
+
}, {
|
|
418
|
+
size: () => 1
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// 2.3. Close emptyStream.
|
|
422
|
+
controller.close()
|
|
423
|
+
|
|
424
|
+
// 2.4. Return emptyStream.
|
|
425
|
+
return emptyStream
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 3. Let stream be this’s body’s stream.
|
|
429
|
+
/** @type {ReadableStream} */
|
|
430
|
+
const stream = this_.body.stream
|
|
431
|
+
|
|
432
|
+
// 4. Let decoder be a new TextDecoderStream object in this’s relevant realm.
|
|
433
|
+
// 5. Set up decoder with UTF-8.
|
|
434
|
+
const decoder = new TextDecoderStream('UTF-8')
|
|
435
|
+
|
|
436
|
+
// 6. Return the result of stream, piped through decoder.
|
|
437
|
+
return stream.pipeThrough(decoder)
|
|
395
438
|
}
|
|
396
439
|
}
|
|
397
440
|
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -930,6 +930,7 @@ function makeRequest (init) {
|
|
|
930
930
|
referrerPolicy: init.referrerPolicy ?? '',
|
|
931
931
|
mode: init.mode ?? 'no-cors',
|
|
932
932
|
useCORSPreflightFlag: init.useCORSPreflightFlag ?? false,
|
|
933
|
+
// TODO: is this credentials mode? https://fetch.spec.whatwg.org/#concept-request-credentials-mode
|
|
933
934
|
credentials: init.credentials ?? 'same-origin',
|
|
934
935
|
useCredentials: init.useCredentials ?? false,
|
|
935
936
|
cache: init.cache ?? 'default',
|
|
@@ -39,6 +39,9 @@ class ByteParser extends Writable {
|
|
|
39
39
|
/** @type {import('./websocket').Handler} */
|
|
40
40
|
#handler
|
|
41
41
|
|
|
42
|
+
/** @type {number} */
|
|
43
|
+
#maxFragments
|
|
44
|
+
|
|
42
45
|
/** @type {number} */
|
|
43
46
|
#maxPayloadSize
|
|
44
47
|
|
|
@@ -52,6 +55,7 @@ class ByteParser extends Writable {
|
|
|
52
55
|
|
|
53
56
|
this.#handler = handler
|
|
54
57
|
this.#extensions = extensions == null ? new Map() : extensions
|
|
58
|
+
this.#maxFragments = options.maxFragments ?? 0
|
|
55
59
|
this.#maxPayloadSize = options.maxPayloadSize ?? 0
|
|
56
60
|
|
|
57
61
|
if (this.#extensions.has('permessage-deflate')) {
|
|
@@ -75,7 +79,7 @@ class ByteParser extends Writable {
|
|
|
75
79
|
if (
|
|
76
80
|
this.#maxPayloadSize > 0 &&
|
|
77
81
|
!isControlFrame(this.#info.opcode) &&
|
|
78
|
-
this.#info.payloadLength > this.#maxPayloadSize
|
|
82
|
+
this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
|
|
79
83
|
) {
|
|
80
84
|
failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size')
|
|
81
85
|
return false
|
|
@@ -242,7 +246,9 @@ class ByteParser extends Writable {
|
|
|
242
246
|
this.#state = parserStates.INFO
|
|
243
247
|
} else {
|
|
244
248
|
if (!this.#info.compressed) {
|
|
245
|
-
this.writeFragments(body)
|
|
249
|
+
if (!this.writeFragments(body)) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
246
252
|
|
|
247
253
|
// If the frame is not fragmented, a message has been received.
|
|
248
254
|
// If the frame is fragmented, it will terminate with a fin bit set
|
|
@@ -264,7 +270,9 @@ class ByteParser extends Writable {
|
|
|
264
270
|
return
|
|
265
271
|
}
|
|
266
272
|
|
|
267
|
-
this.writeFragments(data)
|
|
273
|
+
if (!this.writeFragments(data)) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
268
276
|
|
|
269
277
|
// Check cumulative fragment size
|
|
270
278
|
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
|
@@ -345,8 +353,17 @@ class ByteParser extends Writable {
|
|
|
345
353
|
}
|
|
346
354
|
|
|
347
355
|
writeFragments (fragment) {
|
|
356
|
+
if (
|
|
357
|
+
this.#maxFragments > 0 &&
|
|
358
|
+
this.#fragments.length === this.#maxFragments
|
|
359
|
+
) {
|
|
360
|
+
failWebsocketConnection(this.#handler, 1008, 'Too many message fragments')
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
|
|
348
364
|
this.#fragmentsBytes += fragment.length
|
|
349
365
|
this.#fragments.push(fragment)
|
|
366
|
+
return true
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
consumeFragments () {
|
|
@@ -258,7 +258,14 @@ class WebSocketStream {
|
|
|
258
258
|
#onConnectionEstablished (response, parsedExtensions) {
|
|
259
259
|
this.#handler.socket = response.socket
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
// Get options from dispatcher options
|
|
262
|
+
const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments
|
|
263
|
+
const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize
|
|
264
|
+
|
|
265
|
+
const parser = new ByteParser(this.#handler, parsedExtensions, {
|
|
266
|
+
maxFragments,
|
|
267
|
+
maxPayloadSize
|
|
268
|
+
})
|
|
262
269
|
parser.on('drain', () => this.#handler.onParserDrain())
|
|
263
270
|
parser.on('error', (err) => this.#handler.onParserError(err))
|
|
264
271
|
|
|
@@ -468,10 +468,12 @@ class WebSocket extends EventTarget {
|
|
|
468
468
|
// once this happens, the connection is open
|
|
469
469
|
this.#handler.socket = response.socket
|
|
470
470
|
|
|
471
|
-
// Get
|
|
471
|
+
// Get options from dispatcher options
|
|
472
|
+
const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments
|
|
472
473
|
const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize
|
|
473
474
|
|
|
474
475
|
const parser = new ByteParser(this.#handler, parsedExtensions, {
|
|
476
|
+
maxFragments,
|
|
475
477
|
maxPayloadSize
|
|
476
478
|
})
|
|
477
479
|
parser.on('drain', () => this.#handler.onParserDrain())
|
package/package.json
CHANGED
package/types/client.d.ts
CHANGED
|
@@ -116,6 +116,11 @@ export declare namespace Client {
|
|
|
116
116
|
bytesRead?: number
|
|
117
117
|
}
|
|
118
118
|
export interface WebSocketOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Maximum number of fragments in a message. Set to 0 to disable the limit.
|
|
121
|
+
* @default 131072
|
|
122
|
+
*/
|
|
123
|
+
maxFragments?: number;
|
|
119
124
|
/**
|
|
120
125
|
* Maximum allowed payload size in bytes for WebSocket messages.
|
|
121
126
|
* Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
|
package/types/fetch.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export class BodyMixin {
|
|
|
57
57
|
readonly formData: () => Promise<FormData>
|
|
58
58
|
readonly json: () => Promise<unknown>
|
|
59
59
|
readonly text: () => Promise<string>
|
|
60
|
+
readonly textStream: () => ReadableStream<string>
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export interface SpecIterator<T, TReturn = any, TNext = undefined> {
|