undici 7.23.0 → 7.24.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/Dispatcher.md +1 -0
- package/docs/docs/api/Errors.md +1 -0
- package/docs/docs/api/WebSocket.md +29 -0
- package/lib/core/errors.js +20 -1
- package/lib/core/request.js +12 -2
- package/lib/dispatcher/balanced-pool.js +9 -6
- package/lib/handler/deduplication-handler.js +306 -62
- package/lib/interceptor/deduplicate.js +15 -5
- package/lib/web/websocket/permessage-deflate.js +56 -3
- package/lib/web/websocket/receiver.js +18 -7
- package/lib/web/websocket/util.js +9 -1
- package/lib/web/websocket/websocket.js +22 -2
- package/package.json +1 -1
- package/types/errors.d.ts +6 -0
- package/types/interceptors.d.ts +7 -0
- package/types/websocket.d.ts +8 -1
|
@@ -1245,6 +1245,7 @@ The `deduplicate` interceptor deduplicates concurrent identical requests. When m
|
|
|
1245
1245
|
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to deduplicate. Default `['GET']`.
|
|
1246
1246
|
- `skipHeaderNames` - Header names that, if present in a request, will cause the request to skip deduplication entirely. Useful for headers like `idempotency-key` where presence indicates unique processing. Header name matching is case-insensitive. Default `[]`.
|
|
1247
1247
|
- `excludeHeaderNames` - Header names to exclude from the deduplication key. Requests with different values for these headers will still be deduplicated together. Useful for headers like `x-request-id` that vary per request but shouldn't affect deduplication. Header name matching is case-insensitive. Default `[]`.
|
|
1248
|
+
- `maxBufferSize` - Maximum bytes buffered per paused waiting deduplicated handler. If a waiting handler remains paused and exceeds this threshold, it is failed with an abort error to prevent unbounded memory growth. Default `5 * 1024 * 1024`.
|
|
1248
1249
|
|
|
1249
1250
|
**Usage**
|
|
1250
1251
|
|
package/docs/docs/api/Errors.md
CHANGED
|
@@ -26,6 +26,7 @@ import { errors } from 'undici'
|
|
|
26
26
|
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
|
|
27
27
|
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
|
|
28
28
|
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
|
|
29
|
+
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |
|
|
29
30
|
|
|
30
31
|
Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.
|
|
31
32
|
### `SocketError`
|
|
@@ -11,6 +11,15 @@ Arguments:
|
|
|
11
11
|
* **url** `URL | string`
|
|
12
12
|
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](/docs/docs/api/Dispatcher.md).
|
|
13
13
|
|
|
14
|
+
### WebSocketInit
|
|
15
|
+
|
|
16
|
+
When passing an object as the second argument, the following options are available:
|
|
17
|
+
|
|
18
|
+
* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use.
|
|
19
|
+
* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection.
|
|
20
|
+
* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request.
|
|
21
|
+
* **maxDecompressedMessageSize** `number` (optional) - Maximum allowed size in bytes for decompressed messages when using the `permessage-deflate` extension. **Default:** `4194304` (4 MB).
|
|
22
|
+
|
|
14
23
|
### Example:
|
|
15
24
|
|
|
16
25
|
This example will not work in browsers or other platforms that don't allow passing an object.
|
|
@@ -34,6 +43,26 @@ import { WebSocket } from 'undici'
|
|
|
34
43
|
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
|
|
35
44
|
```
|
|
36
45
|
|
|
46
|
+
### Example with custom decompression limit:
|
|
47
|
+
|
|
48
|
+
To protect against decompression bombs (small compressed payloads that expand to very large sizes), you can set a custom limit:
|
|
49
|
+
|
|
50
|
+
```mjs
|
|
51
|
+
import { WebSocket } from 'undici'
|
|
52
|
+
|
|
53
|
+
// Limit decompressed messages to 1 MB
|
|
54
|
+
const ws = new WebSocket('wss://echo.websocket.events', {
|
|
55
|
+
maxDecompressedMessageSize: 1 * 1024 * 1024
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
ws.addEventListener('error', (event) => {
|
|
59
|
+
// Connection will be closed if a message exceeds the limit
|
|
60
|
+
console.error('WebSocket error:', event.error)
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> ⚠️ **Security Note**: The `maxDecompressedMessageSize` option protects against memory exhaustion attacks where a malicious server sends a small compressed payload that decompresses to an extremely large size. If you increase this limit significantly above the default, ensure your application can handle the increased memory usage.
|
|
65
|
+
|
|
37
66
|
### Example with HTTP/2:
|
|
38
67
|
|
|
39
68
|
> ⚠️ Warning: WebSocket over HTTP/2 is experimental, it is likely to change in the future.
|
package/lib/core/errors.js
CHANGED
|
@@ -430,6 +430,24 @@ class Socks5ProxyError extends UndiciError {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
|
|
434
|
+
class MessageSizeExceededError extends UndiciError {
|
|
435
|
+
constructor (message) {
|
|
436
|
+
super(message)
|
|
437
|
+
this.name = 'MessageSizeExceededError'
|
|
438
|
+
this.message = message || 'Max decompressed message size exceeded'
|
|
439
|
+
this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
static [Symbol.hasInstance] (instance) {
|
|
443
|
+
return instance && instance[kMessageSizeExceededError] === true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
get [kMessageSizeExceededError] () {
|
|
447
|
+
return true
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
433
451
|
module.exports = {
|
|
434
452
|
AbortError,
|
|
435
453
|
HTTPParserError,
|
|
@@ -454,5 +472,6 @@ module.exports = {
|
|
|
454
472
|
ResponseError,
|
|
455
473
|
SecureProxyConnectionError,
|
|
456
474
|
MaxOriginsReachedError,
|
|
457
|
-
Socks5ProxyError
|
|
475
|
+
Socks5ProxyError,
|
|
476
|
+
MessageSizeExceededError
|
|
458
477
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -70,6 +70,10 @@ class Request {
|
|
|
70
70
|
throw new InvalidArgumentError('upgrade must be a string')
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
if (upgrade && !isValidHeaderValue(upgrade)) {
|
|
74
|
+
throw new InvalidArgumentError('invalid upgrade header')
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
|
|
74
78
|
throw new InvalidArgumentError('invalid headersTimeout')
|
|
75
79
|
}
|
|
@@ -385,13 +389,19 @@ function processHeader (request, key, val) {
|
|
|
385
389
|
val = `${val}`
|
|
386
390
|
}
|
|
387
391
|
|
|
388
|
-
if (
|
|
392
|
+
if (headerName === 'host') {
|
|
393
|
+
if (request.host !== null) {
|
|
394
|
+
throw new InvalidArgumentError('duplicate host header')
|
|
395
|
+
}
|
|
389
396
|
if (typeof val !== 'string') {
|
|
390
397
|
throw new InvalidArgumentError('invalid host header')
|
|
391
398
|
}
|
|
392
399
|
// Consumed by Client
|
|
393
400
|
request.host = val
|
|
394
|
-
} else if (
|
|
401
|
+
} else if (headerName === 'content-length') {
|
|
402
|
+
if (request.contentLength !== null) {
|
|
403
|
+
throw new InvalidArgumentError('duplicate content-length header')
|
|
404
|
+
}
|
|
395
405
|
request.contentLength = parseInt(val, 10)
|
|
396
406
|
if (!Number.isFinite(request.contentLength)) {
|
|
397
407
|
throw new InvalidArgumentError('invalid content-length header')
|
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
} = require('./pool-base')
|
|
15
15
|
const Pool = require('./pool')
|
|
16
16
|
const { kUrl } = require('../core/symbols')
|
|
17
|
-
const
|
|
17
|
+
const util = require('../core/util')
|
|
18
18
|
const kFactory = Symbol('factory')
|
|
19
19
|
|
|
20
20
|
const kOptions = Symbol('options')
|
|
@@ -56,7 +56,10 @@ class BalancedPool extends PoolBase {
|
|
|
56
56
|
|
|
57
57
|
super()
|
|
58
58
|
|
|
59
|
-
this[kOptions] = opts
|
|
59
|
+
this[kOptions] = { ...util.deepClone(opts) }
|
|
60
|
+
this[kOptions].interceptors = opts.interceptors
|
|
61
|
+
? { ...opts.interceptors }
|
|
62
|
+
: undefined
|
|
60
63
|
this[kIndex] = -1
|
|
61
64
|
this[kCurrentWeight] = 0
|
|
62
65
|
|
|
@@ -76,7 +79,7 @@ class BalancedPool extends PoolBase {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
addUpstream (upstream) {
|
|
79
|
-
const upstreamOrigin = parseOrigin(upstream).origin
|
|
82
|
+
const upstreamOrigin = util.parseOrigin(upstream).origin
|
|
80
83
|
|
|
81
84
|
if (this[kClients].find((pool) => (
|
|
82
85
|
pool[kUrl].origin === upstreamOrigin &&
|
|
@@ -85,7 +88,7 @@ class BalancedPool extends PoolBase {
|
|
|
85
88
|
))) {
|
|
86
89
|
return this
|
|
87
90
|
}
|
|
88
|
-
const pool = this[kFactory](upstreamOrigin,
|
|
91
|
+
const pool = this[kFactory](upstreamOrigin, this[kOptions])
|
|
89
92
|
|
|
90
93
|
this[kAddClient](pool)
|
|
91
94
|
pool.on('connect', () => {
|
|
@@ -125,7 +128,7 @@ class BalancedPool extends PoolBase {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
removeUpstream (upstream) {
|
|
128
|
-
const upstreamOrigin = parseOrigin(upstream).origin
|
|
131
|
+
const upstreamOrigin = util.parseOrigin(upstream).origin
|
|
129
132
|
|
|
130
133
|
const pool = this[kClients].find((pool) => (
|
|
131
134
|
pool[kUrl].origin === upstreamOrigin &&
|
|
@@ -141,7 +144,7 @@ class BalancedPool extends PoolBase {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
getUpstream (upstream) {
|
|
144
|
-
const upstreamOrigin = parseOrigin(upstream).origin
|
|
147
|
+
const upstreamOrigin = util.parseOrigin(upstream).origin
|
|
145
148
|
|
|
146
149
|
return this[kClients].find((pool) => (
|
|
147
150
|
pool[kUrl].origin === upstreamOrigin &&
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RequestAbortedError } = require('../core/errors')
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} WaitingHandler
|
|
13
|
+
* @property {DispatchHandler} handler
|
|
14
|
+
* @property {import('../../types/dispatcher.d.ts').default.DispatchController} controller
|
|
15
|
+
* @property {Buffer[]} bufferedChunks
|
|
16
|
+
* @property {number} bufferedBytes
|
|
17
|
+
* @property {object | null} pendingTrailers
|
|
18
|
+
* @property {boolean} done
|
|
19
|
+
*/
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
|
-
* Handler that
|
|
22
|
+
* Handler that forwards response events to multiple waiting handlers.
|
|
9
23
|
* Used for request deduplication.
|
|
10
24
|
*
|
|
11
25
|
* @implements {DispatchHandler}
|
|
@@ -17,14 +31,14 @@ class DeduplicationHandler {
|
|
|
17
31
|
#primaryHandler
|
|
18
32
|
|
|
19
33
|
/**
|
|
20
|
-
* @type {
|
|
34
|
+
* @type {WaitingHandler[]}
|
|
21
35
|
*/
|
|
22
36
|
#waitingHandlers = []
|
|
23
37
|
|
|
24
38
|
/**
|
|
25
|
-
* @type {
|
|
39
|
+
* @type {number}
|
|
26
40
|
*/
|
|
27
|
-
#
|
|
41
|
+
#maxBufferSize = DEFAULT_MAX_BUFFER_SIZE
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
44
|
* @type {number}
|
|
@@ -46,6 +60,21 @@ class DeduplicationHandler {
|
|
|
46
60
|
*/
|
|
47
61
|
#aborted = false
|
|
48
62
|
|
|
63
|
+
/**
|
|
64
|
+
* @type {boolean}
|
|
65
|
+
*/
|
|
66
|
+
#responseStarted = false
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @type {boolean}
|
|
70
|
+
*/
|
|
71
|
+
#responseDataStarted = false
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @type {boolean}
|
|
75
|
+
*/
|
|
76
|
+
#completed = false
|
|
77
|
+
|
|
49
78
|
/**
|
|
50
79
|
* @type {import('../../types/dispatcher.d.ts').default.DispatchController | null}
|
|
51
80
|
*/
|
|
@@ -59,22 +88,60 @@ class DeduplicationHandler {
|
|
|
59
88
|
/**
|
|
60
89
|
* @param {DispatchHandler} primaryHandler The primary handler
|
|
61
90
|
* @param {() => void} onComplete Callback when request completes
|
|
91
|
+
* @param {number} [maxBufferSize] Maximum paused buffer size per waiting handler
|
|
62
92
|
*/
|
|
63
|
-
constructor (primaryHandler, onComplete) {
|
|
93
|
+
constructor (primaryHandler, onComplete, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE) {
|
|
64
94
|
this.#primaryHandler = primaryHandler
|
|
65
95
|
this.#onComplete = onComplete
|
|
96
|
+
this.#maxBufferSize = maxBufferSize
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
/**
|
|
69
|
-
* Add a waiting handler that will receive
|
|
100
|
+
* Add a waiting handler that will receive response events.
|
|
101
|
+
* Returns false if deduplication can no longer safely attach this handler.
|
|
102
|
+
*
|
|
70
103
|
* @param {DispatchHandler} handler
|
|
104
|
+
* @returns {boolean}
|
|
71
105
|
*/
|
|
72
106
|
addWaitingHandler (handler) {
|
|
73
|
-
this.#
|
|
107
|
+
if (this.#completed || this.#responseDataStarted) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const waitingHandler = this.#createWaitingHandler(handler)
|
|
112
|
+
const waitingController = waitingHandler.controller
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
handler.onRequestStart?.(waitingController, null)
|
|
116
|
+
|
|
117
|
+
if (waitingController.aborted) {
|
|
118
|
+
waitingHandler.done = true
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.#responseStarted) {
|
|
123
|
+
handler.onResponseStart?.(
|
|
124
|
+
waitingController,
|
|
125
|
+
this.#statusCode,
|
|
126
|
+
this.#headers,
|
|
127
|
+
this.#statusMessage
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Ignore errors from waiting handlers
|
|
132
|
+
waitingHandler.done = true
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!waitingController.aborted) {
|
|
137
|
+
this.#waitingHandlers.push(waitingHandler)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true
|
|
74
141
|
}
|
|
75
142
|
|
|
76
143
|
/**
|
|
77
|
-
* @param {()
|
|
144
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
|
|
78
145
|
* @param {any} context
|
|
79
146
|
*/
|
|
80
147
|
onRequestStart (controller, context) {
|
|
@@ -99,10 +166,38 @@ class DeduplicationHandler {
|
|
|
99
166
|
* @param {string} statusMessage
|
|
100
167
|
*/
|
|
101
168
|
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
169
|
+
this.#responseStarted = true
|
|
102
170
|
this.#statusCode = statusCode
|
|
103
171
|
this.#headers = headers
|
|
104
172
|
this.#statusMessage = statusMessage
|
|
173
|
+
|
|
105
174
|
this.#primaryHandler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
|
175
|
+
|
|
176
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
177
|
+
const { handler, controller: waitingController } = waitingHandler
|
|
178
|
+
|
|
179
|
+
if (waitingHandler.done || waitingController.aborted) {
|
|
180
|
+
waitingHandler.done = true
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
handler.onResponseStart?.(
|
|
186
|
+
waitingController,
|
|
187
|
+
statusCode,
|
|
188
|
+
headers,
|
|
189
|
+
statusMessage
|
|
190
|
+
)
|
|
191
|
+
} catch {
|
|
192
|
+
// Ignore errors from waiting handlers
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (waitingController.aborted) {
|
|
196
|
+
waitingHandler.done = true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.#pruneDoneWaitingHandlers()
|
|
106
201
|
}
|
|
107
202
|
|
|
108
203
|
/**
|
|
@@ -110,9 +205,41 @@ class DeduplicationHandler {
|
|
|
110
205
|
* @param {Buffer} chunk
|
|
111
206
|
*/
|
|
112
207
|
onResponseData (controller, chunk) {
|
|
113
|
-
|
|
114
|
-
|
|
208
|
+
if (this.#aborted || this.#completed) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.#responseDataStarted = true
|
|
213
|
+
|
|
115
214
|
this.#primaryHandler.onResponseData?.(controller, chunk)
|
|
215
|
+
|
|
216
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
217
|
+
const { handler, controller: waitingController } = waitingHandler
|
|
218
|
+
|
|
219
|
+
if (waitingHandler.done || waitingController.aborted) {
|
|
220
|
+
waitingHandler.done = true
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (waitingController.paused) {
|
|
225
|
+
this.#bufferWaitingChunk(waitingHandler, chunk)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
handler.onResponseData?.(waitingController, chunk)
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore errors from waiting handlers
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (waitingController.aborted) {
|
|
236
|
+
waitingHandler.done = true
|
|
237
|
+
waitingHandler.bufferedChunks = []
|
|
238
|
+
waitingHandler.bufferedBytes = 0
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.#pruneDoneWaitingHandlers()
|
|
116
243
|
}
|
|
117
244
|
|
|
118
245
|
/**
|
|
@@ -120,8 +247,41 @@ class DeduplicationHandler {
|
|
|
120
247
|
* @param {object} trailers
|
|
121
248
|
*/
|
|
122
249
|
onResponseEnd (controller, trailers) {
|
|
250
|
+
if (this.#aborted || this.#completed) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.#completed = true
|
|
123
255
|
this.#primaryHandler.onResponseEnd?.(controller, trailers)
|
|
124
|
-
|
|
256
|
+
|
|
257
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
258
|
+
if (waitingHandler.done || waitingHandler.controller.aborted) {
|
|
259
|
+
waitingHandler.done = true
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.#flushWaitingHandler(waitingHandler)
|
|
264
|
+
|
|
265
|
+
if (waitingHandler.done || waitingHandler.controller.aborted) {
|
|
266
|
+
waitingHandler.done = true
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (waitingHandler.controller.paused && waitingHandler.bufferedChunks.length > 0) {
|
|
271
|
+
waitingHandler.pendingTrailers = trailers
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, trailers)
|
|
277
|
+
} catch {
|
|
278
|
+
// Ignore errors from waiting handlers
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
waitingHandler.done = true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.#pruneDoneWaitingHandlers()
|
|
125
285
|
this.#onComplete?.()
|
|
126
286
|
}
|
|
127
287
|
|
|
@@ -130,86 +290,170 @@ class DeduplicationHandler {
|
|
|
130
290
|
* @param {Error} err
|
|
131
291
|
*/
|
|
132
292
|
onResponseError (controller, err) {
|
|
293
|
+
if (this.#completed) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
133
297
|
this.#aborted = true
|
|
298
|
+
this.#completed = true
|
|
299
|
+
|
|
134
300
|
this.#primaryHandler.onResponseError?.(controller, err)
|
|
135
|
-
|
|
301
|
+
|
|
302
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
303
|
+
this.#errorWaitingHandler(waitingHandler, err)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.#waitingHandlers = []
|
|
136
307
|
this.#onComplete?.()
|
|
137
308
|
}
|
|
138
309
|
|
|
139
310
|
/**
|
|
140
|
-
*
|
|
311
|
+
* @param {DispatchHandler} handler
|
|
312
|
+
* @returns {WaitingHandler}
|
|
141
313
|
*/
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
get reason () { return null },
|
|
153
|
-
abort () {}
|
|
154
|
-
}
|
|
314
|
+
#createWaitingHandler (handler) {
|
|
315
|
+
/** @type {WaitingHandler} */
|
|
316
|
+
const waitingHandler = {
|
|
317
|
+
handler,
|
|
318
|
+
controller: null,
|
|
319
|
+
bufferedChunks: [],
|
|
320
|
+
bufferedBytes: 0,
|
|
321
|
+
pendingTrailers: null,
|
|
322
|
+
done: false
|
|
323
|
+
}
|
|
155
324
|
|
|
156
|
-
|
|
157
|
-
|
|
325
|
+
const state = {
|
|
326
|
+
aborted: false,
|
|
327
|
+
paused: false,
|
|
328
|
+
reason: null
|
|
329
|
+
}
|
|
158
330
|
|
|
159
|
-
|
|
160
|
-
|
|
331
|
+
waitingHandler.controller = {
|
|
332
|
+
resume: () => {
|
|
333
|
+
if (state.aborted) {
|
|
334
|
+
return
|
|
161
335
|
}
|
|
162
336
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.#statusCode,
|
|
166
|
-
this.#headers,
|
|
167
|
-
this.#statusMessage
|
|
168
|
-
)
|
|
337
|
+
state.paused = false
|
|
338
|
+
this.#flushWaitingHandler(waitingHandler)
|
|
169
339
|
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
340
|
+
if (
|
|
341
|
+
this.#completed &&
|
|
342
|
+
waitingHandler.pendingTrailers &&
|
|
343
|
+
waitingHandler.bufferedChunks.length === 0 &&
|
|
344
|
+
!state.paused &&
|
|
345
|
+
!state.aborted
|
|
346
|
+
) {
|
|
347
|
+
try {
|
|
348
|
+
waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, waitingHandler.pendingTrailers)
|
|
349
|
+
} catch {
|
|
350
|
+
// Ignore errors from waiting handlers
|
|
351
|
+
}
|
|
173
352
|
|
|
174
|
-
|
|
175
|
-
|
|
353
|
+
waitingHandler.pendingTrailers = null
|
|
354
|
+
waitingHandler.done = true
|
|
176
355
|
}
|
|
177
356
|
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
357
|
+
this.#pruneDoneWaitingHandlers()
|
|
358
|
+
},
|
|
359
|
+
pause: () => {
|
|
360
|
+
if (!state.aborted) {
|
|
361
|
+
state.paused = true
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
get paused () { return state.paused },
|
|
365
|
+
get aborted () { return state.aborted },
|
|
366
|
+
get reason () { return state.reason },
|
|
367
|
+
abort: (reason) => {
|
|
368
|
+
state.aborted = true
|
|
369
|
+
state.reason = reason ?? null
|
|
370
|
+
waitingHandler.done = true
|
|
371
|
+
waitingHandler.pendingTrailers = null
|
|
372
|
+
waitingHandler.bufferedChunks = []
|
|
373
|
+
waitingHandler.bufferedBytes = 0
|
|
181
374
|
}
|
|
182
375
|
}
|
|
183
376
|
|
|
184
|
-
|
|
185
|
-
this.#chunks = []
|
|
377
|
+
return waitingHandler
|
|
186
378
|
}
|
|
187
379
|
|
|
188
380
|
/**
|
|
189
|
-
*
|
|
190
|
-
* @param {
|
|
381
|
+
* @param {WaitingHandler} waitingHandler
|
|
382
|
+
* @param {Buffer} chunk
|
|
191
383
|
*/
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
384
|
+
#bufferWaitingChunk (waitingHandler, chunk) {
|
|
385
|
+
if (waitingHandler.done || waitingHandler.controller.aborted) {
|
|
386
|
+
waitingHandler.done = true
|
|
387
|
+
waitingHandler.bufferedChunks = []
|
|
388
|
+
waitingHandler.bufferedBytes = 0
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const bufferedChunk = Buffer.from(chunk)
|
|
393
|
+
waitingHandler.bufferedChunks.push(bufferedChunk)
|
|
394
|
+
waitingHandler.bufferedBytes += bufferedChunk.length
|
|
395
|
+
|
|
396
|
+
if (waitingHandler.bufferedBytes > this.#maxBufferSize) {
|
|
397
|
+
const err = new RequestAbortedError(`Deduplicated waiting handler exceeded maxBufferSize (${this.#maxBufferSize} bytes) while paused`)
|
|
398
|
+
this.#errorWaitingHandler(waitingHandler, err)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {WaitingHandler} waitingHandler
|
|
404
|
+
*/
|
|
405
|
+
#flushWaitingHandler (waitingHandler) {
|
|
406
|
+
const { handler, controller } = waitingHandler
|
|
407
|
+
|
|
408
|
+
while (
|
|
409
|
+
!waitingHandler.done &&
|
|
410
|
+
!controller.aborted &&
|
|
411
|
+
!controller.paused &&
|
|
412
|
+
waitingHandler.bufferedChunks.length > 0
|
|
413
|
+
) {
|
|
414
|
+
const bufferedChunk = waitingHandler.bufferedChunks.shift()
|
|
415
|
+
waitingHandler.bufferedBytes -= bufferedChunk.length
|
|
202
416
|
|
|
203
417
|
try {
|
|
204
|
-
handler.
|
|
205
|
-
handler.onResponseError?.(waitingController, err)
|
|
418
|
+
handler.onResponseData?.(controller, bufferedChunk)
|
|
206
419
|
} catch {
|
|
207
420
|
// Ignore errors from waiting handlers
|
|
208
421
|
}
|
|
422
|
+
|
|
423
|
+
if (controller.aborted) {
|
|
424
|
+
waitingHandler.done = true
|
|
425
|
+
waitingHandler.pendingTrailers = null
|
|
426
|
+
waitingHandler.bufferedChunks = []
|
|
427
|
+
waitingHandler.bufferedBytes = 0
|
|
428
|
+
break
|
|
429
|
+
}
|
|
209
430
|
}
|
|
431
|
+
}
|
|
210
432
|
|
|
211
|
-
|
|
212
|
-
|
|
433
|
+
/**
|
|
434
|
+
* @param {WaitingHandler} waitingHandler
|
|
435
|
+
* @param {Error} err
|
|
436
|
+
*/
|
|
437
|
+
#errorWaitingHandler (waitingHandler, err) {
|
|
438
|
+
if (waitingHandler.done) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
waitingHandler.done = true
|
|
443
|
+
waitingHandler.pendingTrailers = null
|
|
444
|
+
waitingHandler.bufferedChunks = []
|
|
445
|
+
waitingHandler.bufferedBytes = 0
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
waitingHandler.controller.abort(err)
|
|
449
|
+
waitingHandler.handler.onResponseError?.(waitingHandler.controller, err)
|
|
450
|
+
} catch {
|
|
451
|
+
// Ignore errors from waiting handlers
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#pruneDoneWaitingHandlers () {
|
|
456
|
+
this.#waitingHandlers = this.#waitingHandlers.filter(waitingHandler => waitingHandler.done === false)
|
|
213
457
|
}
|
|
214
458
|
}
|
|
215
459
|
|
|
@@ -15,7 +15,8 @@ module.exports = (opts = {}) => {
|
|
|
15
15
|
const {
|
|
16
16
|
methods = ['GET'],
|
|
17
17
|
skipHeaderNames = [],
|
|
18
|
-
excludeHeaderNames = []
|
|
18
|
+
excludeHeaderNames = [],
|
|
19
|
+
maxBufferSize = 5 * 1024 * 1024
|
|
19
20
|
} = opts
|
|
20
21
|
|
|
21
22
|
if (typeof opts !== 'object' || opts === null) {
|
|
@@ -40,6 +41,10 @@ module.exports = (opts = {}) => {
|
|
|
40
41
|
throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
|
|
45
|
+
throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
// Convert to lowercase Set for case-insensitive header matching
|
|
44
49
|
const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
|
|
45
50
|
|
|
@@ -78,9 +83,13 @@ module.exports = (opts = {}) => {
|
|
|
78
83
|
// Check if there's already a pending request for this key
|
|
79
84
|
const pendingHandler = pendingRequests.get(dedupeKey)
|
|
80
85
|
if (pendingHandler) {
|
|
81
|
-
// Add this handler to the waiting list
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// Add this handler to the waiting list when safe.
|
|
87
|
+
// If body streaming has already started, this request must be sent independently.
|
|
88
|
+
if (pendingHandler.addWaitingHandler(handler)) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return dispatch(opts, handler)
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
// Create a new deduplication handler
|
|
@@ -92,7 +101,8 @@ module.exports = (opts = {}) => {
|
|
|
92
101
|
if (pendingRequestsChannel.hasSubscribers) {
|
|
93
102
|
pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
|
|
94
103
|
}
|
|
95
|
-
}
|
|
104
|
+
},
|
|
105
|
+
maxBufferSize
|
|
96
106
|
)
|
|
97
107
|
|
|
98
108
|
// Register the pending request
|
|
@@ -2,20 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
|
|
4
4
|
const { isValidClientWindowBits } = require('./util')
|
|
5
|
+
const { MessageSizeExceededError } = require('../../core/errors')
|
|
5
6
|
|
|
6
7
|
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
|
7
8
|
const kBuffer = Symbol('kBuffer')
|
|
8
9
|
const kLength = Symbol('kLength')
|
|
9
10
|
|
|
11
|
+
// Default maximum decompressed message size: 4 MB
|
|
12
|
+
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
|
|
13
|
+
|
|
10
14
|
class PerMessageDeflate {
|
|
11
15
|
/** @type {import('node:zlib').InflateRaw} */
|
|
12
16
|
#inflate
|
|
13
17
|
|
|
14
18
|
#options = {}
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
/** @type {number} */
|
|
21
|
+
#maxDecompressedSize
|
|
22
|
+
|
|
23
|
+
/** @type {boolean} */
|
|
24
|
+
#aborted = false
|
|
25
|
+
|
|
26
|
+
/** @type {Function|null} */
|
|
27
|
+
#currentCallback = null
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Map<string, string>} extensions
|
|
31
|
+
* @param {{ maxDecompressedMessageSize?: number }} [options]
|
|
32
|
+
*/
|
|
33
|
+
constructor (extensions, options = {}) {
|
|
17
34
|
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
|
18
35
|
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
|
36
|
+
this.#maxDecompressedSize = options.maxDecompressedMessageSize ?? kDefaultMaxDecompressedSize
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
decompress (chunk, fin, callback) {
|
|
@@ -24,6 +42,11 @@ class PerMessageDeflate {
|
|
|
24
42
|
// payload of the message.
|
|
25
43
|
// 2. Decompress the resulting data using DEFLATE.
|
|
26
44
|
|
|
45
|
+
if (this.#aborted) {
|
|
46
|
+
callback(new MessageSizeExceededError())
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
if (!this.#inflate) {
|
|
28
51
|
let windowBits = Z_DEFAULT_WINDOWBITS
|
|
29
52
|
|
|
@@ -36,13 +59,37 @@ class PerMessageDeflate {
|
|
|
36
59
|
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
|
|
37
60
|
}
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
try {
|
|
63
|
+
this.#inflate = createInflateRaw({ windowBits })
|
|
64
|
+
} catch (err) {
|
|
65
|
+
callback(err)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
40
68
|
this.#inflate[kBuffer] = []
|
|
41
69
|
this.#inflate[kLength] = 0
|
|
42
70
|
|
|
43
71
|
this.#inflate.on('data', (data) => {
|
|
44
|
-
this.#
|
|
72
|
+
if (this.#aborted) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
this.#inflate[kLength] += data.length
|
|
77
|
+
|
|
78
|
+
if (this.#inflate[kLength] > this.#maxDecompressedSize) {
|
|
79
|
+
this.#aborted = true
|
|
80
|
+
this.#inflate.removeAllListeners()
|
|
81
|
+
this.#inflate.destroy()
|
|
82
|
+
this.#inflate = null
|
|
83
|
+
|
|
84
|
+
if (this.#currentCallback) {
|
|
85
|
+
const cb = this.#currentCallback
|
|
86
|
+
this.#currentCallback = null
|
|
87
|
+
cb(new MessageSizeExceededError())
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#inflate[kBuffer].push(data)
|
|
46
93
|
})
|
|
47
94
|
|
|
48
95
|
this.#inflate.on('error', (err) => {
|
|
@@ -51,16 +98,22 @@ class PerMessageDeflate {
|
|
|
51
98
|
})
|
|
52
99
|
}
|
|
53
100
|
|
|
101
|
+
this.#currentCallback = callback
|
|
54
102
|
this.#inflate.write(chunk)
|
|
55
103
|
if (fin) {
|
|
56
104
|
this.#inflate.write(tail)
|
|
57
105
|
}
|
|
58
106
|
|
|
59
107
|
this.#inflate.flush(() => {
|
|
108
|
+
if (this.#aborted || !this.#inflate) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
60
112
|
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
|
|
61
113
|
|
|
62
114
|
this.#inflate[kBuffer].length = 0
|
|
63
115
|
this.#inflate[kLength] = 0
|
|
116
|
+
this.#currentCallback = null
|
|
64
117
|
|
|
65
118
|
callback(null, full)
|
|
66
119
|
})
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
const { failWebsocketConnection } = require('./connection')
|
|
16
16
|
const { WebsocketFrameSend } = require('./frame')
|
|
17
17
|
const { PerMessageDeflate } = require('./permessage-deflate')
|
|
18
|
+
const { MessageSizeExceededError } = require('../../core/errors')
|
|
18
19
|
|
|
19
20
|
// This code was influenced by ws released under the MIT license.
|
|
20
21
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
@@ -38,14 +39,23 @@ class ByteParser extends Writable {
|
|
|
38
39
|
/** @type {import('./websocket').Handler} */
|
|
39
40
|
#handler
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
/** @type {{ maxDecompressedMessageSize?: number }} */
|
|
43
|
+
#options
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {import('./websocket').Handler} handler
|
|
47
|
+
* @param {Map<string, string>|null} extensions
|
|
48
|
+
* @param {{ maxDecompressedMessageSize?: number }} [options]
|
|
49
|
+
*/
|
|
50
|
+
constructor (handler, extensions, options = {}) {
|
|
42
51
|
super()
|
|
43
52
|
|
|
44
53
|
this.#handler = handler
|
|
45
54
|
this.#extensions = extensions == null ? new Map() : extensions
|
|
55
|
+
this.#options = options
|
|
46
56
|
|
|
47
57
|
if (this.#extensions.has('permessage-deflate')) {
|
|
48
|
-
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
|
58
|
+
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -180,6 +190,7 @@ class ByteParser extends Writable {
|
|
|
180
190
|
|
|
181
191
|
const buffer = this.consume(8)
|
|
182
192
|
const upper = buffer.readUInt32BE(0)
|
|
193
|
+
const lower = buffer.readUInt32BE(4)
|
|
183
194
|
|
|
184
195
|
// 2^31 is the maximum bytes an arraybuffer can contain
|
|
185
196
|
// on 32-bit systems. Although, on 64-bit systems, this is
|
|
@@ -187,14 +198,12 @@ class ByteParser extends Writable {
|
|
|
187
198
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
|
|
188
199
|
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
|
|
189
200
|
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
|
|
190
|
-
if (upper > 2 ** 31 - 1) {
|
|
201
|
+
if (upper !== 0 || lower > 2 ** 31 - 1) {
|
|
191
202
|
failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
|
|
192
203
|
return
|
|
193
204
|
}
|
|
194
205
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.#info.payloadLength = (upper << 8) + lower
|
|
206
|
+
this.#info.payloadLength = lower
|
|
198
207
|
this.#state = parserStates.READ_DATA
|
|
199
208
|
} else if (this.#state === parserStates.READ_DATA) {
|
|
200
209
|
if (this.#byteOffset < this.#info.payloadLength) {
|
|
@@ -222,7 +231,9 @@ class ByteParser extends Writable {
|
|
|
222
231
|
} else {
|
|
223
232
|
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
|
224
233
|
if (error) {
|
|
225
|
-
|
|
234
|
+
// Use 1009 (Message Too Big) for decompression size limit errors
|
|
235
|
+
const code = error instanceof MessageSizeExceededError ? 1009 : 1007
|
|
236
|
+
failWebsocketConnection(this.#handler, code, error.message)
|
|
226
237
|
return
|
|
227
238
|
}
|
|
228
239
|
|
|
@@ -227,6 +227,12 @@ function parseExtensions (extensions) {
|
|
|
227
227
|
* @returns {boolean}
|
|
228
228
|
*/
|
|
229
229
|
function isValidClientWindowBits (value) {
|
|
230
|
+
// Must have at least one character
|
|
231
|
+
if (value.length === 0) {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check all characters are ASCII digits
|
|
230
236
|
for (let i = 0; i < value.length; i++) {
|
|
231
237
|
const byte = value.charCodeAt(i)
|
|
232
238
|
|
|
@@ -235,7 +241,9 @@ function isValidClientWindowBits (value) {
|
|
|
235
241
|
}
|
|
236
242
|
}
|
|
237
243
|
|
|
238
|
-
|
|
244
|
+
// Check numeric range: zlib requires windowBits in range 8-15
|
|
245
|
+
const num = Number.parseInt(value, 10)
|
|
246
|
+
return num >= 8 && num <= 15
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
/**
|
|
@@ -109,6 +109,8 @@ class WebSocket extends EventTarget {
|
|
|
109
109
|
#binaryType
|
|
110
110
|
/** @type {import('./receiver').ByteParser} */
|
|
111
111
|
#parser
|
|
112
|
+
/** @type {{ maxDecompressedMessageSize?: number }} */
|
|
113
|
+
#options
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* @param {string} url
|
|
@@ -154,6 +156,11 @@ class WebSocket extends EventTarget {
|
|
|
154
156
|
// 5. Set this's url to urlRecord.
|
|
155
157
|
this.#url = new URL(urlRecord.href)
|
|
156
158
|
|
|
159
|
+
// Store options for later use (e.g., maxDecompressedMessageSize)
|
|
160
|
+
this.#options = {
|
|
161
|
+
maxDecompressedMessageSize: options.maxDecompressedMessageSize
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
// 6. Let client be this's relevant settings object.
|
|
158
165
|
const client = environmentSettingsObject.settingsObject
|
|
159
166
|
|
|
@@ -452,11 +459,11 @@ class WebSocket extends EventTarget {
|
|
|
452
459
|
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
|
453
460
|
*/
|
|
454
461
|
#onConnectionEstablished (response, parsedExtensions) {
|
|
455
|
-
// processResponse is called when the "response
|
|
462
|
+
// processResponse is called when the "response's header list has been received and initialized."
|
|
456
463
|
// once this happens, the connection is open
|
|
457
464
|
this.#handler.socket = response.socket
|
|
458
465
|
|
|
459
|
-
const parser = new ByteParser(this.#handler, parsedExtensions)
|
|
466
|
+
const parser = new ByteParser(this.#handler, parsedExtensions, this.#options)
|
|
460
467
|
parser.on('drain', () => this.#handler.onParserDrain())
|
|
461
468
|
parser.on('error', (err) => this.#handler.onParserError(err))
|
|
462
469
|
|
|
@@ -708,6 +715,19 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
|
|
|
708
715
|
{
|
|
709
716
|
key: 'headers',
|
|
710
717
|
converter: webidl.nullableConverter(webidl.converters.HeadersInit)
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
key: 'maxDecompressedMessageSize',
|
|
721
|
+
converter: webidl.nullableConverter((V) => {
|
|
722
|
+
V = webidl.converters['unsigned long long'](V)
|
|
723
|
+
if (V <= 0) {
|
|
724
|
+
throw webidl.errors.exception({
|
|
725
|
+
header: 'WebSocket constructor',
|
|
726
|
+
message: 'maxDecompressedMessageSize must be greater than 0'
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
return V
|
|
730
|
+
})
|
|
711
731
|
}
|
|
712
732
|
])
|
|
713
733
|
|
package/package.json
CHANGED
package/types/errors.d.ts
CHANGED
|
@@ -168,4 +168,10 @@ declare namespace Errors {
|
|
|
168
168
|
name: 'Socks5ProxyError'
|
|
169
169
|
code: string
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
/** WebSocket decompressed message exceeded maximum size. */
|
|
173
|
+
export class MessageSizeExceededError extends UndiciError {
|
|
174
|
+
name: 'MessageSizeExceededError'
|
|
175
|
+
code: 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
|
|
176
|
+
}
|
|
171
177
|
}
|
package/types/interceptors.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ declare namespace Interceptors {
|
|
|
60
60
|
* @default []
|
|
61
61
|
*/
|
|
62
62
|
excludeHeaderNames?: string[]
|
|
63
|
+
/**
|
|
64
|
+
* Maximum bytes buffered per paused waiting deduplicated handler.
|
|
65
|
+
* If a waiting handler remains paused and exceeds this threshold,
|
|
66
|
+
* it is failed with an abort error to prevent unbounded memory growth.
|
|
67
|
+
* @default 5 * 1024 * 1024
|
|
68
|
+
*/
|
|
69
|
+
maxBufferSize?: number
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
export function dump (opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
|
package/types/websocket.d.ts
CHANGED
|
@@ -147,7 +147,14 @@ export declare const ErrorEvent: {
|
|
|
147
147
|
interface WebSocketInit {
|
|
148
148
|
protocols?: string | string[],
|
|
149
149
|
dispatcher?: Dispatcher,
|
|
150
|
-
headers?: HeadersInit
|
|
150
|
+
headers?: HeadersInit,
|
|
151
|
+
/**
|
|
152
|
+
* Maximum size in bytes for decompressed WebSocket messages.
|
|
153
|
+
* When a message exceeds this limit during decompression, the connection
|
|
154
|
+
* will be closed with status code 1009 (Message Too Big).
|
|
155
|
+
* @default 4194304 (4 MB)
|
|
156
|
+
*/
|
|
157
|
+
maxDecompressedMessageSize?: number
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
interface WebSocketStreamOptions {
|