undici 7.14.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/docs/api/Agent.md +1 -0
- package/docs/docs/api/Dispatcher.md +59 -0
- package/docs/docs/api/Errors.md +0 -1
- package/index-fetch.js +2 -2
- package/index.js +6 -9
- package/lib/api/api-request.js +22 -8
- package/lib/api/readable.js +7 -5
- package/lib/core/errors.js +217 -13
- package/lib/core/request.js +5 -1
- package/lib/core/util.js +45 -11
- package/lib/dispatcher/agent.js +44 -23
- package/lib/dispatcher/client-h1.js +20 -9
- package/lib/dispatcher/client-h2.js +13 -3
- package/lib/dispatcher/client.js +57 -57
- package/lib/dispatcher/dispatcher-base.js +12 -7
- package/lib/dispatcher/env-http-proxy-agent.js +12 -16
- package/lib/dispatcher/fixed-queue.js +15 -39
- package/lib/dispatcher/h2c-client.js +6 -6
- package/lib/dispatcher/pool-base.js +60 -43
- package/lib/dispatcher/pool.js +2 -2
- package/lib/dispatcher/proxy-agent.js +14 -9
- package/lib/global.js +19 -1
- package/lib/interceptor/cache.js +61 -0
- package/lib/interceptor/decompress.js +253 -0
- package/lib/llhttp/constants.d.ts +99 -1
- package/lib/llhttp/constants.js +34 -1
- package/lib/llhttp/llhttp-wasm.js +1 -1
- package/lib/llhttp/llhttp_simd-wasm.js +1 -1
- package/lib/llhttp/utils.d.ts +2 -2
- package/lib/llhttp/utils.js +3 -6
- package/lib/mock/mock-agent.js +4 -4
- package/lib/mock/mock-errors.js +10 -0
- package/lib/mock/mock-utils.js +12 -10
- package/lib/util/cache.js +6 -7
- package/lib/util/date.js +534 -140
- package/lib/web/cookies/index.js +1 -1
- package/lib/web/cookies/parse.js +2 -2
- package/lib/web/eventsource/eventsource-stream.js +2 -2
- package/lib/web/eventsource/eventsource.js +34 -29
- package/lib/web/eventsource/util.js +1 -9
- package/lib/web/fetch/body.js +20 -26
- package/lib/web/fetch/index.js +15 -16
- package/lib/web/fetch/response.js +2 -4
- package/lib/web/fetch/util.js +8 -230
- package/lib/web/subresource-integrity/Readme.md +9 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +306 -0
- package/lib/web/webidl/index.js +203 -42
- package/lib/web/websocket/connection.js +4 -3
- package/lib/web/websocket/events.js +1 -1
- package/lib/web/websocket/stream/websocketerror.js +22 -1
- package/lib/web/websocket/stream/websocketstream.js +16 -7
- package/lib/web/websocket/websocket.js +32 -42
- package/package.json +9 -7
- package/types/agent.d.ts +1 -0
- package/types/diagnostics-channel.d.ts +0 -1
- package/types/errors.d.ts +5 -15
- package/types/interceptors.d.ts +5 -0
- package/types/snapshot-agent.d.ts +5 -3
- package/types/webidl.d.ts +82 -21
- package/lib/api/util.js +0 -95
- package/lib/llhttp/constants.js.map +0 -1
- package/lib/llhttp/utils.js.map +0 -1
|
@@ -12,8 +12,6 @@ class H2CClient extends DispatcherBase {
|
|
|
12
12
|
#client = null
|
|
13
13
|
|
|
14
14
|
constructor (origin, clientOpts) {
|
|
15
|
-
super()
|
|
16
|
-
|
|
17
15
|
if (typeof origin === 'string') {
|
|
18
16
|
origin = new URL(origin)
|
|
19
17
|
}
|
|
@@ -47,6 +45,8 @@ class H2CClient extends DispatcherBase {
|
|
|
47
45
|
)
|
|
48
46
|
}
|
|
49
47
|
|
|
48
|
+
super()
|
|
49
|
+
|
|
50
50
|
this.#client = new Client(origin, {
|
|
51
51
|
...opts,
|
|
52
52
|
connect: this.#buildConnector(connect),
|
|
@@ -110,12 +110,12 @@ class H2CClient extends DispatcherBase {
|
|
|
110
110
|
return this.#client.dispatch(opts, handler)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
[kClose] () {
|
|
114
|
+
return this.#client.close()
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
[kDestroy] () {
|
|
118
|
+
return this.#client.destroy()
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -18,54 +18,55 @@ const kAddClient = Symbol('add client')
|
|
|
18
18
|
const kRemoveClient = Symbol('remove client')
|
|
19
19
|
|
|
20
20
|
class PoolBase extends DispatcherBase {
|
|
21
|
-
|
|
22
|
-
super()
|
|
21
|
+
[kQueue] = new FixedQueue();
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
this[kClients] = []
|
|
26
|
-
this[kQueued] = 0
|
|
23
|
+
[kQueued] = 0;
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
[kClients] = [];
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
const queue = pool[kQueue]
|
|
27
|
+
[kNeedDrain] = false;
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
[kOnDrain] (client, origin, targets) {
|
|
30
|
+
const queue = this[kQueue]
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
needDrain = !this.dispatch(item.opts, item.handler)
|
|
32
|
+
let needDrain = false
|
|
33
|
+
|
|
34
|
+
while (!needDrain) {
|
|
35
|
+
const item = queue.shift()
|
|
36
|
+
if (!item) {
|
|
37
|
+
break
|
|
42
38
|
}
|
|
39
|
+
this[kQueued]--
|
|
40
|
+
needDrain = !client.dispatch(item.opts, item.handler)
|
|
41
|
+
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
client[kNeedDrain] = needDrain
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
if (!needDrain && this[kNeedDrain]) {
|
|
46
|
+
this[kNeedDrain] = false
|
|
47
|
+
this.emit('drain', origin, [this, ...targets])
|
|
48
|
+
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (this[kClosedResolve] && queue.isEmpty()) {
|
|
51
|
+
const closeAll = new Array(this[kClients].length)
|
|
52
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
53
|
+
closeAll[i] = this[kClients][i].close()
|
|
55
54
|
}
|
|
55
|
+
Promise.all(closeAll)
|
|
56
|
+
.then(this[kClosedResolve])
|
|
56
57
|
}
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
[kOnConnect] = (origin, targets) => {
|
|
61
|
+
this.emit('connect', origin, [this, ...targets])
|
|
62
|
+
};
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
[kOnDisconnect] = (origin, targets, err) => {
|
|
65
|
+
this.emit('disconnect', origin, [this, ...targets], err)
|
|
66
|
+
};
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
68
|
+
[kOnConnectionError] = (origin, targets, err) => {
|
|
69
|
+
this.emit('connectionError', origin, [this, ...targets], err)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
get [kBusy] () {
|
|
@@ -73,11 +74,19 @@ class PoolBase extends DispatcherBase {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
get [kConnected] () {
|
|
76
|
-
|
|
77
|
+
let ret = 0
|
|
78
|
+
for (const { [kConnected]: connected } of this[kClients]) {
|
|
79
|
+
ret += connected
|
|
80
|
+
}
|
|
81
|
+
return ret
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
get [kFree] () {
|
|
80
|
-
|
|
85
|
+
let ret = 0
|
|
86
|
+
for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
|
|
87
|
+
ret += connected && !needDrain
|
|
88
|
+
}
|
|
89
|
+
return ret
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
get [kPending] () {
|
|
@@ -108,17 +117,21 @@ class PoolBase extends DispatcherBase {
|
|
|
108
117
|
return new PoolStats(this)
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
[kClose] () {
|
|
112
121
|
if (this[kQueue].isEmpty()) {
|
|
113
|
-
|
|
122
|
+
const closeAll = new Array(this[kClients].length)
|
|
123
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
124
|
+
closeAll[i] = this[kClients][i].close()
|
|
125
|
+
}
|
|
126
|
+
return Promise.all(closeAll)
|
|
114
127
|
} else {
|
|
115
|
-
|
|
128
|
+
return new Promise((resolve) => {
|
|
116
129
|
this[kClosedResolve] = resolve
|
|
117
130
|
})
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
[kDestroy] (err) {
|
|
122
135
|
while (true) {
|
|
123
136
|
const item = this[kQueue].shift()
|
|
124
137
|
if (!item) {
|
|
@@ -127,7 +140,11 @@ class PoolBase extends DispatcherBase {
|
|
|
127
140
|
item.handler.onError(err)
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
|
|
143
|
+
const destroyAll = new Array(this[kClients].length)
|
|
144
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
145
|
+
destroyAll[i] = this[kClients][i].destroy(err)
|
|
146
|
+
}
|
|
147
|
+
return Promise.all(destroyAll)
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
[kDispatch] (opts, handler) {
|
|
@@ -147,7 +164,7 @@ class PoolBase extends DispatcherBase {
|
|
|
147
164
|
|
|
148
165
|
[kAddClient] (client) {
|
|
149
166
|
client
|
|
150
|
-
.on('drain', this[kOnDrain])
|
|
167
|
+
.on('drain', this[kOnDrain].bind(this, client))
|
|
151
168
|
.on('connect', this[kOnConnect])
|
|
152
169
|
.on('disconnect', this[kOnDisconnect])
|
|
153
170
|
.on('connectionError', this[kOnConnectionError])
|
|
@@ -157,7 +174,7 @@ class PoolBase extends DispatcherBase {
|
|
|
157
174
|
if (this[kNeedDrain]) {
|
|
158
175
|
queueMicrotask(() => {
|
|
159
176
|
if (this[kNeedDrain]) {
|
|
160
|
-
this[kOnDrain](client[kUrl], [
|
|
177
|
+
this[kOnDrain](client, client[kUrl], [client, this])
|
|
161
178
|
}
|
|
162
179
|
})
|
|
163
180
|
}
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -51,8 +51,6 @@ class Pool extends PoolBase {
|
|
|
51
51
|
throw new InvalidArgumentError('connect must be a function or an object')
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
super()
|
|
55
|
-
|
|
56
54
|
if (typeof connect !== 'function') {
|
|
57
55
|
connect = buildConnector({
|
|
58
56
|
...tls,
|
|
@@ -65,6 +63,8 @@ class Pool extends PoolBase {
|
|
|
65
63
|
})
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
super()
|
|
67
|
+
|
|
68
68
|
this[kConnections] = connections || null
|
|
69
69
|
this[kUrl] = util.parseOrigin(origin)
|
|
70
70
|
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
|
|
@@ -37,11 +37,12 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
37
37
|
#client
|
|
38
38
|
|
|
39
39
|
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
|
40
|
-
super()
|
|
41
40
|
if (!proxyUrl) {
|
|
42
41
|
throw new InvalidArgumentError('Proxy URL is mandatory')
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
super()
|
|
45
|
+
|
|
45
46
|
this[kProxyHeaders] = headers
|
|
46
47
|
if (factory) {
|
|
47
48
|
this.#client = factory(proxyUrl, { connect })
|
|
@@ -80,11 +81,11 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
80
81
|
return this.#client[kDispatch](opts, handler)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
[kClose] () {
|
|
84
85
|
return this.#client.close()
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
[kDestroy] (err) {
|
|
88
89
|
return this.#client.destroy(err)
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -220,14 +221,18 @@ class ProxyAgent extends DispatcherBase {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
[kClose] () {
|
|
225
|
+
return Promise.all([
|
|
226
|
+
this[kAgent].close(),
|
|
227
|
+
this[kClient].close()
|
|
228
|
+
])
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
[kDestroy] () {
|
|
232
|
+
return Promise.all([
|
|
233
|
+
this[kAgent].destroy(),
|
|
234
|
+
this[kClient].destroy()
|
|
235
|
+
])
|
|
231
236
|
}
|
|
232
237
|
}
|
|
233
238
|
|
package/lib/global.js
CHANGED
|
@@ -26,7 +26,25 @@ function getGlobalDispatcher () {
|
|
|
26
26
|
return globalThis[globalDispatcher]
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// These are the globals that can be installed by undici.install().
|
|
30
|
+
// Not exported by index.js to avoid use outside of this module.
|
|
31
|
+
const installedExports = /** @type {const} */ (
|
|
32
|
+
[
|
|
33
|
+
'fetch',
|
|
34
|
+
'Headers',
|
|
35
|
+
'Response',
|
|
36
|
+
'Request',
|
|
37
|
+
'FormData',
|
|
38
|
+
'WebSocket',
|
|
39
|
+
'CloseEvent',
|
|
40
|
+
'ErrorEvent',
|
|
41
|
+
'MessageEvent',
|
|
42
|
+
'EventSource'
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
29
46
|
module.exports = {
|
|
30
47
|
setGlobalDispatcher,
|
|
31
|
-
getGlobalDispatcher
|
|
48
|
+
getGlobalDispatcher,
|
|
49
|
+
installedExports
|
|
32
50
|
}
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -56,6 +56,22 @@ function needsRevalidation (result, cacheControlDirectives) {
|
|
|
56
56
|
return false
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Check if we're within the stale-while-revalidate window for a stale response
|
|
61
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function withinStaleWhileRevalidateWindow (result) {
|
|
65
|
+
const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
|
|
66
|
+
if (!staleWhileRevalidate) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const now = Date.now()
|
|
71
|
+
const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
|
|
72
|
+
return now <= staleWhileRevalidateExpiry
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
/**
|
|
60
76
|
* @param {DispatchFn} dispatch
|
|
61
77
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
@@ -231,6 +247,51 @@ function handleResult (
|
|
|
231
247
|
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
232
248
|
}
|
|
233
249
|
|
|
250
|
+
// RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
|
|
251
|
+
// and revalidate in background
|
|
252
|
+
if (withinStaleWhileRevalidateWindow(result)) {
|
|
253
|
+
// Serve stale response immediately
|
|
254
|
+
sendCachedValue(handler, opts, result, age, null, true)
|
|
255
|
+
|
|
256
|
+
// Start background revalidation (fire-and-forget)
|
|
257
|
+
queueMicrotask(() => {
|
|
258
|
+
let headers = {
|
|
259
|
+
...opts.headers,
|
|
260
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.etag) {
|
|
264
|
+
headers['if-none-match'] = result.etag
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result.vary) {
|
|
268
|
+
headers = {
|
|
269
|
+
...headers,
|
|
270
|
+
...result.vary
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Background revalidation - update cache if we get new data
|
|
275
|
+
dispatch(
|
|
276
|
+
{
|
|
277
|
+
...opts,
|
|
278
|
+
headers
|
|
279
|
+
},
|
|
280
|
+
new CacheHandler(globalOpts, cacheKey, {
|
|
281
|
+
// Silent handler that just updates the cache
|
|
282
|
+
onRequestStart () {},
|
|
283
|
+
onRequestUpgrade () {},
|
|
284
|
+
onResponseStart () {},
|
|
285
|
+
onResponseData () {},
|
|
286
|
+
onResponseEnd () {},
|
|
287
|
+
onResponseError () {}
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
|
|
234
295
|
let withinStaleIfErrorThreshold = false
|
|
235
296
|
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
|
|
236
297
|
if (staleIfErrorExpiry) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
|
|
4
|
+
const { pipeline } = require('node:stream')
|
|
5
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('node:stream').Transform} Transform */
|
|
8
|
+
/** @typedef {import('node:stream').Transform} Controller */
|
|
9
|
+
/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
|
|
10
|
+
|
|
11
|
+
/** @type {Record<string, () => DecompressorStream>} */
|
|
12
|
+
const supportedEncodings = {
|
|
13
|
+
gzip: createGunzip,
|
|
14
|
+
'x-gzip': createGunzip,
|
|
15
|
+
br: createBrotliDecompress,
|
|
16
|
+
deflate: createInflate,
|
|
17
|
+
compress: createInflate,
|
|
18
|
+
'x-compress': createInflate,
|
|
19
|
+
...(createZstdDecompress ? { zstd: createZstdDecompress } : {})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
|
|
23
|
+
|
|
24
|
+
let warningEmitted = /** @type {boolean} */ (false)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} DecompressHandlerOptions
|
|
28
|
+
* @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
|
|
29
|
+
* @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
class DecompressHandler extends DecoratorHandler {
|
|
33
|
+
/** @type {Transform[]} */
|
|
34
|
+
#decompressors = []
|
|
35
|
+
/** @type {NodeJS.WritableStream&NodeJS.ReadableStream|null} */
|
|
36
|
+
#pipelineStream
|
|
37
|
+
/** @type {Readonly<number[]>} */
|
|
38
|
+
#skipStatusCodes
|
|
39
|
+
/** @type {boolean} */
|
|
40
|
+
#skipErrorResponses
|
|
41
|
+
|
|
42
|
+
constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) {
|
|
43
|
+
super(handler)
|
|
44
|
+
this.#skipStatusCodes = skipStatusCodes
|
|
45
|
+
this.#skipErrorResponses = skipErrorResponses
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Determines if decompression should be skipped based on encoding and status code
|
|
50
|
+
* @param {string} contentEncoding - Content-Encoding header value
|
|
51
|
+
* @param {number} statusCode - HTTP status code of the response
|
|
52
|
+
* @returns {boolean} - True if decompression should be skipped
|
|
53
|
+
*/
|
|
54
|
+
#shouldSkipDecompression (contentEncoding, statusCode) {
|
|
55
|
+
if (!contentEncoding || statusCode < 200) return true
|
|
56
|
+
if (this.#skipStatusCodes.includes(statusCode)) return true
|
|
57
|
+
if (this.#skipErrorResponses && statusCode >= 400) return true
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates a chain of decompressors for multiple content encodings
|
|
63
|
+
*
|
|
64
|
+
* @param {string} encodings - Comma-separated list of content encodings
|
|
65
|
+
* @returns {Array<DecompressorStream>} - Array of decompressor streams
|
|
66
|
+
*/
|
|
67
|
+
#createDecompressionChain (encodings) {
|
|
68
|
+
const parts = encodings.split(',')
|
|
69
|
+
|
|
70
|
+
/** @type {DecompressorStream[]} */
|
|
71
|
+
const decompressors = []
|
|
72
|
+
|
|
73
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
74
|
+
const encoding = parts[i].trim()
|
|
75
|
+
if (!encoding) continue
|
|
76
|
+
|
|
77
|
+
if (!supportedEncodings[encoding]) {
|
|
78
|
+
decompressors.length = 0 // Clear if unsupported encoding
|
|
79
|
+
return decompressors // Unsupported encoding
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
decompressors.push(supportedEncodings[encoding]())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return decompressors
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Sets up event handlers for a decompressor stream using readable events
|
|
90
|
+
* @param {DecompressorStream} decompressor - The decompressor stream
|
|
91
|
+
* @param {Controller} controller - The controller to coordinate with
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
#setupDecompressorEvents (decompressor, controller) {
|
|
95
|
+
decompressor.on('readable', () => {
|
|
96
|
+
let chunk
|
|
97
|
+
while ((chunk = decompressor.read()) !== null) {
|
|
98
|
+
const result = super.onResponseData(controller, chunk)
|
|
99
|
+
if (result === false) {
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
decompressor.on('error', (error) => {
|
|
106
|
+
super.onResponseError(controller, error)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sets up event handling for a single decompressor
|
|
112
|
+
* @param {Controller} controller - The controller to handle events
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
#setupSingleDecompressor (controller) {
|
|
116
|
+
const decompressor = this.#decompressors[0]
|
|
117
|
+
this.#setupDecompressorEvents(decompressor, controller)
|
|
118
|
+
|
|
119
|
+
decompressor.on('end', () => {
|
|
120
|
+
super.onResponseEnd(controller, {})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sets up event handling for multiple chained decompressors using pipeline
|
|
126
|
+
* @param {Controller} controller - The controller to handle events
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
#setupMultipleDecompressors (controller) {
|
|
130
|
+
const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
|
|
131
|
+
this.#setupDecompressorEvents(lastDecompressor, controller)
|
|
132
|
+
|
|
133
|
+
this.#pipelineStream = pipeline(this.#decompressors, (err) => {
|
|
134
|
+
if (err) {
|
|
135
|
+
super.onResponseError(controller, err)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
super.onResponseEnd(controller, {})
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Cleans up decompressor references to prevent memory leaks
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
#cleanupDecompressors () {
|
|
147
|
+
this.#decompressors.length = 0
|
|
148
|
+
this.#pipelineStream = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {Controller} controller
|
|
153
|
+
* @param {number} statusCode
|
|
154
|
+
* @param {Record<string, string | string[] | undefined>} headers
|
|
155
|
+
* @param {string} statusMessage
|
|
156
|
+
* @returns {void}
|
|
157
|
+
*/
|
|
158
|
+
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
159
|
+
const contentEncoding = headers['content-encoding']
|
|
160
|
+
|
|
161
|
+
// If content encoding is not supported or status code is in skip list
|
|
162
|
+
if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
|
|
163
|
+
return super.onResponseStart(controller, statusCode, headers, statusMessage)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
|
|
167
|
+
|
|
168
|
+
if (decompressors.length === 0) {
|
|
169
|
+
this.#cleanupDecompressors()
|
|
170
|
+
return super.onResponseStart(controller, statusCode, headers, statusMessage)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.#decompressors = decompressors
|
|
174
|
+
|
|
175
|
+
// Remove compression headers since we're decompressing
|
|
176
|
+
const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
|
|
177
|
+
|
|
178
|
+
if (this.#decompressors.length === 1) {
|
|
179
|
+
this.#setupSingleDecompressor(controller)
|
|
180
|
+
} else {
|
|
181
|
+
this.#setupMultipleDecompressors(controller)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {Controller} controller
|
|
189
|
+
* @param {Buffer} chunk
|
|
190
|
+
* @returns {void}
|
|
191
|
+
*/
|
|
192
|
+
onResponseData (controller, chunk) {
|
|
193
|
+
if (this.#decompressors.length > 0) {
|
|
194
|
+
this.#decompressors[0].write(chunk)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
super.onResponseData(controller, chunk)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {Controller} controller
|
|
202
|
+
* @param {Record<string, string | string[]> | undefined} trailers
|
|
203
|
+
* @returns {void}
|
|
204
|
+
*/
|
|
205
|
+
onResponseEnd (controller, trailers) {
|
|
206
|
+
if (this.#decompressors.length > 0) {
|
|
207
|
+
this.#decompressors[0].end()
|
|
208
|
+
this.#cleanupDecompressors()
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
super.onResponseEnd(controller, trailers)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {Controller} controller
|
|
216
|
+
* @param {Error} err
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
219
|
+
onResponseError (controller, err) {
|
|
220
|
+
if (this.#decompressors.length > 0) {
|
|
221
|
+
for (const decompressor of this.#decompressors) {
|
|
222
|
+
decompressor.destroy(err)
|
|
223
|
+
}
|
|
224
|
+
this.#cleanupDecompressors()
|
|
225
|
+
}
|
|
226
|
+
super.onResponseError(controller, err)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates a decompression interceptor for HTTP responses
|
|
232
|
+
* @param {DecompressHandlerOptions} [options] - Options for the interceptor
|
|
233
|
+
* @returns {Function} - Interceptor function
|
|
234
|
+
*/
|
|
235
|
+
function createDecompressInterceptor (options = {}) {
|
|
236
|
+
// Emit experimental warning only once
|
|
237
|
+
if (!warningEmitted) {
|
|
238
|
+
process.emitWarning(
|
|
239
|
+
'DecompressInterceptor is experimental and subject to change',
|
|
240
|
+
'ExperimentalWarning'
|
|
241
|
+
)
|
|
242
|
+
warningEmitted = true
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return (dispatch) => {
|
|
246
|
+
return (opts, handler) => {
|
|
247
|
+
const decompressHandler = new DecompressHandler(handler, options)
|
|
248
|
+
return dispatch(opts, decompressHandler)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = createDecompressInterceptor
|