undici 7.13.0 → 7.15.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 +5 -5
- package/docs/docs/api/DiagnosticsChannel.md +25 -1
- package/docs/docs/api/Dispatcher.md +59 -0
- package/index.js +2 -1
- package/lib/core/util.js +13 -1
- package/lib/dispatcher/agent.js +25 -16
- package/lib/dispatcher/client-h1.js +1 -1
- package/lib/dispatcher/proxy-agent.js +1 -2
- package/lib/handler/cache-handler.js +22 -4
- package/lib/interceptor/cache.js +2 -2
- 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/snapshot-agent.js +73 -59
- package/lib/mock/snapshot-recorder.js +254 -191
- package/lib/mock/snapshot-utils.js +158 -0
- package/lib/util/cache.js +9 -10
- package/lib/web/cache/cache.js +4 -4
- package/lib/web/cookies/parse.js +2 -2
- package/lib/web/eventsource/eventsource.js +17 -2
- package/lib/web/fetch/body.js +4 -4
- package/lib/web/fetch/formdata.js +1 -1
- package/lib/web/fetch/index.js +1 -1
- package/lib/web/fetch/response.js +8 -4
- package/lib/web/fetch/util.js +0 -216
- package/lib/web/subresource-integrity/Readme.md +9 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +306 -0
- package/lib/web/websocket/stream/websocketstream.js +2 -2
- package/lib/web/websocket/websocket.js +11 -4
- package/package.json +8 -7
- package/types/diagnostics-channel.d.ts +0 -1
- package/types/eventsource.d.ts +6 -1
- package/types/index.d.ts +4 -1
- package/types/interceptors.d.ts +5 -0
- package/types/snapshot-agent.d.ts +5 -3
- package/lib/api/util.js +0 -95
- package/lib/llhttp/constants.js.map +0 -1
- package/lib/llhttp/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -622,11 +622,11 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es
|
|
|
622
622
|
|
|
623
623
|
Undici aligns with the Node.js LTS schedule. The following table shows the supported versions:
|
|
624
624
|
|
|
625
|
-
| Version | Node.js
|
|
626
|
-
|
|
627
|
-
| 5.x
|
|
628
|
-
| 6.x
|
|
629
|
-
| 7.x
|
|
625
|
+
| Undici Version | Bundled in Node.js | Node.js Versions Supported | End of Life |
|
|
626
|
+
|----------------|-------------------|----------------------------|-------------|
|
|
627
|
+
| 5.x | 18.x | ≥14.0 (tested: 14, 16, 18) | 2024-04-30 |
|
|
628
|
+
| 6.x | 20.x, 22.x | ≥18.17 (tested: 18, 20, 21, 22) | 2026-04-30 |
|
|
629
|
+
| 7.x | 24.x | ≥20.18.1 (tested: 20, 22, 24) | 2027-04-30 |
|
|
630
630
|
|
|
631
631
|
## License
|
|
632
632
|
|
|
@@ -169,14 +169,38 @@ This message is published after the client has successfully connected to a serve
|
|
|
169
169
|
```js
|
|
170
170
|
import diagnosticsChannel from 'diagnostics_channel'
|
|
171
171
|
|
|
172
|
-
diagnosticsChannel.channel('undici:websocket:open').subscribe(({
|
|
172
|
+
diagnosticsChannel.channel('undici:websocket:open').subscribe(({
|
|
173
|
+
address, // { address: string, family: string, port: number }
|
|
174
|
+
protocol, // string - negotiated subprotocol
|
|
175
|
+
extensions, // string - negotiated extensions
|
|
176
|
+
websocket, // WebSocket - the WebSocket instance
|
|
177
|
+
handshakeResponse // object - HTTP response that upgraded the connection
|
|
178
|
+
}) => {
|
|
173
179
|
console.log(address) // address, family, and port
|
|
174
180
|
console.log(protocol) // negotiated subprotocols
|
|
175
181
|
console.log(extensions) // negotiated extensions
|
|
176
182
|
console.log(websocket) // the WebSocket instance
|
|
183
|
+
|
|
184
|
+
// Handshake response details
|
|
185
|
+
console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade
|
|
186
|
+
console.log(handshakeResponse.statusText) // 'Switching Protocols'
|
|
187
|
+
console.log(handshakeResponse.headers) // Object containing response headers
|
|
177
188
|
})
|
|
178
189
|
```
|
|
179
190
|
|
|
191
|
+
### Handshake Response Object
|
|
192
|
+
|
|
193
|
+
The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket:
|
|
194
|
+
|
|
195
|
+
- `status` (number): The HTTP status code (101 for successful WebSocket upgrade)
|
|
196
|
+
- `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade)
|
|
197
|
+
- `headers` (object): The HTTP response headers from the server, including:
|
|
198
|
+
- `upgrade: 'websocket'`
|
|
199
|
+
- `connection: 'upgrade'`
|
|
200
|
+
- `sec-websocket-accept` and other WebSocket-related headers
|
|
201
|
+
|
|
202
|
+
This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection.
|
|
203
|
+
|
|
180
204
|
## `undici:websocket:close`
|
|
181
205
|
|
|
182
206
|
This message is published after the connection has closed.
|
|
@@ -1094,6 +1094,65 @@ await client.request({
|
|
|
1094
1094
|
});
|
|
1095
1095
|
```
|
|
1096
1096
|
|
|
1097
|
+
##### `decompress`
|
|
1098
|
+
|
|
1099
|
+
⚠️ The decompress interceptor is experimental and subject to change.
|
|
1100
|
+
|
|
1101
|
+
The `decompress` interceptor automatically decompresses response bodies that are compressed with gzip, deflate, brotli, or zstd compression. It removes the `content-encoding` and `content-length` headers from decompressed responses and supports RFC-9110 compliant multiple encodings.
|
|
1102
|
+
|
|
1103
|
+
**Options**
|
|
1104
|
+
|
|
1105
|
+
- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
|
|
1106
|
+
- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
|
|
1107
|
+
|
|
1108
|
+
**Example - Basic Decompress Interceptor**
|
|
1109
|
+
|
|
1110
|
+
```js
|
|
1111
|
+
const { Client, interceptors } = require("undici");
|
|
1112
|
+
const { decompress } = interceptors;
|
|
1113
|
+
|
|
1114
|
+
const client = new Client("http://example.com").compose(
|
|
1115
|
+
decompress()
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
// Automatically decompresses gzip/deflate/brotli/zstd responses
|
|
1119
|
+
const response = await client.request({
|
|
1120
|
+
method: "GET",
|
|
1121
|
+
path: "/"
|
|
1122
|
+
});
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
**Example - Custom Options**
|
|
1126
|
+
|
|
1127
|
+
```js
|
|
1128
|
+
const { Client, interceptors } = require("undici");
|
|
1129
|
+
const { decompress } = interceptors;
|
|
1130
|
+
|
|
1131
|
+
const client = new Client("http://example.com").compose(
|
|
1132
|
+
decompress({
|
|
1133
|
+
skipErrorResponses: false, // Decompress 5xx responses
|
|
1134
|
+
skipStatusCodes: [204, 304, 201] // Skip these status codes
|
|
1135
|
+
})
|
|
1136
|
+
);
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
**Supported Encodings**
|
|
1140
|
+
|
|
1141
|
+
- `gzip` / `x-gzip` - GZIP compression
|
|
1142
|
+
- `deflate` / `x-compress` - DEFLATE compression
|
|
1143
|
+
- `br` - Brotli compression
|
|
1144
|
+
- `zstd` - Zstandard compression
|
|
1145
|
+
- Multiple encodings (e.g., `gzip, deflate`) are supported per RFC-9110
|
|
1146
|
+
|
|
1147
|
+
**Behavior**
|
|
1148
|
+
|
|
1149
|
+
- Skips decompression for status codes < 200 or >= 400 (configurable)
|
|
1150
|
+
- Skips decompression for 204 No Content and 304 Not Modified by default
|
|
1151
|
+
- Removes `content-encoding` and `content-length` headers when decompressing
|
|
1152
|
+
- Passes through unsupported encodings unchanged
|
|
1153
|
+
- Handles case-insensitive encoding names
|
|
1154
|
+
- Supports streaming decompression without buffering
|
|
1155
|
+
|
|
1097
1156
|
##### `Cache Interceptor`
|
|
1098
1157
|
|
|
1099
1158
|
The `cache` interceptor implements client-side response caching as described in
|
package/index.js
CHANGED
|
@@ -46,7 +46,8 @@ module.exports.interceptors = {
|
|
|
46
46
|
retry: require('./lib/interceptor/retry'),
|
|
47
47
|
dump: require('./lib/interceptor/dump'),
|
|
48
48
|
dns: require('./lib/interceptor/dns'),
|
|
49
|
-
cache: require('./lib/interceptor/cache')
|
|
49
|
+
cache: require('./lib/interceptor/cache'),
|
|
50
|
+
decompress: require('./lib/interceptor/decompress')
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
module.exports.cacheStores = {
|
package/lib/core/util.js
CHANGED
|
@@ -102,13 +102,24 @@ function isBlobLike (object) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} url The path to check for query strings or fragments.
|
|
107
|
+
* @returns {boolean} Returns true if the path contains a query string or fragment.
|
|
108
|
+
*/
|
|
109
|
+
function pathHasQueryOrFragment (url) {
|
|
110
|
+
return (
|
|
111
|
+
url.includes('?') ||
|
|
112
|
+
url.includes('#')
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
/**
|
|
106
117
|
* @param {string} url The URL to add the query params to
|
|
107
118
|
* @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string
|
|
108
119
|
* @returns {string} The URL with the query params added
|
|
109
120
|
*/
|
|
110
121
|
function serializePathWithQuery (url, queryParams) {
|
|
111
|
-
if (
|
|
122
|
+
if (pathHasQueryOrFragment(url)) {
|
|
112
123
|
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
|
|
113
124
|
}
|
|
114
125
|
|
|
@@ -924,6 +935,7 @@ module.exports = {
|
|
|
924
935
|
assertRequestHandler,
|
|
925
936
|
getSocketInfo,
|
|
926
937
|
isFormDataLike,
|
|
938
|
+
pathHasQueryOrFragment,
|
|
927
939
|
serializePathWithQuery,
|
|
928
940
|
addAbortListener,
|
|
929
941
|
isValidHTTPToken,
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -45,27 +45,14 @@ class Agent extends DispatcherBase {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
this[kOnConnect] = (origin, targets) => {
|
|
48
|
-
const result = this[kClients].get(origin)
|
|
49
|
-
if (result) {
|
|
50
|
-
result.count += 1
|
|
51
|
-
}
|
|
52
48
|
this.emit('connect', origin, [this, ...targets])
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
this[kOnDisconnect] = (origin, targets, err) => {
|
|
56
|
-
const result = this[kClients].get(origin)
|
|
57
|
-
if (result) {
|
|
58
|
-
result.count -= 1
|
|
59
|
-
if (result.count <= 0) {
|
|
60
|
-
this[kClients].delete(origin)
|
|
61
|
-
result.dispatcher.destroy()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
52
|
this.emit('disconnect', origin, [this, ...targets], err)
|
|
65
53
|
}
|
|
66
54
|
|
|
67
55
|
this[kOnConnectionError] = (origin, targets, err) => {
|
|
68
|
-
// TODO: should this decrement result.count here?
|
|
69
56
|
this.emit('connectionError', origin, [this, ...targets], err)
|
|
70
57
|
}
|
|
71
58
|
}
|
|
@@ -89,11 +76,33 @@ class Agent extends DispatcherBase {
|
|
|
89
76
|
const result = this[kClients].get(key)
|
|
90
77
|
let dispatcher = result && result.dispatcher
|
|
91
78
|
if (!dispatcher) {
|
|
79
|
+
const closeClientIfUnused = (connected) => {
|
|
80
|
+
const result = this[kClients].get(key)
|
|
81
|
+
if (result) {
|
|
82
|
+
if (connected) result.count -= 1
|
|
83
|
+
if (result.count <= 0) {
|
|
84
|
+
this[kClients].delete(key)
|
|
85
|
+
result.dispatcher.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
92
89
|
dispatcher = this[kFactory](opts.origin, this[kOptions])
|
|
93
90
|
.on('drain', this[kOnDrain])
|
|
94
|
-
.on('connect',
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
.on('connect', (origin, targets) => {
|
|
92
|
+
const result = this[kClients].get(key)
|
|
93
|
+
if (result) {
|
|
94
|
+
result.count += 1
|
|
95
|
+
}
|
|
96
|
+
this[kOnConnect](origin, targets)
|
|
97
|
+
})
|
|
98
|
+
.on('disconnect', (origin, targets, err) => {
|
|
99
|
+
closeClientIfUnused(true)
|
|
100
|
+
this[kOnDisconnect](origin, targets, err)
|
|
101
|
+
})
|
|
102
|
+
.on('connectionError', (origin, targets, err) => {
|
|
103
|
+
closeClientIfUnused(false)
|
|
104
|
+
this[kOnConnectionError](origin, targets, err)
|
|
105
|
+
})
|
|
97
106
|
|
|
98
107
|
this[kClients].set(key, { count: 0, dispatcher })
|
|
99
108
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
|
4
|
-
const { URL } = require('node:url')
|
|
5
4
|
const Agent = require('./agent')
|
|
6
5
|
const Pool = require('./pool')
|
|
7
6
|
const DispatcherBase = require('./dispatcher-base')
|
|
@@ -208,7 +207,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
/**
|
|
211
|
-
* @param {import('
|
|
210
|
+
* @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts
|
|
212
211
|
* @returns {URL}
|
|
213
212
|
*/
|
|
214
213
|
#getUrl (opts) {
|
|
@@ -15,6 +15,15 @@ const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
|
15
15
|
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
// Status codes which semantic is not handled by the cache
|
|
19
|
+
// https://datatracker.ietf.org/doc/html/rfc9111#section-3
|
|
20
|
+
// This list should not grow beyond 206 and 304 unless the RFC is updated
|
|
21
|
+
// by a newer one including more. Please introduce another list if
|
|
22
|
+
// implementing caching of responses with the 'must-understand' directive.
|
|
23
|
+
const NOT_UNDERSTOOD_STATUS_CODES = [
|
|
24
|
+
206, 304
|
|
25
|
+
]
|
|
26
|
+
|
|
18
27
|
const MAX_RESPONSE_AGE = 2147483647000
|
|
19
28
|
|
|
20
29
|
/**
|
|
@@ -241,10 +250,19 @@ class CacheHandler {
|
|
|
241
250
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
242
251
|
*/
|
|
243
252
|
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
// Status code must be final and understood.
|
|
254
|
+
if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
// Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
|
|
258
|
+
// directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
|
|
259
|
+
if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] &&
|
|
260
|
+
!cacheControlDirectives.public &&
|
|
261
|
+
cacheControlDirectives['max-age'] === undefined &&
|
|
262
|
+
// RFC 9111: a private response directive, if the cache is not shared
|
|
263
|
+
!(cacheControlDirectives.private && cacheType === 'private') &&
|
|
264
|
+
!(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
|
|
265
|
+
) {
|
|
248
266
|
return false
|
|
249
267
|
}
|
|
250
268
|
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -6,7 +6,7 @@ const util = require('../core/util')
|
|
|
6
6
|
const CacheHandler = require('../handler/cache-handler')
|
|
7
7
|
const MemoryCacheStore = require('../cache/memory-cache-store')
|
|
8
8
|
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
|
|
9
|
-
const { assertCacheStore, assertCacheMethods, makeCacheKey,
|
|
9
|
+
const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
|
|
10
10
|
const { AbortError } = require('../core/errors.js')
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -326,7 +326,7 @@ module.exports = (opts = {}) => {
|
|
|
326
326
|
|
|
327
327
|
opts = {
|
|
328
328
|
...opts,
|
|
329
|
-
headers:
|
|
329
|
+
headers: normalizeHeaders(opts)
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
const reqCacheControl = opts.headers?.['cache-control']
|
|
@@ -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
|
|
@@ -15,7 +15,7 @@ export declare const H_METHOD_MAP: {
|
|
|
15
15
|
[k: string]: number;
|
|
16
16
|
};
|
|
17
17
|
export declare const STATUSES_HTTP: number[];
|
|
18
|
-
export type CharList =
|
|
18
|
+
export type CharList = (string | number)[];
|
|
19
19
|
export declare const ALPHA: CharList;
|
|
20
20
|
export declare const NUM_MAP: {
|
|
21
21
|
0: number;
|
|
@@ -95,3 +95,101 @@ export declare const SPECIAL_HEADERS: {
|
|
|
95
95
|
'transfer-encoding': number;
|
|
96
96
|
upgrade: number;
|
|
97
97
|
};
|
|
98
|
+
declare const _default: {
|
|
99
|
+
ERROR: IntDict;
|
|
100
|
+
TYPE: IntDict;
|
|
101
|
+
FLAGS: IntDict;
|
|
102
|
+
LENIENT_FLAGS: IntDict;
|
|
103
|
+
METHODS: IntDict;
|
|
104
|
+
STATUSES: IntDict;
|
|
105
|
+
FINISH: IntDict;
|
|
106
|
+
HEADER_STATE: IntDict;
|
|
107
|
+
ALPHA: CharList;
|
|
108
|
+
NUM_MAP: {
|
|
109
|
+
0: number;
|
|
110
|
+
1: number;
|
|
111
|
+
2: number;
|
|
112
|
+
3: number;
|
|
113
|
+
4: number;
|
|
114
|
+
5: number;
|
|
115
|
+
6: number;
|
|
116
|
+
7: number;
|
|
117
|
+
8: number;
|
|
118
|
+
9: number;
|
|
119
|
+
};
|
|
120
|
+
HEX_MAP: {
|
|
121
|
+
0: number;
|
|
122
|
+
1: number;
|
|
123
|
+
2: number;
|
|
124
|
+
3: number;
|
|
125
|
+
4: number;
|
|
126
|
+
5: number;
|
|
127
|
+
6: number;
|
|
128
|
+
7: number;
|
|
129
|
+
8: number;
|
|
130
|
+
9: number;
|
|
131
|
+
A: number;
|
|
132
|
+
B: number;
|
|
133
|
+
C: number;
|
|
134
|
+
D: number;
|
|
135
|
+
E: number;
|
|
136
|
+
F: number;
|
|
137
|
+
a: number;
|
|
138
|
+
b: number;
|
|
139
|
+
c: number;
|
|
140
|
+
d: number;
|
|
141
|
+
e: number;
|
|
142
|
+
f: number;
|
|
143
|
+
};
|
|
144
|
+
NUM: CharList;
|
|
145
|
+
ALPHANUM: CharList;
|
|
146
|
+
MARK: CharList;
|
|
147
|
+
USERINFO_CHARS: CharList;
|
|
148
|
+
URL_CHAR: CharList;
|
|
149
|
+
HEX: CharList;
|
|
150
|
+
TOKEN: CharList;
|
|
151
|
+
HEADER_CHARS: CharList;
|
|
152
|
+
CONNECTION_TOKEN_CHARS: CharList;
|
|
153
|
+
QUOTED_STRING: CharList;
|
|
154
|
+
HTAB_SP_VCHAR_OBS_TEXT: CharList;
|
|
155
|
+
MAJOR: {
|
|
156
|
+
0: number;
|
|
157
|
+
1: number;
|
|
158
|
+
2: number;
|
|
159
|
+
3: number;
|
|
160
|
+
4: number;
|
|
161
|
+
5: number;
|
|
162
|
+
6: number;
|
|
163
|
+
7: number;
|
|
164
|
+
8: number;
|
|
165
|
+
9: number;
|
|
166
|
+
};
|
|
167
|
+
MINOR: {
|
|
168
|
+
0: number;
|
|
169
|
+
1: number;
|
|
170
|
+
2: number;
|
|
171
|
+
3: number;
|
|
172
|
+
4: number;
|
|
173
|
+
5: number;
|
|
174
|
+
6: number;
|
|
175
|
+
7: number;
|
|
176
|
+
8: number;
|
|
177
|
+
9: number;
|
|
178
|
+
};
|
|
179
|
+
SPECIAL_HEADERS: {
|
|
180
|
+
connection: number;
|
|
181
|
+
'content-length': number;
|
|
182
|
+
'proxy-connection': number;
|
|
183
|
+
'transfer-encoding': number;
|
|
184
|
+
upgrade: number;
|
|
185
|
+
};
|
|
186
|
+
METHODS_HTTP: number[];
|
|
187
|
+
METHODS_ICE: number[];
|
|
188
|
+
METHODS_RTSP: number[];
|
|
189
|
+
METHOD_MAP: IntDict;
|
|
190
|
+
H_METHOD_MAP: {
|
|
191
|
+
[k: string]: number;
|
|
192
|
+
};
|
|
193
|
+
STATUSES_HTTP: number[];
|
|
194
|
+
};
|
|
195
|
+
export default _default;
|