undici 8.10.0 → 8.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Client.md +4 -0
- package/docs/docs/api/EnvHttpProxyAgent.md +3 -2
- package/docs/docs/api/EventSource.md +4 -0
- package/lib/cache/memory-cache-store.js +1 -1
- package/lib/dispatcher/agent.js +7 -2
- package/lib/dispatcher/balanced-pool.js +1 -1
- package/lib/dispatcher/client-h1.js +12 -4
- package/lib/dispatcher/client-h2.js +43 -3
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +13 -1
- package/lib/dispatcher/dispatcher.js +10 -0
- package/lib/dispatcher/env-http-proxy-agent.js +10 -3
- package/lib/dispatcher/proxy-agent.js +1 -1
- package/lib/dispatcher/round-robin-pool.js +1 -1
- package/lib/dispatcher/socks5-proxy-agent.js +3 -1
- package/lib/handler/cache-handler.js +8 -0
- package/lib/handler/decorator-handler.js +7 -1
- package/lib/handler/deduplication-handler.js +12 -6
- package/lib/handler/redirect-handler.js +8 -0
- package/lib/handler/retry-handler.js +83 -11
- package/lib/interceptor/dump.js +1 -1
- package/lib/mock/mock-agent.js +13 -1
- package/lib/mock/mock-utils.js +11 -10
- package/lib/web/cookies/parse.js +4 -2
- package/lib/web/eventsource/eventsource-stream.js +28 -1
- package/lib/web/eventsource/eventsource.js +4 -1
- package/lib/web/fetch/request.js +2 -3
- package/package.json +3 -3
- package/types/client.d.ts +10 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -147,6 +147,10 @@ added: v1.0.0
|
|
|
147
147
|
WebSocket messages. Applied to uncompressed messages, compressed frame
|
|
148
148
|
payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
|
|
149
149
|
disable the limit. **Default:** `134217728`.
|
|
150
|
+
* `eventSource` {Object} (optional) EventSource-specific configuration.
|
|
151
|
+
* `maxEventSize` {number} The maximum allowed event size, in bytes, for
|
|
152
|
+
EventSource messages. Set to `0` to disable the limit.
|
|
153
|
+
**Default:** `buffer.kStringMaxLength`.
|
|
150
154
|
* Returns: {Client}
|
|
151
155
|
|
|
152
156
|
Instantiating a `Client` does not open a connection; the connection is
|
|
@@ -25,8 +25,9 @@ it is used only for HTTPS requests.
|
|
|
25
25
|
proxied. Each entry may include a leading dot or `*.` wildcard (for example
|
|
26
26
|
`.example.com`) to match subdomains, and an optional `:port` suffix to restrict
|
|
27
27
|
the match to a specific port. A request bypasses the proxy when its host equals
|
|
28
|
-
an entry or is a subdomain of one.
|
|
29
|
-
|
|
28
|
+
an entry or is a subdomain of one. A trailing dot is ignored on both sides, so
|
|
29
|
+
`example.com.` and `example.com` match each other. Setting `no_proxy` to `*`
|
|
30
|
+
bypasses the proxy for every request.
|
|
30
31
|
|
|
31
32
|
The uppercase variants `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are also
|
|
32
33
|
honored. When both the lowercase and uppercase forms of a variable are set, the
|
|
@@ -66,6 +66,9 @@ added: v6.5.0
|
|
|
66
66
|
wait before re-establishing a dropped connection. The server may override
|
|
67
67
|
this value with a `retry` field. **Default:** `3000`.
|
|
68
68
|
|
|
69
|
+
EventSource-specific limits can be configured on the dispatcher using the
|
|
70
|
+
`eventSource` option. See [`Client`][] for details.
|
|
71
|
+
|
|
69
72
|
Creates a new `EventSource` and immediately begins connecting to `url`. The
|
|
70
73
|
request is sent with the `Accept: text/event-stream` header, a cache mode of
|
|
71
74
|
`no-store`, and an initiator type of `other`.
|
|
@@ -349,6 +352,7 @@ eventSource.onerror = () => {
|
|
|
349
352
|
```
|
|
350
353
|
|
|
351
354
|
[WHATWG-conformant]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
|
355
|
+
[`Client`]: Client.md#new-clienturl-options
|
|
352
356
|
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
|
353
357
|
[`addEventListener()`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
|
354
358
|
[server-sent events]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
|
@@ -179,7 +179,7 @@ class MemoryCacheStore extends EventEmitter {
|
|
|
179
179
|
|
|
180
180
|
// Perform eviction
|
|
181
181
|
for (const [key, entries] of store.#entries) {
|
|
182
|
-
for (const entry of entries.splice(0, entries.length / 2)) {
|
|
182
|
+
for (const entry of entries.splice(0, Math.ceil(entries.length / 2))) {
|
|
183
183
|
store.#size -= entry.size
|
|
184
184
|
store.#count -= 1
|
|
185
185
|
}
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
|
|
4
|
-
const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
4
|
+
const { kBusy, kClients, kConnected, kRunning, kPending, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
5
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const Client = require('./client')
|
|
@@ -97,7 +97,12 @@ class Agent extends DispatcherBase {
|
|
|
97
97
|
return
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// A GOAWAY detaches the HTTP/2 session before requeued requests are
|
|
101
|
+
// dispatched on a replacement connection. At that point the pool has
|
|
102
|
+
// no connected clients and is not busy, but it still has pending work.
|
|
103
|
+
// Closing it here lets the replacement Client finish those requests
|
|
104
|
+
// and then destroys that new connection with ClientDestroyedError.
|
|
105
|
+
if (dispatcher[kConnected] > 0 || dispatcher[kBusy] || dispatcher[kPending] > 0) {
|
|
101
106
|
return
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -1052,7 +1052,7 @@ function onSocketClose () {
|
|
|
1052
1052
|
|
|
1053
1053
|
function clearIdleSocketValidation (socket) {
|
|
1054
1054
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
1055
|
-
|
|
1055
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
1056
1056
|
socket[kIdleSocketValidationTimeout] = null
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
@@ -1061,15 +1061,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
1061
1061
|
|
|
1062
1062
|
function scheduleIdleSocketValidation (client, socket) {
|
|
1063
1063
|
socket[kIdleSocketValidation] = 1
|
|
1064
|
-
|
|
1064
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
1065
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
1066
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
1067
|
+
//
|
|
1068
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
1069
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
1070
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
1071
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
1072
|
+
// return immediately — the hybrid those issues asked for.
|
|
1073
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
1065
1074
|
socket[kIdleSocketValidationTimeout] = null
|
|
1066
1075
|
socket[kIdleSocketValidation] = 2
|
|
1067
1076
|
|
|
1068
1077
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1069
1078
|
client[kResume]()
|
|
1070
1079
|
}
|
|
1071
|
-
}
|
|
1072
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1080
|
+
})
|
|
1073
1081
|
}
|
|
1074
1082
|
|
|
1075
1083
|
/**
|
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
InformationalError,
|
|
11
11
|
InvalidArgumentError,
|
|
12
12
|
HeadersTimeoutError,
|
|
13
|
-
BodyTimeoutError
|
|
13
|
+
BodyTimeoutError,
|
|
14
|
+
ResponseExceededMaxSizeError
|
|
14
15
|
} = require('../core/errors.js')
|
|
15
16
|
const {
|
|
16
17
|
kUrl,
|
|
@@ -39,7 +40,8 @@ const {
|
|
|
39
40
|
kRemoteSettings,
|
|
40
41
|
kHTTP2Stream,
|
|
41
42
|
kHTTP2SessionState,
|
|
42
|
-
kHTTP2Options
|
|
43
|
+
kHTTP2Options,
|
|
44
|
+
kMaxResponseSize
|
|
43
45
|
} = require('../core/symbols.js')
|
|
44
46
|
const { channels } = require('../core/diagnostics.js')
|
|
45
47
|
|
|
@@ -1022,9 +1024,11 @@ function writeH2 (client, request) {
|
|
|
1022
1024
|
const state = {
|
|
1023
1025
|
abort: null,
|
|
1024
1026
|
body: request.body,
|
|
1027
|
+
bytesRead: 0,
|
|
1025
1028
|
client,
|
|
1026
1029
|
contentLength: null,
|
|
1027
1030
|
expectsPayload: false,
|
|
1031
|
+
maxResponseSize: client[kMaxResponseSize],
|
|
1028
1032
|
request,
|
|
1029
1033
|
headersTimeout,
|
|
1030
1034
|
bodyTimeout,
|
|
@@ -1260,6 +1264,7 @@ function writeH2 (client, request) {
|
|
|
1260
1264
|
// become unreachable once the stream closes, so plain `on` avoids the
|
|
1261
1265
|
// per-listener `once` wrapper allocation.
|
|
1262
1266
|
stream.on('response', onResponse)
|
|
1267
|
+
stream.on('headers', onInterimResponse)
|
|
1263
1268
|
stream.on('end', onEnd)
|
|
1264
1269
|
stream.on('error', onError)
|
|
1265
1270
|
stream.on('frameError', onFrameError)
|
|
@@ -1280,6 +1285,7 @@ function removeRequestStreamListeners (stream) {
|
|
|
1280
1285
|
stream.off('error', noop)
|
|
1281
1286
|
stream.off('continue', writeBodyH2)
|
|
1282
1287
|
stream.off('response', onResponse)
|
|
1288
|
+
stream.off('headers', onInterimResponse)
|
|
1283
1289
|
stream.off('end', onEnd)
|
|
1284
1290
|
stream.off('error', onError)
|
|
1285
1291
|
stream.off('frameError', onFrameError)
|
|
@@ -1322,17 +1328,51 @@ function onData (chunk) {
|
|
|
1322
1328
|
return
|
|
1323
1329
|
}
|
|
1324
1330
|
|
|
1325
|
-
const { request } = state
|
|
1331
|
+
const { request, maxResponseSize } = state
|
|
1326
1332
|
|
|
1327
1333
|
if (request.aborted || request.completed) {
|
|
1328
1334
|
return
|
|
1329
1335
|
}
|
|
1330
1336
|
|
|
1337
|
+
if (maxResponseSize > -1 && state.bytesRead + chunk.length > maxResponseSize) {
|
|
1338
|
+
// Unlike HTTP/1.1, which destroys the socket because it cannot abandon one
|
|
1339
|
+
// response without losing framing, resetting the offending stream leaves
|
|
1340
|
+
// the session usable for its siblings.
|
|
1341
|
+
state.abort(new ResponseExceededMaxSizeError())
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
state.bytesRead += chunk.length
|
|
1346
|
+
|
|
1331
1347
|
if (request.onResponseData(chunk) === false) {
|
|
1332
1348
|
stream.pause()
|
|
1333
1349
|
}
|
|
1334
1350
|
}
|
|
1335
1351
|
|
|
1352
|
+
function onInterimResponse (headers) {
|
|
1353
|
+
const stream = this
|
|
1354
|
+
const state = stream[kRequestStreamState]
|
|
1355
|
+
|
|
1356
|
+
if (state == null) {
|
|
1357
|
+
return
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const { request } = state
|
|
1361
|
+
|
|
1362
|
+
if (request.aborted || request.completed) {
|
|
1363
|
+
return
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// node http2 emits 'headers' for interim (1xx) informational responses,
|
|
1367
|
+
// while the final response arrives via 'response'. Forward these to the
|
|
1368
|
+
// handler so that onInfo is invoked, matching the HTTP/1 behaviour and the
|
|
1369
|
+
// documented onInfo contract.
|
|
1370
|
+
const statusCode = headers[HTTP2_HEADER_STATUS]
|
|
1371
|
+
delete headers[HTTP2_HEADER_STATUS]
|
|
1372
|
+
|
|
1373
|
+
request.onResponseStart(Number(statusCode), headers, noop, '')
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1336
1376
|
function onResponse (headers) {
|
|
1337
1377
|
const stream = this
|
|
1338
1378
|
const state = stream[kRequestStreamState]
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -137,7 +137,8 @@ class Client extends DispatcherBase {
|
|
|
137
137
|
connectionWindowSize,
|
|
138
138
|
pingInterval,
|
|
139
139
|
webSocket,
|
|
140
|
-
h2Options
|
|
140
|
+
h2Options,
|
|
141
|
+
eventSource
|
|
141
142
|
} = {}) {
|
|
142
143
|
if (keepAlive !== undefined) {
|
|
143
144
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -276,7 +277,7 @@ class Client extends DispatcherBase {
|
|
|
276
277
|
}
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
super({ webSocket })
|
|
280
|
+
super({ webSocket, eventSource })
|
|
280
281
|
|
|
281
282
|
if (typeof connect !== 'function') {
|
|
282
283
|
connect = buildConnector({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const buffer = require('node:buffer')
|
|
3
4
|
const Dispatcher = require('./dispatcher')
|
|
4
5
|
const {
|
|
5
6
|
ClientDestroyedError,
|
|
@@ -11,6 +12,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
|
|
|
11
12
|
const kOnDestroyed = Symbol('onDestroyed')
|
|
12
13
|
const kOnClosed = Symbol('onClosed')
|
|
13
14
|
const kWebSocketOptions = Symbol('webSocketOptions')
|
|
15
|
+
const kEventSourceOptions = Symbol('eventSourceOptions')
|
|
14
16
|
|
|
15
17
|
class DispatcherBase extends Dispatcher {
|
|
16
18
|
/** @type {boolean} */
|
|
@@ -31,10 +33,11 @@ class DispatcherBase extends Dispatcher {
|
|
|
31
33
|
constructor (opts) {
|
|
32
34
|
super()
|
|
33
35
|
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
|
36
|
+
this[kEventSourceOptions] = opts?.eventSource ?? {}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
* @returns {import('../../types/
|
|
40
|
+
* @returns {import('../../types/client').Client.WebSocketOptions}
|
|
38
41
|
*/
|
|
39
42
|
get webSocketOptions () {
|
|
40
43
|
return {
|
|
@@ -43,6 +46,15 @@ class DispatcherBase extends Dispatcher {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @returns {import('../../types/client').Client.EventSourceOptions}
|
|
51
|
+
*/
|
|
52
|
+
get eventSourceOptions () {
|
|
53
|
+
return {
|
|
54
|
+
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/** @returns {boolean} */
|
|
47
59
|
get destroyed () {
|
|
48
60
|
return this[kDestroyed]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const EventEmitter = require('node:events')
|
|
3
|
+
const { kUrl } = require('../core/symbols')
|
|
3
4
|
|
|
4
5
|
class Dispatcher extends EventEmitter {
|
|
5
6
|
dispatch () {
|
|
@@ -35,6 +36,15 @@ class Dispatcher extends EventEmitter {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const originalDispatch = dispatch
|
|
40
|
+
const self = this
|
|
41
|
+
dispatch = function (opts, handler) {
|
|
42
|
+
if (opts && typeof opts === 'object' && !opts.origin && self[kUrl]) {
|
|
43
|
+
opts = Object.assign({}, opts, { origin: self[kUrl].origin })
|
|
44
|
+
}
|
|
45
|
+
return originalDispatch(opts, handler)
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
return new Proxy(this, {
|
|
39
49
|
get: (target, key) => key === 'dispatch' ? dispatch : target[key]
|
|
40
50
|
})
|
|
@@ -16,7 +16,7 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
16
16
|
#opts = null
|
|
17
17
|
|
|
18
18
|
constructor (opts = {}) {
|
|
19
|
-
super()
|
|
19
|
+
super(opts)
|
|
20
20
|
this.#opts = opts
|
|
21
21
|
|
|
22
22
|
const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
|
|
@@ -69,6 +69,13 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
69
69
|
// brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the
|
|
70
70
|
// result matches the unbracketed form stored by #parseNoProxy.
|
|
71
71
|
hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase()
|
|
72
|
+
// Drop a trailing dot: it only marks the fully qualified form of a domain
|
|
73
|
+
// name ("example.com." and "example.com" are the same name, RFC 1034 root
|
|
74
|
+
// label). This runs on every dispatch, so it is a charCode check rather
|
|
75
|
+
// than a third regex. `length > 1` leaves the degenerate host "." alone.
|
|
76
|
+
if (hostname.length > 1 && hostname.charCodeAt(hostname.length - 1) === 46) {
|
|
77
|
+
hostname = hostname.slice(0, -1)
|
|
78
|
+
}
|
|
72
79
|
port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
|
|
73
80
|
if (!this.#shouldProxy(hostname, port)) {
|
|
74
81
|
return this[kNoProxyAgent]
|
|
@@ -143,8 +150,8 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
noProxyEntries.push({
|
|
146
|
-
// strip leading dot or asterisk with dot
|
|
147
|
-
hostname: hostname.replace(/^\*?\./, '').toLowerCase(),
|
|
153
|
+
// strip leading dot or asterisk with dot, and any trailing dot
|
|
154
|
+
hostname: hostname.replace(/^\*?\./, '').replace(/^(.+)\.$/, '$1').toLowerCase(),
|
|
148
155
|
port
|
|
149
156
|
})
|
|
150
157
|
}
|
|
@@ -120,7 +120,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
120
120
|
|
|
121
121
|
const { proxyTunnel, connectTimeout } = opts
|
|
122
122
|
|
|
123
|
-
super()
|
|
123
|
+
super(opts)
|
|
124
124
|
|
|
125
125
|
const url = this.#getUrl(opts)
|
|
126
126
|
const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
|
|
@@ -29,7 +29,7 @@ let experimentalWarningEmitted = false
|
|
|
29
29
|
*/
|
|
30
30
|
class Socks5ProxyAgent extends DispatcherBase {
|
|
31
31
|
constructor (proxyUrl, options = {}) {
|
|
32
|
-
super()
|
|
32
|
+
super(options)
|
|
33
33
|
|
|
34
34
|
// Emit experimental warning only once
|
|
35
35
|
if (!experimentalWarningEmitted) {
|
|
@@ -115,6 +115,7 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
115
115
|
const authenticationReady = Promise.withResolvers()
|
|
116
116
|
|
|
117
117
|
const authenticationTimeout = setTimeout(() => {
|
|
118
|
+
socks5Client.destroy()
|
|
118
119
|
authenticationReady.reject(new Error('SOCKS5 authentication timeout'))
|
|
119
120
|
}, 5000)
|
|
120
121
|
|
|
@@ -148,6 +149,7 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
148
149
|
const connectionReady = Promise.withResolvers()
|
|
149
150
|
|
|
150
151
|
const connectionTimeout = setTimeout(() => {
|
|
152
|
+
socks5Client.destroy()
|
|
151
153
|
connectionReady.reject(new Error('SOCKS5 connection timeout'))
|
|
152
154
|
}, 5000)
|
|
153
155
|
|
|
@@ -173,6 +173,14 @@ class CacheHandler {
|
|
|
173
173
|
this.#handler.onRequestStart?.(controller, context)
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
onBodySent (chunk) {
|
|
177
|
+
this.#handler.onBodySent?.(chunk)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onRequestSent () {
|
|
181
|
+
this.#handler.onRequestSent?.()
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
177
185
|
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
|
178
186
|
}
|
|
@@ -62,5 +62,11 @@ module.exports = class DecoratorHandler {
|
|
|
62
62
|
/**
|
|
63
63
|
* @deprecated
|
|
64
64
|
*/
|
|
65
|
-
onBodySent () {
|
|
65
|
+
onBodySent (...args) {
|
|
66
|
+
return this.#handler.onBodySent?.(...args)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onRequestSent (...args) {
|
|
70
|
+
return this.#handler.onRequestSent?.(...args)
|
|
71
|
+
}
|
|
66
72
|
}
|
|
@@ -365,12 +365,22 @@ class DeduplicationHandler {
|
|
|
365
365
|
get aborted () { return state.aborted },
|
|
366
366
|
get reason () { return state.reason },
|
|
367
367
|
abort: (reason) => {
|
|
368
|
+
if (state.aborted) {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
368
372
|
state.aborted = true
|
|
369
373
|
state.reason = reason ?? null
|
|
370
374
|
waitingHandler.done = true
|
|
371
375
|
waitingHandler.pendingTrailers = null
|
|
372
376
|
waitingHandler.bufferedChunks = []
|
|
373
377
|
waitingHandler.bufferedBytes = 0
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
handler.onResponseError?.(waitingHandler.controller, state.reason ?? new RequestAbortedError())
|
|
381
|
+
} catch {
|
|
382
|
+
// Ignore errors from waiting handlers
|
|
383
|
+
}
|
|
374
384
|
}
|
|
375
385
|
}
|
|
376
386
|
|
|
@@ -444,12 +454,8 @@ class DeduplicationHandler {
|
|
|
444
454
|
waitingHandler.bufferedChunks = []
|
|
445
455
|
waitingHandler.bufferedBytes = 0
|
|
446
456
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
waitingHandler.handler.onResponseError?.(waitingHandler.controller, err)
|
|
450
|
-
} catch {
|
|
451
|
-
// Ignore errors from waiting handlers
|
|
452
|
-
}
|
|
457
|
+
// controller.abort(err) notifies the handler via onResponseError
|
|
458
|
+
waitingHandler.controller.abort(err)
|
|
453
459
|
}
|
|
454
460
|
|
|
455
461
|
#pruneDoneWaitingHandlers () {
|
|
@@ -43,6 +43,14 @@ class RedirectHandler {
|
|
|
43
43
|
this.handler.onRequestStart?.(controller, { ...context, history: this.history })
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
onBodySent (chunk) {
|
|
47
|
+
this.handler.onBodySent?.(chunk)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onRequestSent () {
|
|
51
|
+
this.handler.onRequestSent?.()
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
47
55
|
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
|
48
56
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const assert = require('node:assert')
|
|
3
3
|
|
|
4
4
|
const { kRetryHandlerDefaultRetry } = require('../core/symbols')
|
|
5
|
-
const { RequestRetryError } = require('../core/errors')
|
|
5
|
+
const { RequestRetryError, RequestAbortedError } = require('../core/errors')
|
|
6
6
|
const {
|
|
7
7
|
isDisturbed,
|
|
8
8
|
parseRangeHeader,
|
|
@@ -41,14 +41,26 @@ function validatePartialResponseContentLength (headers, range, statusCode, retry
|
|
|
41
41
|
// new one: backpressure pauses the new connection's controller, but the
|
|
42
42
|
// consumer's resume() targets the old one, so the resumed body stalls forever.
|
|
43
43
|
// The proxy always forwards to the controller of the currently active connection.
|
|
44
|
+
// An abort is additionally reported to the handler so it can cancel a pending
|
|
45
|
+
// retry backoff instead of letting the request hang until the backoff elapses.
|
|
46
|
+
// The notification is a private callback the handler hands over on construction,
|
|
47
|
+
// so nothing outside the handler can trigger it.
|
|
44
48
|
class RetryController {
|
|
45
|
-
|
|
49
|
+
#onAbort
|
|
50
|
+
|
|
51
|
+
constructor (onAbort) {
|
|
52
|
+
this.#onAbort = onAbort
|
|
46
53
|
this.target = null
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
pause () { this.target?.pause() }
|
|
50
57
|
resume () { this.target?.resume() }
|
|
51
|
-
|
|
58
|
+
|
|
59
|
+
abort (reason) {
|
|
60
|
+
this.target?.abort(reason)
|
|
61
|
+
this.#onAbort(reason)
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
get paused () { return this.target?.paused ?? false }
|
|
53
65
|
get aborted () { return this.target?.aborted ?? false }
|
|
54
66
|
get reason () { return this.target?.reason ?? null }
|
|
@@ -112,7 +124,16 @@ class RetryHandler {
|
|
|
112
124
|
this.etag = null
|
|
113
125
|
this.statusCode = null
|
|
114
126
|
this.headers = null
|
|
115
|
-
this.controllerProxy = new RetryController()
|
|
127
|
+
this.controllerProxy = new RetryController(reason => this.#onAbort(reason))
|
|
128
|
+
// A retry decision is in flight (the policy may be holding a backoff
|
|
129
|
+
// timer). While pending, a consumer abort cancels the wait.
|
|
130
|
+
this.retryPending = false
|
|
131
|
+
// Backoff timer returned by the retry policy, so #onAbort can cancel it.
|
|
132
|
+
// Null for custom policies that do not return their timer.
|
|
133
|
+
this.retryTimer = null
|
|
134
|
+
// Set once an abort during the backoff delivered the terminal error
|
|
135
|
+
// downstream; late policy callbacks and connection errors are then moot.
|
|
136
|
+
this.aborted = false
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
|
|
@@ -135,6 +156,13 @@ class RetryHandler {
|
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
function shouldRetry (passedErr) {
|
|
159
|
+
if (this.aborted) {
|
|
160
|
+
// Aborted while the policy was deciding; the decision is moot.
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
this.retryPending = false
|
|
164
|
+
this.retryTimer = null
|
|
165
|
+
|
|
138
166
|
if (passedErr) {
|
|
139
167
|
this.headersSent = true
|
|
140
168
|
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
@@ -154,14 +182,17 @@ class RetryHandler {
|
|
|
154
182
|
// between, leaving this one paused forever -- the very stall the proxy exists
|
|
155
183
|
// to prevent.
|
|
156
184
|
controller.pause()
|
|
157
|
-
|
|
185
|
+
// The default policy returns its backoff timer so an abort can cancel it;
|
|
186
|
+
// a custom policy may return anything (or nothing), which is ignored.
|
|
187
|
+
this.retryPending = true
|
|
188
|
+
this.retryTimer = this.retryOpts.retry(
|
|
158
189
|
err,
|
|
159
190
|
{
|
|
160
191
|
state: { counter: this.retryCount },
|
|
161
192
|
opts: { retryOptions: this.retryOpts, ...this.opts }
|
|
162
193
|
},
|
|
163
194
|
shouldRetry.bind(this)
|
|
164
|
-
)
|
|
195
|
+
) ?? null
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
onRequestStart (controller, context) {
|
|
@@ -176,6 +207,14 @@ class RetryHandler {
|
|
|
176
207
|
}
|
|
177
208
|
}
|
|
178
209
|
|
|
210
|
+
onBodySent (chunk) {
|
|
211
|
+
this.handler.onBodySent?.(chunk)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
onRequestSent () {
|
|
215
|
+
this.handler.onRequestSent?.()
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
onRequestUpgrade (_controller, statusCode, headers, socket) {
|
|
180
219
|
this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
|
|
181
220
|
}
|
|
@@ -190,7 +229,8 @@ class RetryHandler {
|
|
|
190
229
|
timeoutFactor,
|
|
191
230
|
statusCodes,
|
|
192
231
|
errorCodes,
|
|
193
|
-
methods
|
|
232
|
+
methods,
|
|
233
|
+
retryAfter
|
|
194
234
|
} = retryOptions
|
|
195
235
|
const { counter } = state
|
|
196
236
|
|
|
@@ -222,7 +262,7 @@ class RetryHandler {
|
|
|
222
262
|
return
|
|
223
263
|
}
|
|
224
264
|
|
|
225
|
-
let retryAfterHeader = headers?.['retry-after']
|
|
265
|
+
let retryAfterHeader = retryAfter === false ? undefined : headers?.['retry-after']
|
|
226
266
|
if (retryAfterHeader) {
|
|
227
267
|
retryAfterHeader = Number(retryAfterHeader)
|
|
228
268
|
retryAfterHeader = Number.isNaN(retryAfterHeader)
|
|
@@ -237,7 +277,9 @@ class RetryHandler {
|
|
|
237
277
|
? Math.min(retryAfterHeader, maxTimeout)
|
|
238
278
|
: Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
|
|
239
279
|
|
|
240
|
-
|
|
280
|
+
// Return the backoff timer so the handler can cancel it when the
|
|
281
|
+
// consumer aborts while the retry decision is pending.
|
|
282
|
+
return setTimeout(() => cb(null), retryTimeout)
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
@@ -435,6 +477,12 @@ class RetryHandler {
|
|
|
435
477
|
}
|
|
436
478
|
|
|
437
479
|
onResponseError (controller, err) {
|
|
480
|
+
if (this.aborted) {
|
|
481
|
+
// #onAbort already delivered the terminal error downstream; the late
|
|
482
|
+
// error of the torn-down connection must not be forwarded twice.
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
438
486
|
// controller is THIS failed connection (not the proxy): we inspect whether
|
|
439
487
|
// the consumer aborted it to decide retry-vs-propagate.
|
|
440
488
|
if (controller?.aborted || isDisturbed(this.opts.body)) {
|
|
@@ -443,6 +491,13 @@ class RetryHandler {
|
|
|
443
491
|
}
|
|
444
492
|
|
|
445
493
|
function shouldRetry (returnedErr) {
|
|
494
|
+
if (this.aborted) {
|
|
495
|
+
// Aborted while the policy was deciding; the decision is moot.
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
this.retryPending = false
|
|
499
|
+
this.retryTimer = null
|
|
500
|
+
|
|
446
501
|
if (!returnedErr) {
|
|
447
502
|
this.retry()
|
|
448
503
|
return
|
|
@@ -462,14 +517,31 @@ class RetryHandler {
|
|
|
462
517
|
this.retryCount += 1
|
|
463
518
|
}
|
|
464
519
|
|
|
465
|
-
this.
|
|
520
|
+
this.retryPending = true
|
|
521
|
+
this.retryTimer = this.retryOpts.retry(
|
|
466
522
|
err,
|
|
467
523
|
{
|
|
468
524
|
state: { counter: this.retryCount },
|
|
469
525
|
opts: { retryOptions: this.retryOpts, ...this.opts }
|
|
470
526
|
},
|
|
471
527
|
shouldRetry.bind(this)
|
|
472
|
-
)
|
|
528
|
+
) ?? null
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#onAbort (reason) {
|
|
532
|
+
// A consumer abort lands on the controller proxy. If the retry policy is
|
|
533
|
+
// still deciding (typically holding a backoff timer), cancel the wait and
|
|
534
|
+
// surface the abort immediately instead of letting the request hang until
|
|
535
|
+
// the backoff elapses.
|
|
536
|
+
if (!this.retryPending) {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.aborted = true
|
|
541
|
+
this.retryPending = false
|
|
542
|
+
clearTimeout(this.retryTimer)
|
|
543
|
+
this.retryTimer = null
|
|
544
|
+
this.handler.onResponseError?.(this.controllerProxy, reason ?? new RequestAbortedError())
|
|
473
545
|
}
|
|
474
546
|
}
|
|
475
547
|
|
package/lib/interceptor/dump.js
CHANGED
package/lib/mock/mock-agent.js
CHANGED
|
@@ -73,7 +73,7 @@ class MockAgent extends Dispatcher {
|
|
|
73
73
|
opts.origin = normalizeOrigin(opts.origin)
|
|
74
74
|
|
|
75
75
|
// Call MockAgent.get to perform additional setup before dispatching as normal
|
|
76
|
-
this.get(opts.origin)
|
|
76
|
+
const mockDispatcher = this.get(opts.origin)
|
|
77
77
|
|
|
78
78
|
this[kMockAgentAddCallHistoryLog](opts)
|
|
79
79
|
|
|
@@ -81,6 +81,18 @@ class MockAgent extends Dispatcher {
|
|
|
81
81
|
|
|
82
82
|
const dispatchOpts = { ...opts }
|
|
83
83
|
|
|
84
|
+
// Agent keeps HTTP/1.1-only dispatchers under a separate key. Legacy
|
|
85
|
+
// global dispatcher consumers use that path, so mirror the mock dispatches
|
|
86
|
+
// before delegating to the internal Agent.
|
|
87
|
+
if (dispatchOpts.allowH2 === false) {
|
|
88
|
+
const http1OnlyKey = `${dispatchOpts.origin}#http1-only`
|
|
89
|
+
if (!this[kClients].has(http1OnlyKey)) {
|
|
90
|
+
const http1OnlyDispatcher = this[kFactory](dispatchOpts.origin)
|
|
91
|
+
http1OnlyDispatcher[kDispatches] = mockDispatcher[kDispatches]
|
|
92
|
+
this[kMockAgentSet](http1OnlyKey, http1OnlyDispatcher)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
if (acceptNonStandardSearchParameters && dispatchOpts.path) {
|
|
85
97
|
const [path, searchParams] = dispatchOpts.path.split('?')
|
|
86
98
|
const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters)
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -333,8 +333,7 @@ function mockDispatch (opts, handler) {
|
|
|
333
333
|
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
|
334
334
|
return
|
|
335
335
|
}
|
|
336
|
-
mockDispatch
|
|
337
|
-
dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler)
|
|
336
|
+
dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler, { ...responseDefaults, ...resolvedData })
|
|
338
337
|
},
|
|
339
338
|
(error) => {
|
|
340
339
|
handler.onResponseError(null, error)
|
|
@@ -347,7 +346,7 @@ function mockDispatch (opts, handler) {
|
|
|
347
346
|
throw new InvalidArgumentError('reply options callback must return an object')
|
|
348
347
|
}
|
|
349
348
|
|
|
350
|
-
mockDispatch
|
|
349
|
+
return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler, { ...responseDefaults, ...callbackResult })
|
|
351
350
|
}
|
|
352
351
|
|
|
353
352
|
return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler)
|
|
@@ -356,9 +355,13 @@ function mockDispatch (opts, handler) {
|
|
|
356
355
|
/**
|
|
357
356
|
* Replies to a request once the mock dispatch data is fully resolved
|
|
358
357
|
*/
|
|
359
|
-
function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
360
|
-
// Parse mockDispatch data
|
|
361
|
-
|
|
358
|
+
function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler, resolvedResponse) {
|
|
359
|
+
// Parse mockDispatch data. When a reply callback has already been resolved
|
|
360
|
+
// in mockDispatch() (i.e. no body lifecycle hooks are involved), the resolved
|
|
361
|
+
// response is passed in here, leaving mockDispatch.data untouched so the
|
|
362
|
+
// callback can be re-invoked for persistent / times() replies.
|
|
363
|
+
const { data: responseData, delay } = mockDispatch
|
|
364
|
+
const response = resolvedResponse ?? responseData
|
|
362
365
|
|
|
363
366
|
// If specified, trigger dispatch error
|
|
364
367
|
if (response.error !== null) {
|
|
@@ -454,8 +457,7 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
454
457
|
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
|
455
458
|
return
|
|
456
459
|
}
|
|
457
|
-
|
|
458
|
-
handleReply(dispatches, mockDispatch.data)
|
|
460
|
+
handleReply(dispatches, { ...responseDefaults, ...resolvedData })
|
|
459
461
|
},
|
|
460
462
|
(err) => {
|
|
461
463
|
handler.onResponseError(null, err)
|
|
@@ -468,8 +470,7 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
468
470
|
throw new InvalidArgumentError('reply options callback must return an object')
|
|
469
471
|
}
|
|
470
472
|
|
|
471
|
-
|
|
472
|
-
handleReply(dispatches, mockDispatch.data)
|
|
473
|
+
handleReply(dispatches, { ...responseDefaults, ...callbackResult })
|
|
473
474
|
return
|
|
474
475
|
}
|
|
475
476
|
|
package/lib/web/cookies/parse.js
CHANGED
|
@@ -189,14 +189,16 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
|
|
|
189
189
|
// 1. If the first character of the attribute-value is not a DIGIT or a
|
|
190
190
|
// "-" character, ignore the cookie-av.
|
|
191
191
|
const charCode = attributeValue.charCodeAt(0)
|
|
192
|
+
const startsWithDigit = charCode >= 48 && charCode <= 57
|
|
193
|
+
const startsWithSignedDigit = attributeValue[0] === '-' && attributeValue.length > 1
|
|
192
194
|
|
|
193
|
-
if (
|
|
195
|
+
if (!startsWithDigit && !startsWithSignedDigit) {
|
|
194
196
|
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
// 2. If the remainder of attribute-value contains a non-DIGIT
|
|
198
200
|
// character, ignore the cookie-av.
|
|
199
|
-
if (
|
|
201
|
+
if (/[^\d]/.test(attributeValue.slice(1))) {
|
|
200
202
|
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
+
const buffer = require('node:buffer')
|
|
2
3
|
const { Transform } = require('node:stream')
|
|
3
4
|
const { isASCIINumber, isValidLastEventId } = require('./util')
|
|
4
5
|
|
|
@@ -23,6 +24,8 @@ const COLON = 0x3A
|
|
|
23
24
|
*/
|
|
24
25
|
const SPACE = 0x20
|
|
25
26
|
|
|
27
|
+
const defaultMaxEventSize = buffer.kStringMaxLength
|
|
28
|
+
|
|
26
29
|
const DATA = Buffer.from('data')
|
|
27
30
|
const EVENT = Buffer.from('event')
|
|
28
31
|
const ID = Buffer.from('id')
|
|
@@ -66,6 +69,12 @@ function isFieldName (line, length, field) {
|
|
|
66
69
|
return true
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
function createMaxEventSizeExceededError () {
|
|
73
|
+
const error = new Error('EventSource message size exceeded')
|
|
74
|
+
error.aborted = false
|
|
75
|
+
return error
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* @typedef {object} EventSourceStreamEvent
|
|
71
80
|
* @type {object}
|
|
@@ -114,6 +123,8 @@ class EventSourceStream extends Transform {
|
|
|
114
123
|
pos = 0
|
|
115
124
|
lineChunkIndex = 0
|
|
116
125
|
linePos = 0
|
|
126
|
+
eventDataSize = 0
|
|
127
|
+
maxEventSize
|
|
117
128
|
|
|
118
129
|
event = {
|
|
119
130
|
data: undefined,
|
|
@@ -125,6 +136,7 @@ class EventSourceStream extends Transform {
|
|
|
125
136
|
/**
|
|
126
137
|
* @param {object} options
|
|
127
138
|
* @param {boolean} [options.readableObjectMode]
|
|
139
|
+
* @param {number} [options.maxEventSize]
|
|
128
140
|
* @param {eventSourceSettings} [options.eventSourceSettings]
|
|
129
141
|
* @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push]
|
|
130
142
|
*/
|
|
@@ -136,6 +148,7 @@ class EventSourceStream extends Transform {
|
|
|
136
148
|
super(options)
|
|
137
149
|
|
|
138
150
|
this.state = options.eventSourceSettings || {}
|
|
151
|
+
this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize
|
|
139
152
|
if (options.push) {
|
|
140
153
|
this.push = options.push
|
|
141
154
|
}
|
|
@@ -231,7 +244,12 @@ class EventSourceStream extends Transform {
|
|
|
231
244
|
|
|
232
245
|
// In any case, we can process the line as we reached an
|
|
233
246
|
// end-of-line character
|
|
234
|
-
|
|
247
|
+
try {
|
|
248
|
+
this.parseLine(this.readLine(), this.event)
|
|
249
|
+
} catch (error) {
|
|
250
|
+
callback(error)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
235
253
|
this.consumeCurrentByte()
|
|
236
254
|
// A line was processed and this could be the end of the event. We need
|
|
237
255
|
// to check if the next line is empty to determine if the event is
|
|
@@ -282,6 +300,13 @@ class EventSourceStream extends Transform {
|
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
if (isFieldName(line, fieldLength, DATA)) {
|
|
303
|
+
const valueBytes = line.length - valueStart
|
|
304
|
+
const eventDataSize = this.eventDataSize + (event.data === undefined ? 0 : 1) + valueBytes
|
|
305
|
+
|
|
306
|
+
if (this.maxEventSize > 0 && eventDataSize > this.maxEventSize) {
|
|
307
|
+
throw createMaxEventSizeExceededError()
|
|
308
|
+
}
|
|
309
|
+
|
|
285
310
|
const value = line.toString('utf8', valueStart)
|
|
286
311
|
|
|
287
312
|
if (event.data === undefined) {
|
|
@@ -289,6 +314,7 @@ class EventSourceStream extends Transform {
|
|
|
289
314
|
} else {
|
|
290
315
|
event.data += `\n${value}`
|
|
291
316
|
}
|
|
317
|
+
this.eventDataSize = eventDataSize
|
|
292
318
|
return
|
|
293
319
|
}
|
|
294
320
|
|
|
@@ -345,6 +371,7 @@ class EventSourceStream extends Transform {
|
|
|
345
371
|
this.event.event = undefined
|
|
346
372
|
this.event.id = undefined
|
|
347
373
|
this.event.retry = undefined
|
|
374
|
+
this.eventDataSize = 0
|
|
348
375
|
}
|
|
349
376
|
|
|
350
377
|
hasPendingEvent () {
|
|
@@ -10,6 +10,7 @@ const { isNetworkError } = require('../fetch/response')
|
|
|
10
10
|
const { kEnumerableProperty } = require('../../core/util')
|
|
11
11
|
const { environmentSettingsObject } = require('../fetch/util')
|
|
12
12
|
const { createPotentialCORSRequest } = require('./util')
|
|
13
|
+
const { getGlobalDispatcher } = require('../../global')
|
|
13
14
|
|
|
14
15
|
let experimentalWarned = false
|
|
15
16
|
|
|
@@ -281,6 +282,7 @@ class EventSource extends EventTarget {
|
|
|
281
282
|
|
|
282
283
|
const eventSourceStream = new EventSourceStream({
|
|
283
284
|
eventSourceSettings: this.#state,
|
|
285
|
+
maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize,
|
|
284
286
|
push: (event) => {
|
|
285
287
|
this.dispatchEvent(createFastMessageEvent(
|
|
286
288
|
event.type,
|
|
@@ -465,7 +467,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
|
|
|
465
467
|
},
|
|
466
468
|
{
|
|
467
469
|
key: 'dispatcher', // undici only
|
|
468
|
-
converter: webidl.converters.any
|
|
470
|
+
converter: webidl.converters.any,
|
|
471
|
+
defaultValue: () => getGlobalDispatcher()
|
|
469
472
|
},
|
|
470
473
|
{
|
|
471
474
|
key: 'node', // undici only
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -923,7 +923,7 @@ function makeRequest (init) {
|
|
|
923
923
|
serviceWorkers: init.serviceWorkers ?? 'all',
|
|
924
924
|
initiator: init.initiator ?? '',
|
|
925
925
|
destination: init.destination ?? '',
|
|
926
|
-
priority: init.priority ??
|
|
926
|
+
priority: init.priority ?? 'auto',
|
|
927
927
|
origin: init.origin ?? 'client',
|
|
928
928
|
policyContainer: init.policyContainer ?? 'client',
|
|
929
929
|
referrer: init.referrer ?? 'client',
|
|
@@ -1129,8 +1129,7 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
|
|
|
1129
1129
|
{
|
|
1130
1130
|
key: 'priority',
|
|
1131
1131
|
converter: webidl.converters.DOMString,
|
|
1132
|
-
allowedValues: ['high', 'low', 'auto']
|
|
1133
|
-
defaultValue: () => 'auto'
|
|
1132
|
+
allowedValues: ['high', 'low', 'auto']
|
|
1134
1133
|
}
|
|
1135
1134
|
])
|
|
1136
1135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "8.10.
|
|
3
|
+
"version": "8.10.1",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"test:websocket:autobahn": "node test/autobahn/client.js",
|
|
98
98
|
"test:websocket:autobahn:report": "node test/autobahn/report.js",
|
|
99
99
|
"test:wpt:setup": "node test/web-platform-tests/wpt-runner.mjs setup",
|
|
100
|
-
"test:wpt": "npm run test:wpt:setup && node test/web-platform-tests/wpt-runner.mjs run /fetch /mimesniff /
|
|
100
|
+
"test:wpt": "npm run test:wpt:setup && node test/web-platform-tests/wpt-runner.mjs run /fetch /mimesniff /xhr /websockets /eventsource",
|
|
101
101
|
"test:cache-tests": "node test/cache-interceptor/cache-tests.mjs --ci",
|
|
102
102
|
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
|
|
103
103
|
"coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"prepare": "husky && node ./scripts/platform-shell.js"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
|
-
"@fastify/busboy": "3.2.
|
|
112
|
+
"@fastify/busboy": "3.2.2",
|
|
113
113
|
"@matteo.collina/tspl": "^0.2.0",
|
|
114
114
|
"@metcoder95/https-pem": "^1.0.0",
|
|
115
115
|
"@sinonjs/fake-timers": "^12.0.0",
|
package/types/client.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare namespace Client {
|
|
|
80
80
|
maxResponseSize?: number;
|
|
81
81
|
/** WebSocket-specific options */
|
|
82
82
|
webSocket?: Client.WebSocketOptions;
|
|
83
|
+
/** EventSource-specific options */
|
|
84
|
+
eventSource?: Client.EventSourceOptions;
|
|
83
85
|
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
|
|
84
86
|
autoSelectFamily?: boolean;
|
|
85
87
|
/** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
|
|
@@ -169,6 +171,14 @@ export declare namespace Client {
|
|
|
169
171
|
*/
|
|
170
172
|
settings?: Omit<SessionOptions['settings'], 'enablePush' | 'maxConcurrentStreams' | 'enableConnectProtocol'>
|
|
171
173
|
}
|
|
174
|
+
export interface EventSourceOptions {
|
|
175
|
+
/**
|
|
176
|
+
* Maximum allowed event size in bytes for EventSource messages.
|
|
177
|
+
* Set to 0 to disable the limit.
|
|
178
|
+
* @default buffer.kStringMaxLength
|
|
179
|
+
*/
|
|
180
|
+
maxEventSize?: number;
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
export default Client
|