undici 7.29.1 → 7.30.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/DiagnosticsChannel.md +3 -1
- package/docs/docs/api/Dispatcher.md +1 -0
- package/lib/core/request.js +23 -2
- package/lib/dispatcher/client-h1.js +11 -4
- package/lib/dispatcher/client-h2.js +14 -5
- package/lib/handler/retry-handler.js +71 -20
- package/lib/interceptor/decompress.js +228 -68
- package/lib/web/websocket/connection.js +18 -8
- package/package.json +1 -1
- package/types/interceptors.d.ts +1 -1
|
@@ -53,7 +53,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>
|
|
|
53
53
|
|
|
54
54
|
## `undici:request:headers`
|
|
55
55
|
|
|
56
|
-
This message is published after the response headers have been received.
|
|
56
|
+
This message is published after the response headers have been received. This includes a successful CONNECT or
|
|
57
|
+
protocol upgrade response.
|
|
57
58
|
|
|
58
59
|
```js
|
|
59
60
|
import diagnosticsChannel from 'diagnostics_channel'
|
|
@@ -82,6 +83,7 @@ diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ requ
|
|
|
82
83
|
## `undici:request:trailers`
|
|
83
84
|
|
|
84
85
|
This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
|
86
|
+
After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array.
|
|
85
87
|
|
|
86
88
|
```js
|
|
87
89
|
import diagnosticsChannel from 'diagnostics_channel'
|
|
@@ -1153,6 +1153,7 @@ The `decompress` interceptor automatically decompresses response bodies that are
|
|
|
1153
1153
|
|
|
1154
1154
|
- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
|
|
1155
1155
|
- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
|
|
1156
|
+
- `maxSize` - Maximum decompressed response size in bytes for each decompression stage. Set to `0` to disable the limit. Default: `0`.
|
|
1156
1157
|
|
|
1157
1158
|
**Example - Basic Decompress Interceptor**
|
|
1158
1159
|
|
package/lib/core/request.js
CHANGED
|
@@ -302,11 +302,32 @@ class Request {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
/**
|
|
306
|
+
* @param {number} statusCode
|
|
307
|
+
* @param {Buffer[]|string[]} headers
|
|
308
|
+
* @param {import('node:stream').Duplex} socket
|
|
309
|
+
* @param {string} [statusText]
|
|
310
|
+
*/
|
|
311
|
+
onUpgrade (statusCode, headers, socket, statusText = '') {
|
|
312
|
+
this.onFinally()
|
|
313
|
+
|
|
306
314
|
assert(!this.aborted)
|
|
307
315
|
assert(!this.completed)
|
|
308
316
|
|
|
309
|
-
|
|
317
|
+
if (channels.headers.hasSubscribers) {
|
|
318
|
+
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
|
|
322
|
+
|
|
323
|
+
if (!this.aborted) {
|
|
324
|
+
this.completed = true
|
|
325
|
+
if (channels.trailers.hasSubscribers) {
|
|
326
|
+
channels.trailers.publish({ request: this, trailers: [] })
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return result
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
onComplete (trailers) {
|
|
@@ -69,8 +69,14 @@ function lazyllhttp () {
|
|
|
69
69
|
|
|
70
70
|
let mod
|
|
71
71
|
|
|
72
|
-
// We disable wasm SIMD on
|
|
73
|
-
let useWasmSIMD =
|
|
72
|
+
// We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
|
|
73
|
+
let useWasmSIMD = true
|
|
74
|
+
if (process.arch === 'ppc64') {
|
|
75
|
+
const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
|
|
76
|
+
if (major < 24 || (major === 24 && minor < 12)) {
|
|
77
|
+
useWasmSIMD = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
74
80
|
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
|
|
75
81
|
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
|
|
76
82
|
useWasmSIMD = false
|
|
@@ -521,7 +527,7 @@ class Parser {
|
|
|
521
527
|
* @param {Buffer} head
|
|
522
528
|
*/
|
|
523
529
|
onUpgrade (head) {
|
|
524
|
-
const { upgrade, client, socket, headers, statusCode } = this
|
|
530
|
+
const { upgrade, client, socket, headers, statusCode, statusText } = this
|
|
525
531
|
|
|
526
532
|
assert(upgrade)
|
|
527
533
|
assert(client[kSocket] === socket)
|
|
@@ -556,8 +562,9 @@ class Parser {
|
|
|
556
562
|
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
|
557
563
|
|
|
558
564
|
try {
|
|
559
|
-
request.onUpgrade(statusCode, headers, socket)
|
|
565
|
+
request.onUpgrade(statusCode, headers, socket, statusText)
|
|
560
566
|
} catch (err) {
|
|
567
|
+
util.errorRequest(client, request, err)
|
|
561
568
|
util.destroy(socket, err)
|
|
562
569
|
}
|
|
563
570
|
|
|
@@ -575,13 +575,17 @@ function writeH2 (client, request) {
|
|
|
575
575
|
|
|
576
576
|
stream = session.request(headers, { endStream: false, signal })
|
|
577
577
|
stream[kHTTP2Stream] = true
|
|
578
|
+
++session[kOpenStreams]
|
|
578
579
|
|
|
579
580
|
stream.once('response', (headers, _flags) => {
|
|
580
581
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
581
582
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
try {
|
|
584
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
585
|
+
} catch (err) {
|
|
586
|
+
abort(err)
|
|
587
|
+
return
|
|
588
|
+
}
|
|
585
589
|
completeRequest(client, request)
|
|
586
590
|
})
|
|
587
591
|
|
|
@@ -610,11 +614,16 @@ function writeH2 (client, request) {
|
|
|
610
614
|
// We disabled endStream to allow the user to write to the stream
|
|
611
615
|
stream = session.request(headers, { endStream: false, signal })
|
|
612
616
|
stream[kHTTP2Stream] = true
|
|
617
|
+
++session[kOpenStreams]
|
|
613
618
|
stream.on('response', headers => {
|
|
614
619
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
615
620
|
|
|
616
|
-
|
|
617
|
-
|
|
621
|
+
try {
|
|
622
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
623
|
+
} catch (err) {
|
|
624
|
+
abort(err)
|
|
625
|
+
return
|
|
626
|
+
}
|
|
618
627
|
completeRequest(client, request)
|
|
619
628
|
})
|
|
620
629
|
stream.on('error', abort)
|
|
@@ -35,6 +35,55 @@ function validatePartialResponseContentLength (headers, range, statusCode, retry
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// A stable controller handed to the downstream handler for the lifetime of the
|
|
39
|
+
// request. Each transparent retry/resume is a separate dispatch with its own
|
|
40
|
+
// connection controller. The proxy always forwards to the active connection
|
|
41
|
+
// while preserving a downstream pause across controller replacement.
|
|
42
|
+
class RetryController {
|
|
43
|
+
#paused = false
|
|
44
|
+
#target = null
|
|
45
|
+
|
|
46
|
+
set target (target) {
|
|
47
|
+
this.#target = target
|
|
48
|
+
if (this.#paused) {
|
|
49
|
+
target?.pause()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get target () { return this.#target }
|
|
54
|
+
|
|
55
|
+
pause () {
|
|
56
|
+
this.#paused = true
|
|
57
|
+
this.#target?.pause()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
resume () {
|
|
61
|
+
this.#paused = false
|
|
62
|
+
this.#target?.resume()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
abort (reason) {
|
|
66
|
+
this.#target?.abort(reason)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get paused () { return this.#paused || (this.#target?.paused ?? false) }
|
|
70
|
+
get aborted () { return this.#target?.aborted ?? false }
|
|
71
|
+
get reason () { return this.#target?.reason ?? null }
|
|
72
|
+
get rawHeaders () { return this.#target?.rawHeaders ?? null }
|
|
73
|
+
set rawHeaders (value) {
|
|
74
|
+
if (this.#target) {
|
|
75
|
+
this.#target.rawHeaders = value
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get rawTrailers () { return this.#target?.rawTrailers ?? null }
|
|
80
|
+
set rawTrailers (value) {
|
|
81
|
+
if (this.#target) {
|
|
82
|
+
this.#target.rawTrailers = value
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
38
87
|
class RetryHandler {
|
|
39
88
|
constructor (opts, { dispatch, handler }) {
|
|
40
89
|
const { retryOptions, ...dispatchOpts } = opts
|
|
@@ -89,6 +138,7 @@ class RetryHandler {
|
|
|
89
138
|
this.start = 0
|
|
90
139
|
this.end = null
|
|
91
140
|
this.etag = null
|
|
141
|
+
this.controllerProxy = new RetryController()
|
|
92
142
|
}
|
|
93
143
|
|
|
94
144
|
onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
|
|
@@ -99,11 +149,11 @@ class RetryHandler {
|
|
|
99
149
|
// The downstream handler already received the response from an
|
|
100
150
|
// earlier attempt. Forwarding this response would replace the
|
|
101
151
|
// downstream body and leave the original body pending forever.
|
|
102
|
-
this.handler.onResponseError?.(
|
|
152
|
+
this.handler.onResponseError?.(this.controllerProxy, err)
|
|
103
153
|
} else {
|
|
104
154
|
this.headersSent = true
|
|
105
155
|
this.checkpointResponseEnd(headers)
|
|
106
|
-
this.handler.onResponseStart?.(
|
|
156
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
107
157
|
}
|
|
108
158
|
} else {
|
|
109
159
|
this.error = err
|
|
@@ -115,7 +165,7 @@ class RetryHandler {
|
|
|
115
165
|
if (isDisturbed(this.opts.body)) {
|
|
116
166
|
this.headersSent = true
|
|
117
167
|
this.checkpointResponseEnd(headers)
|
|
118
|
-
this.handler.onResponseStart?.(
|
|
168
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
119
169
|
return
|
|
120
170
|
}
|
|
121
171
|
|
|
@@ -125,11 +175,11 @@ class RetryHandler {
|
|
|
125
175
|
// The downstream handler already received the response from an
|
|
126
176
|
// earlier attempt. Forwarding this response would replace the
|
|
127
177
|
// downstream body and leave the original body pending forever.
|
|
128
|
-
this.handler.onResponseError?.(
|
|
178
|
+
this.handler.onResponseError?.(this.controllerProxy, passedErr)
|
|
129
179
|
} else {
|
|
130
180
|
this.headersSent = true
|
|
131
181
|
this.checkpointResponseEnd(headers)
|
|
132
|
-
this.handler.onResponseStart?.(
|
|
182
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
133
183
|
}
|
|
134
184
|
controller.resume()
|
|
135
185
|
return
|
|
@@ -165,13 +215,14 @@ class RetryHandler {
|
|
|
165
215
|
}
|
|
166
216
|
|
|
167
217
|
onRequestStart (controller, context) {
|
|
218
|
+
this.controllerProxy.target = controller
|
|
168
219
|
if (!this.headersSent) {
|
|
169
|
-
this.handler.onRequestStart?.(
|
|
220
|
+
this.handler.onRequestStart?.(this.controllerProxy, context)
|
|
170
221
|
}
|
|
171
222
|
}
|
|
172
223
|
|
|
173
|
-
onRequestUpgrade (
|
|
174
|
-
this.handler.onRequestUpgrade?.(
|
|
224
|
+
onRequestUpgrade (_controller, statusCode, headers, socket) {
|
|
225
|
+
this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
|
|
175
226
|
}
|
|
176
227
|
|
|
177
228
|
static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
|
|
@@ -302,7 +353,7 @@ class RetryHandler {
|
|
|
302
353
|
if (range == null) {
|
|
303
354
|
this.headersSent = true
|
|
304
355
|
this.handler.onResponseStart?.(
|
|
305
|
-
|
|
356
|
+
this.controllerProxy,
|
|
306
357
|
statusCode,
|
|
307
358
|
headers,
|
|
308
359
|
statusMessage
|
|
@@ -351,7 +402,7 @@ class RetryHandler {
|
|
|
351
402
|
|
|
352
403
|
this.headersSent = true
|
|
353
404
|
this.handler.onResponseStart?.(
|
|
354
|
-
|
|
405
|
+
this.controllerProxy,
|
|
355
406
|
statusCode,
|
|
356
407
|
headers,
|
|
357
408
|
statusMessage
|
|
@@ -364,30 +415,30 @@ class RetryHandler {
|
|
|
364
415
|
}
|
|
365
416
|
}
|
|
366
417
|
|
|
367
|
-
onResponseData (
|
|
418
|
+
onResponseData (_controller, chunk) {
|
|
368
419
|
if (this.error) {
|
|
369
420
|
return
|
|
370
421
|
}
|
|
371
422
|
|
|
372
423
|
this.start += chunk.length
|
|
373
424
|
|
|
374
|
-
this.handler.onResponseData?.(
|
|
425
|
+
this.handler.onResponseData?.(this.controllerProxy, chunk)
|
|
375
426
|
}
|
|
376
427
|
|
|
377
|
-
onResponseEnd (
|
|
428
|
+
onResponseEnd (_controller, trailers) {
|
|
378
429
|
if (this.error && this.retryOpts.throwOnError) {
|
|
379
430
|
throw this.error
|
|
380
431
|
}
|
|
381
432
|
|
|
382
433
|
if (!this.error) {
|
|
383
434
|
this.retryCount = 0
|
|
384
|
-
return this.handler.onResponseEnd?.(
|
|
435
|
+
return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
|
|
385
436
|
}
|
|
386
437
|
|
|
387
|
-
this.retry(
|
|
438
|
+
this.retry()
|
|
388
439
|
}
|
|
389
440
|
|
|
390
|
-
retry (
|
|
441
|
+
retry () {
|
|
391
442
|
if (this.start !== 0) {
|
|
392
443
|
const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
|
|
393
444
|
|
|
@@ -409,23 +460,23 @@ class RetryHandler {
|
|
|
409
460
|
this.retryCountCheckpoint = this.retryCount
|
|
410
461
|
this.dispatch(this.opts, this)
|
|
411
462
|
} catch (err) {
|
|
412
|
-
this.handler.onResponseError?.(
|
|
463
|
+
this.handler.onResponseError?.(this.controllerProxy, err)
|
|
413
464
|
}
|
|
414
465
|
}
|
|
415
466
|
|
|
416
467
|
onResponseError (controller, err) {
|
|
417
468
|
if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
|
|
418
|
-
this.handler.onResponseError?.(
|
|
469
|
+
this.handler.onResponseError?.(this.controllerProxy, err)
|
|
419
470
|
return
|
|
420
471
|
}
|
|
421
472
|
|
|
422
473
|
function shouldRetry (returnedErr) {
|
|
423
474
|
if (!returnedErr) {
|
|
424
|
-
this.retry(
|
|
475
|
+
this.retry()
|
|
425
476
|
return
|
|
426
477
|
}
|
|
427
478
|
|
|
428
|
-
this.handler?.onResponseError?.(
|
|
479
|
+
this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
|
|
429
480
|
}
|
|
430
481
|
|
|
431
482
|
// We reconcile in case of a mix between network errors
|
|
@@ -10,6 +10,60 @@ const { runtimeFeatures } = require('../util/runtime-features')
|
|
|
10
10
|
/** @typedef {import('node:stream').Transform} Controller */
|
|
11
11
|
/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
|
|
12
12
|
|
|
13
|
+
class DecompressController {
|
|
14
|
+
#onPause
|
|
15
|
+
#onResume
|
|
16
|
+
#onAbort
|
|
17
|
+
#paused = false
|
|
18
|
+
|
|
19
|
+
constructor (onPause, onResume, onAbort) {
|
|
20
|
+
this.#onPause = onPause
|
|
21
|
+
this.#onResume = onResume
|
|
22
|
+
this.#onAbort = onAbort
|
|
23
|
+
this.target = null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pause () {
|
|
27
|
+
if (this.#paused) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.#paused = true
|
|
32
|
+
this.#onPause()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resume () {
|
|
36
|
+
if (!this.#paused) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.#paused = false
|
|
41
|
+
this.#onResume()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
abort (reason) {
|
|
45
|
+
this.target?.abort(reason)
|
|
46
|
+
this.#onAbort(reason)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get paused () { return this.#paused }
|
|
50
|
+
get aborted () { return this.target?.aborted ?? false }
|
|
51
|
+
get reason () { return this.target?.reason ?? null }
|
|
52
|
+
get rawHeaders () { return this.target?.rawHeaders ?? null }
|
|
53
|
+
set rawHeaders (value) {
|
|
54
|
+
if (this.target) {
|
|
55
|
+
this.target.rawHeaders = value
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get rawTrailers () { return this.target?.rawTrailers ?? null }
|
|
60
|
+
set rawTrailers (value) {
|
|
61
|
+
if (this.target) {
|
|
62
|
+
this.target.rawTrailers = value
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
13
67
|
/** @type {Record<string, () => DecompressorStream>} */
|
|
14
68
|
const supportedEncodings = {
|
|
15
69
|
gzip: createGunzip,
|
|
@@ -22,7 +76,7 @@ const supportedEncodings = {
|
|
|
22
76
|
}
|
|
23
77
|
|
|
24
78
|
const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
|
|
25
|
-
const defaultMaxSize =
|
|
79
|
+
const defaultMaxSize = 0
|
|
26
80
|
|
|
27
81
|
/**
|
|
28
82
|
* Limits the output of one stage in a decompression chain.
|
|
@@ -54,7 +108,7 @@ let warningEmitted = /** @type {boolean} */ (false)
|
|
|
54
108
|
* @typedef {Object} DecompressHandlerOptions
|
|
55
109
|
* @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
|
|
56
110
|
* @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
|
|
57
|
-
* @property {number} [maxSize=
|
|
111
|
+
* @property {number} [maxSize=0] - Maximum decompressed response size in bytes. 0 disables the limit
|
|
58
112
|
*/
|
|
59
113
|
|
|
60
114
|
class DecompressHandler extends DecoratorHandler {
|
|
@@ -74,16 +128,130 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
74
128
|
#terminated = false
|
|
75
129
|
/** @type {boolean} */
|
|
76
130
|
#inputEnded = false
|
|
131
|
+
/** @type {boolean} */
|
|
132
|
+
#inputBackpressured = false
|
|
133
|
+
/** @type {boolean} */
|
|
134
|
+
#upstreamPaused = false
|
|
135
|
+
/** @type {boolean} */
|
|
136
|
+
#draining = false
|
|
137
|
+
/** @type {boolean} */
|
|
138
|
+
#drainRequested = false
|
|
139
|
+
/** @type {boolean} */
|
|
140
|
+
#completionPending = false
|
|
141
|
+
/** @type {DecompressorStream | undefined} */
|
|
142
|
+
#finalDecompressor
|
|
143
|
+
/** @type {DecompressController} */
|
|
144
|
+
#controller
|
|
77
145
|
|
|
78
146
|
constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true, maxSize = defaultMaxSize } = {}) {
|
|
79
|
-
if (!Number.isSafeInteger(maxSize) || maxSize <
|
|
80
|
-
throw new InvalidArgumentError('maxSize must be a
|
|
147
|
+
if (!Number.isSafeInteger(maxSize) || maxSize < 0) {
|
|
148
|
+
throw new InvalidArgumentError('maxSize must be a non-negative integer')
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
super(handler)
|
|
84
152
|
this.#skipStatusCodes = skipStatusCodes
|
|
85
153
|
this.#skipErrorResponses = skipErrorResponses
|
|
86
154
|
this.#maxSize = maxSize
|
|
155
|
+
this.#controller = new DecompressController(
|
|
156
|
+
() => this.#onDownstreamPause(),
|
|
157
|
+
() => this.#onDownstreamResume(),
|
|
158
|
+
reason => {
|
|
159
|
+
if (this.#inputEnded && !this.#terminated) {
|
|
160
|
+
this.onResponseError(this.#controller, reason)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#onDownstreamPause () {
|
|
167
|
+
this.#pauseUpstream()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#onDownstreamResume () {
|
|
171
|
+
const drainWasDeferred = this.#draining
|
|
172
|
+
this.#drainOutput()
|
|
173
|
+
if (!drainWasDeferred) {
|
|
174
|
+
this.#resumeUpstreamIfNeeded()
|
|
175
|
+
this.#finishIfReady()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#pauseUpstream () {
|
|
180
|
+
if (!this.#upstreamPaused && !this.#terminated) {
|
|
181
|
+
this.#upstreamPaused = true
|
|
182
|
+
this.#controller.target?.pause()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#resumeUpstreamIfNeeded () {
|
|
187
|
+
if (this.#upstreamPaused && !this.#controller.paused && !this.#inputBackpressured) {
|
|
188
|
+
this.#upstreamPaused = false
|
|
189
|
+
if (!this.#inputEnded) {
|
|
190
|
+
this.#controller.target?.resume()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#drainOutput () {
|
|
196
|
+
if (this.#terminated || this.#controller.paused || !this.#finalDecompressor) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.#draining) {
|
|
201
|
+
this.#drainRequested = true
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.#draining = true
|
|
206
|
+
try {
|
|
207
|
+
do {
|
|
208
|
+
this.#drainRequested = false
|
|
209
|
+
let chunk
|
|
210
|
+
while (!this.#terminated && !this.#controller.paused && (chunk = this.#finalDecompressor.read()) !== null) {
|
|
211
|
+
if (this.#maxSize > 0) {
|
|
212
|
+
const decompressedSize = this.#decompressedSize + chunk.length
|
|
213
|
+
if (decompressedSize > this.#maxSize) {
|
|
214
|
+
this.#fail(new ResponseExceededMaxSizeError(
|
|
215
|
+
`Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})`
|
|
216
|
+
))
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.#decompressedSize = decompressedSize
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = super.onResponseData(this.#controller, chunk)
|
|
224
|
+
if (result === false && !this.#controller.paused) {
|
|
225
|
+
this.#controller.pause()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
|
|
229
|
+
} finally {
|
|
230
|
+
this.#draining = false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.#resumeUpstreamIfNeeded()
|
|
234
|
+
this.#finishIfReady()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#finishIfReady () {
|
|
238
|
+
if (this.#terminated || !this.#completionPending || this.#controller.paused || this.#draining) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.#terminated = true
|
|
243
|
+
this.#cleanupDecompressors()
|
|
244
|
+
super.onResponseEnd(this.#controller, this.#trailers)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#onDecompressionEnd () {
|
|
248
|
+
if (this.#terminated) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.#completionPending = true
|
|
253
|
+
this.#drainOutput()
|
|
254
|
+
this.#finishIfReady()
|
|
87
255
|
}
|
|
88
256
|
|
|
89
257
|
/**
|
|
@@ -139,7 +307,7 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
139
307
|
const streams = []
|
|
140
308
|
for (let i = 0; i < decompressors.length; i++) {
|
|
141
309
|
streams.push(decompressors[i])
|
|
142
|
-
if (i < decompressors.length - 1) {
|
|
310
|
+
if (i < decompressors.length - 1 && this.#maxSize > 0) {
|
|
143
311
|
streams.push(createMaxSizeLimiter(this.#maxSize))
|
|
144
312
|
}
|
|
145
313
|
}
|
|
@@ -149,11 +317,10 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
149
317
|
|
|
150
318
|
/**
|
|
151
319
|
* Stops decompression and reports an error.
|
|
152
|
-
* @param {Controller} controller - The controller to coordinate with
|
|
153
320
|
* @param {Error} error - The decompression error
|
|
154
321
|
* @returns {void}
|
|
155
322
|
*/
|
|
156
|
-
#fail (
|
|
323
|
+
#fail (error) {
|
|
157
324
|
if (this.#terminated) {
|
|
158
325
|
return
|
|
159
326
|
}
|
|
@@ -161,75 +328,41 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
161
328
|
if (this.#inputEnded) {
|
|
162
329
|
// The request is already marked complete once the compressed input ends,
|
|
163
330
|
// so controller.abort() can no longer propagate decoder flush errors.
|
|
164
|
-
this.onResponseError(controller, error)
|
|
331
|
+
this.onResponseError(this.#controller, error)
|
|
165
332
|
} else {
|
|
166
|
-
controller.abort(error)
|
|
333
|
+
this.#controller.abort(error)
|
|
167
334
|
}
|
|
168
335
|
}
|
|
169
336
|
|
|
170
337
|
/**
|
|
171
|
-
* Sets up event handlers for
|
|
338
|
+
* Sets up event handlers for the final decompressor stream.
|
|
172
339
|
* @param {DecompressorStream} decompressor - The decompressor stream
|
|
173
|
-
* @param {Controller} controller - The controller to coordinate with
|
|
174
340
|
* @returns {void}
|
|
175
341
|
*/
|
|
176
|
-
#setupDecompressorEvents (decompressor
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let chunk
|
|
183
|
-
while ((chunk = decompressor.read()) !== null) {
|
|
184
|
-
const decompressedSize = this.#decompressedSize + chunk.length
|
|
185
|
-
if (decompressedSize > this.#maxSize) {
|
|
186
|
-
this.#fail(controller, new ResponseExceededMaxSizeError(
|
|
187
|
-
`Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})`
|
|
188
|
-
))
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
this.#decompressedSize = decompressedSize
|
|
193
|
-
const result = super.onResponseData(controller, chunk)
|
|
194
|
-
if (result === false) {
|
|
195
|
-
break
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
decompressor.on('error', (error) => {
|
|
201
|
-
this.#fail(controller, error)
|
|
202
|
-
})
|
|
342
|
+
#setupDecompressorEvents (decompressor) {
|
|
343
|
+
this.#finalDecompressor = decompressor
|
|
344
|
+
decompressor.on('readable', () => this.#drainOutput())
|
|
345
|
+
decompressor.on('error', (error) => this.#fail(error))
|
|
203
346
|
}
|
|
204
347
|
|
|
205
348
|
/**
|
|
206
349
|
* Sets up event handling for a single decompressor
|
|
207
|
-
* @param {Controller} controller - The controller to handle events
|
|
208
350
|
* @returns {void}
|
|
209
351
|
*/
|
|
210
|
-
#setupSingleDecompressor (
|
|
352
|
+
#setupSingleDecompressor () {
|
|
211
353
|
const decompressor = this.#decompressors[0]
|
|
212
|
-
this.#setupDecompressorEvents(decompressor
|
|
354
|
+
this.#setupDecompressorEvents(decompressor)
|
|
213
355
|
|
|
214
|
-
decompressor.on('end', () =>
|
|
215
|
-
if (this.#terminated) {
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
this.#terminated = true
|
|
220
|
-
this.#cleanupDecompressors()
|
|
221
|
-
super.onResponseEnd(controller, this.#trailers)
|
|
222
|
-
})
|
|
356
|
+
decompressor.on('end', () => this.#onDecompressionEnd())
|
|
223
357
|
}
|
|
224
358
|
|
|
225
359
|
/**
|
|
226
360
|
* Sets up event handling for multiple chained decompressors using pipeline
|
|
227
|
-
* @param {Controller} controller - The controller to handle events
|
|
228
361
|
* @returns {void}
|
|
229
362
|
*/
|
|
230
|
-
#setupMultipleDecompressors (
|
|
363
|
+
#setupMultipleDecompressors () {
|
|
231
364
|
const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
|
|
232
|
-
this.#setupDecompressorEvents(lastDecompressor
|
|
365
|
+
this.#setupDecompressorEvents(lastDecompressor)
|
|
233
366
|
|
|
234
367
|
pipeline(this.#decompressors, (err) => {
|
|
235
368
|
if (this.#terminated) {
|
|
@@ -237,13 +370,26 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
237
370
|
}
|
|
238
371
|
|
|
239
372
|
if (err) {
|
|
240
|
-
this.#fail(
|
|
373
|
+
this.#fail(err)
|
|
241
374
|
return
|
|
242
375
|
}
|
|
243
376
|
|
|
244
|
-
this.#
|
|
245
|
-
|
|
246
|
-
|
|
377
|
+
this.#onDecompressionEnd()
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#setupInputBackpressure () {
|
|
382
|
+
const decompressor = this.#decompressors[0]
|
|
383
|
+
decompressor.on('drain', () => {
|
|
384
|
+
if (this.#terminated) {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.#inputBackpressured = false
|
|
389
|
+
if (!this.#controller.paused) {
|
|
390
|
+
this.#drainOutput()
|
|
391
|
+
this.#resumeUpstreamIfNeeded()
|
|
392
|
+
}
|
|
247
393
|
})
|
|
248
394
|
}
|
|
249
395
|
|
|
@@ -253,6 +399,16 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
253
399
|
*/
|
|
254
400
|
#cleanupDecompressors () {
|
|
255
401
|
this.#decompressors.length = 0
|
|
402
|
+
this.#finalDecompressor = undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
onRequestStart (controller, context) {
|
|
406
|
+
this.#controller.target = controller
|
|
407
|
+
return super.onRequestStart(this.#controller, context)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
411
|
+
return super.onRequestUpgrade(this.#controller, statusCode, headers, socket)
|
|
256
412
|
}
|
|
257
413
|
|
|
258
414
|
/**
|
|
@@ -267,14 +423,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
267
423
|
|
|
268
424
|
// If content encoding is not supported or status code is in skip list
|
|
269
425
|
if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
|
|
270
|
-
return super.onResponseStart(controller, statusCode, headers, statusMessage)
|
|
426
|
+
return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
|
|
271
427
|
}
|
|
272
428
|
|
|
273
429
|
const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
|
|
274
430
|
|
|
275
431
|
if (decompressors.length === 0) {
|
|
276
432
|
this.#cleanupDecompressors()
|
|
277
|
-
return super.onResponseStart(controller, statusCode, headers, statusMessage)
|
|
433
|
+
return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
|
|
278
434
|
}
|
|
279
435
|
|
|
280
436
|
this.#decompressors = decompressors
|
|
@@ -282,8 +438,8 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
282
438
|
// Remove compression headers since we're decompressing
|
|
283
439
|
const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
|
|
284
440
|
|
|
285
|
-
if (controller
|
|
286
|
-
const rawHeaders = controller.rawHeaders
|
|
441
|
+
if (this.#controller.rawHeaders) {
|
|
442
|
+
const rawHeaders = this.#controller.rawHeaders
|
|
287
443
|
|
|
288
444
|
if (Array.isArray(rawHeaders)) {
|
|
289
445
|
const filteredHeaders = []
|
|
@@ -309,13 +465,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
309
465
|
}
|
|
310
466
|
}
|
|
311
467
|
|
|
468
|
+
this.#setupInputBackpressure()
|
|
312
469
|
if (this.#decompressors.length === 1) {
|
|
313
|
-
this.#setupSingleDecompressor(
|
|
470
|
+
this.#setupSingleDecompressor()
|
|
314
471
|
} else {
|
|
315
|
-
this.#setupMultipleDecompressors(
|
|
472
|
+
this.#setupMultipleDecompressors()
|
|
316
473
|
}
|
|
317
474
|
|
|
318
|
-
return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
|
|
475
|
+
return super.onResponseStart(this.#controller, statusCode, newHeaders, statusMessage)
|
|
319
476
|
}
|
|
320
477
|
|
|
321
478
|
/**
|
|
@@ -325,10 +482,13 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
325
482
|
*/
|
|
326
483
|
onResponseData (controller, chunk) {
|
|
327
484
|
if (this.#decompressors.length > 0) {
|
|
328
|
-
this.#decompressors[0].write(chunk)
|
|
485
|
+
if (!this.#decompressors[0].write(chunk)) {
|
|
486
|
+
this.#inputBackpressured = true
|
|
487
|
+
this.#pauseUpstream()
|
|
488
|
+
}
|
|
329
489
|
return
|
|
330
490
|
}
|
|
331
|
-
super.onResponseData(controller, chunk)
|
|
491
|
+
return super.onResponseData(this.#controller, chunk)
|
|
332
492
|
}
|
|
333
493
|
|
|
334
494
|
/**
|
|
@@ -343,7 +503,7 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
343
503
|
this.#decompressors[0].end()
|
|
344
504
|
return
|
|
345
505
|
}
|
|
346
|
-
super.onResponseEnd(controller, trailers)
|
|
506
|
+
return super.onResponseEnd(this.#controller, trailers)
|
|
347
507
|
}
|
|
348
508
|
|
|
349
509
|
/**
|
|
@@ -361,7 +521,7 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
361
521
|
decompressor.destroy()
|
|
362
522
|
}
|
|
363
523
|
this.#cleanupDecompressors()
|
|
364
|
-
super.onResponseError(controller, err)
|
|
524
|
+
super.onResponseError(this.#controller, err)
|
|
365
525
|
}
|
|
366
526
|
}
|
|
367
527
|
|
|
@@ -101,13 +101,13 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
101
101
|
// The presence of a session property on the socket indicates HTTP2
|
|
102
102
|
// HTTP1
|
|
103
103
|
if (response.socket?.session == null) {
|
|
104
|
-
|
|
104
|
+
failHandshake(handler, response, 1002, 'Received network error or non-101 status code.', response.error)
|
|
105
105
|
return
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// HTTP2
|
|
109
109
|
if (response.status !== 200) {
|
|
110
|
-
|
|
110
|
+
failHandshake(handler, response, 1002, 'Received network error or non-200 status code.', response.error)
|
|
111
111
|
return
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -122,7 +122,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
122
122
|
// header list results in null, failure, or the empty byte
|
|
123
123
|
// sequence, then fail the WebSocket connection.
|
|
124
124
|
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
|
|
125
|
-
|
|
125
|
+
failHandshake(handler, response, 1002, 'Server did not respond with sent protocols.')
|
|
126
126
|
return
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -138,7 +138,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
138
138
|
// _Fail the WebSocket Connection_.
|
|
139
139
|
// For H2, no upgrade header is expected.
|
|
140
140
|
if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
|
141
|
-
|
|
141
|
+
failHandshake(handler, response, 1002, 'Server did not set Upgrade header to "websocket".')
|
|
142
142
|
return
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -148,7 +148,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
148
148
|
// MUST _Fail the WebSocket Connection_.
|
|
149
149
|
// For H2, no connection header is expected.
|
|
150
150
|
if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
|
|
151
|
-
|
|
151
|
+
failHandshake(handler, response, 1002, 'Server did not set Connection header to "upgrade".')
|
|
152
152
|
return
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -162,7 +162,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
162
162
|
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
|
|
163
163
|
const digest = crypto.hash('sha1', keyValue + uid, 'base64')
|
|
164
164
|
if (secWSAccept !== digest) {
|
|
165
|
-
|
|
165
|
+
failHandshake(handler, response, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
|
|
166
166
|
return
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -180,7 +180,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
180
180
|
extensions = parseExtensions(secExtension)
|
|
181
181
|
|
|
182
182
|
if (!extensions.has('permessage-deflate')) {
|
|
183
|
-
|
|
183
|
+
failHandshake(handler, response, 1002, 'Sec-WebSocket-Extensions header does not match.')
|
|
184
184
|
return
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -201,7 +201,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
201
201
|
// the selected subprotocol values in its response for the connection to
|
|
202
202
|
// be established.
|
|
203
203
|
if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
|
|
204
|
-
|
|
204
|
+
failHandshake(handler, response, 1002, 'Protocol was not set in the opening handshake.')
|
|
205
205
|
return
|
|
206
206
|
}
|
|
207
207
|
}
|
|
@@ -296,6 +296,16 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
function failHandshake (handler, response, code, reason, cause) {
|
|
300
|
+
// The H2 upgrade request has already completed and handed off its stream.
|
|
301
|
+
// Aborting the request cannot close that stream after handshake validation fails.
|
|
302
|
+
if (response.socket?.session != null && !response.socket.destroyed) {
|
|
303
|
+
response.socket.destroy()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
failWebsocketConnection(handler, code, reason, cause)
|
|
307
|
+
}
|
|
308
|
+
|
|
299
309
|
/**
|
|
300
310
|
* @param {import('./websocket').Handler} handler
|
|
301
311
|
* @param {number} code
|
package/package.json
CHANGED
package/types/interceptors.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare namespace Interceptors {
|
|
|
12
12
|
export type DecompressInterceptorOpts = {
|
|
13
13
|
skipErrorResponses?: boolean
|
|
14
14
|
skipStatusCodes?: number[]
|
|
15
|
-
/** Maximum decompressed response size in bytes. @default
|
|
15
|
+
/** Maximum decompressed response size in bytes per stage. 0 disables the limit. @default 0 */
|
|
16
16
|
maxSize?: number
|
|
17
17
|
}
|
|
18
18
|
|