dsh-model-proxy-switch 0.0.0-stage → 1.0.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.
Files changed (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -2
  3. package/client.js +473 -0
  4. package/cordis.patch.yml +7 -0
  5. package/icon.svg +13 -0
  6. package/index.js +341 -0
  7. package/locale/en.json +25 -0
  8. package/locale/zh.json +25 -0
  9. package/package.json +55 -4
  10. package/vendor/http-proxy.js +542 -0
  11. package/vendor/node_modules/undici/LICENSE +21 -0
  12. package/vendor/node_modules/undici/README.md +741 -0
  13. package/vendor/node_modules/undici/docs/docs/api/Agent.md +84 -0
  14. package/vendor/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  15. package/vendor/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  16. package/vendor/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  17. package/vendor/node_modules/undici/docs/docs/api/Client.md +290 -0
  18. package/vendor/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  19. package/vendor/node_modules/undici/docs/docs/api/Connector.md +115 -0
  20. package/vendor/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  21. package/vendor/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  22. package/vendor/node_modules/undici/docs/docs/api/Debug.md +62 -0
  23. package/vendor/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  24. package/vendor/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  25. package/vendor/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  26. package/vendor/node_modules/undici/docs/docs/api/Errors.md +49 -0
  27. package/vendor/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  28. package/vendor/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  29. package/vendor/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  30. package/vendor/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  31. package/vendor/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  32. package/vendor/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  33. package/vendor/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  34. package/vendor/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  35. package/vendor/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  36. package/vendor/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  37. package/vendor/node_modules/undici/docs/docs/api/Pool.md +84 -0
  38. package/vendor/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  39. package/vendor/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  40. package/vendor/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  41. package/vendor/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  42. package/vendor/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  43. package/vendor/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  44. package/vendor/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  45. package/vendor/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  46. package/vendor/node_modules/undici/docs/docs/api/Util.md +25 -0
  47. package/vendor/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  48. package/vendor/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  49. package/vendor/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  50. package/vendor/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  51. package/vendor/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  52. package/vendor/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  53. package/vendor/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  54. package/vendor/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  55. package/vendor/node_modules/undici/index-fetch.js +65 -0
  56. package/vendor/node_modules/undici/index.d.ts +3 -0
  57. package/vendor/node_modules/undici/index.js +234 -0
  58. package/vendor/node_modules/undici/lib/api/abort-signal.js +59 -0
  59. package/vendor/node_modules/undici/lib/api/api-connect.js +110 -0
  60. package/vendor/node_modules/undici/lib/api/api-pipeline.js +252 -0
  61. package/vendor/node_modules/undici/lib/api/api-request.js +214 -0
  62. package/vendor/node_modules/undici/lib/api/api-stream.js +209 -0
  63. package/vendor/node_modules/undici/lib/api/api-upgrade.js +111 -0
  64. package/vendor/node_modules/undici/lib/api/index.js +7 -0
  65. package/vendor/node_modules/undici/lib/api/readable.js +580 -0
  66. package/vendor/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  67. package/vendor/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  68. package/vendor/node_modules/undici/lib/core/connect.js +159 -0
  69. package/vendor/node_modules/undici/lib/core/constants.js +143 -0
  70. package/vendor/node_modules/undici/lib/core/diagnostics.js +227 -0
  71. package/vendor/node_modules/undici/lib/core/errors.js +477 -0
  72. package/vendor/node_modules/undici/lib/core/request.js +485 -0
  73. package/vendor/node_modules/undici/lib/core/socks5-client.js +422 -0
  74. package/vendor/node_modules/undici/lib/core/socks5-utils.js +212 -0
  75. package/vendor/node_modules/undici/lib/core/symbols.js +76 -0
  76. package/vendor/node_modules/undici/lib/core/tree.js +160 -0
  77. package/vendor/node_modules/undici/lib/core/util.js +992 -0
  78. package/vendor/node_modules/undici/lib/dispatcher/agent.js +162 -0
  79. package/vendor/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  80. package/vendor/node_modules/undici/lib/dispatcher/client-h1.js +1744 -0
  81. package/vendor/node_modules/undici/lib/dispatcher/client-h2.js +1056 -0
  82. package/vendor/node_modules/undici/lib/dispatcher/client.js +669 -0
  83. package/vendor/node_modules/undici/lib/dispatcher/dispatcher-base.js +196 -0
  84. package/vendor/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  85. package/vendor/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  86. package/vendor/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  87. package/vendor/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  88. package/vendor/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  89. package/vendor/node_modules/undici/lib/dispatcher/pool.js +118 -0
  90. package/vendor/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  91. package/vendor/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  92. package/vendor/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  93. package/vendor/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  94. package/vendor/node_modules/undici/lib/encoding/index.js +33 -0
  95. package/vendor/node_modules/undici/lib/global.js +59 -0
  96. package/vendor/node_modules/undici/lib/handler/cache-handler.js +824 -0
  97. package/vendor/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  98. package/vendor/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  99. package/vendor/node_modules/undici/lib/handler/deduplication-handler.js +504 -0
  100. package/vendor/node_modules/undici/lib/handler/redirect-handler.js +245 -0
  101. package/vendor/node_modules/undici/lib/handler/retry-handler.js +504 -0
  102. package/vendor/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  103. package/vendor/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  104. package/vendor/node_modules/undici/lib/interceptor/cache.js +621 -0
  105. package/vendor/node_modules/undici/lib/interceptor/decompress.js +561 -0
  106. package/vendor/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  107. package/vendor/node_modules/undici/lib/interceptor/dns.js +571 -0
  108. package/vendor/node_modules/undici/lib/interceptor/dump.js +99 -0
  109. package/vendor/node_modules/undici/lib/interceptor/redirect.js +21 -0
  110. package/vendor/node_modules/undici/lib/interceptor/response-error.js +95 -0
  111. package/vendor/node_modules/undici/lib/interceptor/retry.js +19 -0
  112. package/vendor/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  113. package/vendor/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  114. package/vendor/node_modules/undici/lib/llhttp/constants.js +531 -0
  115. package/vendor/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  116. package/vendor/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  117. package/vendor/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  118. package/vendor/node_modules/undici/lib/llhttp/utils.js +12 -0
  119. package/vendor/node_modules/undici/lib/mock/mock-agent.js +232 -0
  120. package/vendor/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  121. package/vendor/node_modules/undici/lib/mock/mock-client.js +68 -0
  122. package/vendor/node_modules/undici/lib/mock/mock-errors.js +29 -0
  123. package/vendor/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  124. package/vendor/node_modules/undici/lib/mock/mock-pool.js +68 -0
  125. package/vendor/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  126. package/vendor/node_modules/undici/lib/mock/mock-utils.js +486 -0
  127. package/vendor/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  128. package/vendor/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  129. package/vendor/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  130. package/vendor/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  131. package/vendor/node_modules/undici/lib/util/cache.js +676 -0
  132. package/vendor/node_modules/undici/lib/util/date.js +670 -0
  133. package/vendor/node_modules/undici/lib/util/promise.js +28 -0
  134. package/vendor/node_modules/undici/lib/util/runtime-features.js +124 -0
  135. package/vendor/node_modules/undici/lib/util/stats.js +32 -0
  136. package/vendor/node_modules/undici/lib/util/timers.js +425 -0
  137. package/vendor/node_modules/undici/lib/web/cache/cache.js +864 -0
  138. package/vendor/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  139. package/vendor/node_modules/undici/lib/web/cache/util.js +45 -0
  140. package/vendor/node_modules/undici/lib/web/cookies/constants.js +12 -0
  141. package/vendor/node_modules/undici/lib/web/cookies/index.js +201 -0
  142. package/vendor/node_modules/undici/lib/web/cookies/parse.js +315 -0
  143. package/vendor/node_modules/undici/lib/web/cookies/util.js +371 -0
  144. package/vendor/node_modules/undici/lib/web/eventsource/eventsource-stream.js +575 -0
  145. package/vendor/node_modules/undici/lib/web/eventsource/eventsource.js +504 -0
  146. package/vendor/node_modules/undici/lib/web/eventsource/util.js +29 -0
  147. package/vendor/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  148. package/vendor/node_modules/undici/lib/web/fetch/body.js +509 -0
  149. package/vendor/node_modules/undici/lib/web/fetch/constants.js +131 -0
  150. package/vendor/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  151. package/vendor/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  152. package/vendor/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  153. package/vendor/node_modules/undici/lib/web/fetch/global.js +40 -0
  154. package/vendor/node_modules/undici/lib/web/fetch/headers.js +719 -0
  155. package/vendor/node_modules/undici/lib/web/fetch/index.js +2413 -0
  156. package/vendor/node_modules/undici/lib/web/fetch/request.js +1115 -0
  157. package/vendor/node_modules/undici/lib/web/fetch/response.js +641 -0
  158. package/vendor/node_modules/undici/lib/web/fetch/util.js +1522 -0
  159. package/vendor/node_modules/undici/lib/web/infra/index.js +243 -0
  160. package/vendor/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  161. package/vendor/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  162. package/vendor/node_modules/undici/lib/web/webidl/index.js +1006 -0
  163. package/vendor/node_modules/undici/lib/web/websocket/connection.js +339 -0
  164. package/vendor/node_modules/undici/lib/web/websocket/constants.js +126 -0
  165. package/vendor/node_modules/undici/lib/web/websocket/events.js +331 -0
  166. package/vendor/node_modules/undici/lib/web/websocket/frame.js +133 -0
  167. package/vendor/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  168. package/vendor/node_modules/undici/lib/web/websocket/receiver.js +520 -0
  169. package/vendor/node_modules/undici/lib/web/websocket/sender.js +109 -0
  170. package/vendor/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  171. package/vendor/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  172. package/vendor/node_modules/undici/lib/web/websocket/util.js +347 -0
  173. package/vendor/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  174. package/vendor/node_modules/undici/package.json +152 -0
  175. package/vendor/node_modules/undici/scripts/strip-comments.js +10 -0
  176. package/vendor/node_modules/undici/types/README.md +6 -0
  177. package/vendor/node_modules/undici/types/agent.d.ts +32 -0
  178. package/vendor/node_modules/undici/types/api.d.ts +43 -0
  179. package/vendor/node_modules/undici/types/balanced-pool.d.ts +30 -0
  180. package/vendor/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  181. package/vendor/node_modules/undici/types/cache.d.ts +36 -0
  182. package/vendor/node_modules/undici/types/client-stats.d.ts +15 -0
  183. package/vendor/node_modules/undici/types/client.d.ts +153 -0
  184. package/vendor/node_modules/undici/types/connector.d.ts +36 -0
  185. package/vendor/node_modules/undici/types/content-type.d.ts +21 -0
  186. package/vendor/node_modules/undici/types/cookies.d.ts +30 -0
  187. package/vendor/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  188. package/vendor/node_modules/undici/types/dispatcher.d.ts +275 -0
  189. package/vendor/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  190. package/vendor/node_modules/undici/types/errors.d.ts +177 -0
  191. package/vendor/node_modules/undici/types/eventsource.d.ts +66 -0
  192. package/vendor/node_modules/undici/types/fetch.d.ts +231 -0
  193. package/vendor/node_modules/undici/types/formdata.d.ts +114 -0
  194. package/vendor/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  195. package/vendor/node_modules/undici/types/global-origin.d.ts +7 -0
  196. package/vendor/node_modules/undici/types/h2c-client.d.ts +73 -0
  197. package/vendor/node_modules/undici/types/handlers.d.ts +14 -0
  198. package/vendor/node_modules/undici/types/header.d.ts +160 -0
  199. package/vendor/node_modules/undici/types/index.d.ts +91 -0
  200. package/vendor/node_modules/undici/types/interceptors.d.ts +82 -0
  201. package/vendor/node_modules/undici/types/mock-agent.d.ts +68 -0
  202. package/vendor/node_modules/undici/types/mock-call-history.d.ts +111 -0
  203. package/vendor/node_modules/undici/types/mock-client.d.ts +27 -0
  204. package/vendor/node_modules/undici/types/mock-errors.d.ts +12 -0
  205. package/vendor/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  206. package/vendor/node_modules/undici/types/mock-pool.d.ts +27 -0
  207. package/vendor/node_modules/undici/types/patch.d.ts +29 -0
  208. package/vendor/node_modules/undici/types/pool-stats.d.ts +19 -0
  209. package/vendor/node_modules/undici/types/pool.d.ts +41 -0
  210. package/vendor/node_modules/undici/types/proxy-agent.d.ts +29 -0
  211. package/vendor/node_modules/undici/types/readable.d.ts +68 -0
  212. package/vendor/node_modules/undici/types/retry-agent.d.ts +8 -0
  213. package/vendor/node_modules/undici/types/retry-handler.d.ts +125 -0
  214. package/vendor/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  215. package/vendor/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  216. package/vendor/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  217. package/vendor/node_modules/undici/types/util.d.ts +18 -0
  218. package/vendor/node_modules/undici/types/utility.d.ts +7 -0
  219. package/vendor/node_modules/undici/types/webidl.d.ts +347 -0
  220. package/vendor/node_modules/undici/types/websocket.d.ts +188 -0
@@ -0,0 +1,561 @@
1
+ 'use strict'
2
+
3
+ const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
4
+ const { pipeline, Transform: TransformStream } = require('node:stream')
5
+ const { InvalidArgumentError, ResponseExceededMaxSizeError } = require('../core/errors')
6
+ const DecoratorHandler = require('../handler/decorator-handler')
7
+ const { runtimeFeatures } = require('../util/runtime-features')
8
+
9
+ /** @typedef {import('node:stream').Transform} Transform */
10
+ /** @typedef {import('node:stream').Transform} Controller */
11
+ /** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
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
+
67
+ /** @type {Record<string, () => DecompressorStream>} */
68
+ const supportedEncodings = {
69
+ gzip: createGunzip,
70
+ 'x-gzip': createGunzip,
71
+ br: createBrotliDecompress,
72
+ deflate: createInflate,
73
+ compress: createInflate,
74
+ 'x-compress': createInflate,
75
+ ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {})
76
+ }
77
+
78
+ const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
79
+ const defaultMaxSize = 0
80
+
81
+ /**
82
+ * Limits the output of one stage in a decompression chain.
83
+ * @param {number} maxSize - Maximum output size in bytes
84
+ * @returns {Transform}
85
+ */
86
+ function createMaxSizeLimiter (maxSize) {
87
+ let size = 0
88
+
89
+ return new TransformStream({
90
+ transform (chunk, _encoding, callback) {
91
+ const decompressedSize = size + chunk.length
92
+ if (decompressedSize > maxSize) {
93
+ callback(new ResponseExceededMaxSizeError(
94
+ `Decompressed response size (${decompressedSize}) exceeded maxSize (${maxSize})`
95
+ ))
96
+ return
97
+ }
98
+
99
+ size = decompressedSize
100
+ callback(null, chunk)
101
+ }
102
+ })
103
+ }
104
+
105
+ let warningEmitted = /** @type {boolean} */ (false)
106
+
107
+ /**
108
+ * @typedef {Object} DecompressHandlerOptions
109
+ * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
110
+ * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
111
+ * @property {number} [maxSize=0] - Maximum decompressed response size in bytes. 0 disables the limit
112
+ */
113
+
114
+ class DecompressHandler extends DecoratorHandler {
115
+ /** @type {Transform[]} */
116
+ #decompressors = []
117
+ /** @type {Record<string, string | string[]> | undefined} */
118
+ #trailers
119
+ /** @type {Readonly<number[]>} */
120
+ #skipStatusCodes
121
+ /** @type {boolean} */
122
+ #skipErrorResponses
123
+ /** @type {number} */
124
+ #maxSize
125
+ /** @type {number} */
126
+ #decompressedSize = 0
127
+ /** @type {boolean} */
128
+ #terminated = false
129
+ /** @type {boolean} */
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
145
+
146
+ constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true, maxSize = defaultMaxSize } = {}) {
147
+ if (!Number.isSafeInteger(maxSize) || maxSize < 0) {
148
+ throw new InvalidArgumentError('maxSize must be a non-negative integer')
149
+ }
150
+
151
+ super(handler)
152
+ this.#skipStatusCodes = skipStatusCodes
153
+ this.#skipErrorResponses = skipErrorResponses
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
+ try {
224
+ const result = super.onResponseData(this.#controller, chunk)
225
+ if (result === false && !this.#controller.paused) {
226
+ this.#controller.pause()
227
+ }
228
+ } catch (err) {
229
+ this.#fail(err)
230
+ return
231
+ }
232
+ }
233
+ } while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
234
+ } finally {
235
+ this.#draining = false
236
+ }
237
+
238
+ this.#resumeUpstreamIfNeeded()
239
+ this.#finishIfReady()
240
+ }
241
+
242
+ #finishIfReady () {
243
+ if (this.#terminated || !this.#completionPending || this.#controller.paused || this.#draining) {
244
+ return
245
+ }
246
+
247
+ this.#cleanupDecompressors()
248
+ try {
249
+ super.onResponseEnd(this.#controller, this.#trailers)
250
+ } catch (err) {
251
+ this.#fail(err)
252
+ return
253
+ }
254
+ this.#terminated = true
255
+ }
256
+
257
+ #onDecompressionEnd () {
258
+ if (this.#terminated) {
259
+ return
260
+ }
261
+
262
+ this.#completionPending = true
263
+ this.#drainOutput()
264
+ this.#finishIfReady()
265
+ }
266
+
267
+ /**
268
+ * Determines if decompression should be skipped based on encoding and status code
269
+ * @param {string} contentEncoding - Content-Encoding header value
270
+ * @param {number} statusCode - HTTP status code of the response
271
+ * @returns {boolean} - True if decompression should be skipped
272
+ */
273
+ #shouldSkipDecompression (contentEncoding, statusCode) {
274
+ if (!contentEncoding || statusCode < 200) return true
275
+ if (this.#skipStatusCodes.includes(statusCode)) return true
276
+ if (this.#skipErrorResponses && statusCode >= 400) return true
277
+ return false
278
+ }
279
+
280
+ /**
281
+ * Creates a chain of decompressors for multiple content encodings
282
+ *
283
+ * @param {string} encodings - Comma-separated list of content encodings
284
+ * @returns {Array<Transform>} - Array of decompressor and limiting streams
285
+ * @throws {Error} - If the number of content-encodings exceeds the maximum allowed
286
+ */
287
+ #createDecompressionChain (encodings) {
288
+ const parts = encodings.split(',')
289
+
290
+ // Limit the number of content-encodings to prevent resource exhaustion.
291
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
292
+ const maxContentEncodings = 5
293
+ if (parts.length > maxContentEncodings) {
294
+ throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`)
295
+ }
296
+
297
+ /** @type {DecompressorStream[]} */
298
+ const decompressors = []
299
+
300
+ for (let i = parts.length - 1; i >= 0; i--) {
301
+ const encoding = parts[i].trim()
302
+ if (!encoding) continue
303
+
304
+ if (!supportedEncodings[encoding]) {
305
+ decompressors.length = 0 // Clear if unsupported encoding
306
+ return decompressors // Unsupported encoding
307
+ }
308
+
309
+ decompressors.push(supportedEncodings[encoding]())
310
+ }
311
+
312
+ if (decompressors.length < 2) {
313
+ return decompressors
314
+ }
315
+
316
+ /** @type {Transform[]} */
317
+ const streams = []
318
+ for (let i = 0; i < decompressors.length; i++) {
319
+ streams.push(decompressors[i])
320
+ if (i < decompressors.length - 1 && this.#maxSize > 0) {
321
+ streams.push(createMaxSizeLimiter(this.#maxSize))
322
+ }
323
+ }
324
+
325
+ return streams
326
+ }
327
+
328
+ /**
329
+ * Stops decompression and reports an error.
330
+ * @param {Error} error - The decompression error
331
+ * @returns {void}
332
+ */
333
+ #fail (error) {
334
+ if (this.#terminated) {
335
+ return
336
+ }
337
+
338
+ if (this.#inputEnded) {
339
+ // The request is already marked complete once the compressed input ends,
340
+ // so controller.abort() can no longer propagate decoder flush errors.
341
+ this.onResponseError(this.#controller, error)
342
+ } else {
343
+ this.#controller.abort(error)
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Sets up event handlers for the final decompressor stream.
349
+ * @param {DecompressorStream} decompressor - The decompressor stream
350
+ * @returns {void}
351
+ */
352
+ #setupDecompressorEvents (decompressor) {
353
+ this.#finalDecompressor = decompressor
354
+ decompressor.on('readable', () => this.#drainOutput())
355
+ decompressor.on('error', (error) => this.#fail(error))
356
+ }
357
+
358
+ /**
359
+ * Sets up event handling for a single decompressor
360
+ * @returns {void}
361
+ */
362
+ #setupSingleDecompressor () {
363
+ const decompressor = this.#decompressors[0]
364
+ this.#setupDecompressorEvents(decompressor)
365
+
366
+ decompressor.on('end', () => this.#onDecompressionEnd())
367
+ }
368
+
369
+ /**
370
+ * Sets up event handling for multiple chained decompressors using pipeline
371
+ * @returns {void}
372
+ */
373
+ #setupMultipleDecompressors () {
374
+ const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
375
+ this.#setupDecompressorEvents(lastDecompressor)
376
+
377
+ pipeline(this.#decompressors, (err) => {
378
+ if (this.#terminated) {
379
+ return
380
+ }
381
+
382
+ if (err) {
383
+ this.#fail(err)
384
+ return
385
+ }
386
+
387
+ this.#onDecompressionEnd()
388
+ })
389
+ }
390
+
391
+ #setupInputBackpressure () {
392
+ const decompressor = this.#decompressors[0]
393
+ decompressor.on('drain', () => {
394
+ if (this.#terminated) {
395
+ return
396
+ }
397
+
398
+ this.#inputBackpressured = false
399
+ if (!this.#controller.paused) {
400
+ this.#drainOutput()
401
+ this.#resumeUpstreamIfNeeded()
402
+ }
403
+ })
404
+ }
405
+
406
+ /**
407
+ * Cleans up decompressor references to prevent memory leaks
408
+ * @returns {void}
409
+ */
410
+ #cleanupDecompressors () {
411
+ this.#decompressors.length = 0
412
+ this.#finalDecompressor = undefined
413
+ }
414
+
415
+ onRequestStart (controller, context) {
416
+ this.#controller.target = controller
417
+ return super.onRequestStart(this.#controller, context)
418
+ }
419
+
420
+ onRequestUpgrade (controller, statusCode, headers, socket) {
421
+ return super.onRequestUpgrade(this.#controller, statusCode, headers, socket)
422
+ }
423
+
424
+ /**
425
+ * @param {Controller} controller
426
+ * @param {number} statusCode
427
+ * @param {Record<string, string | string[] | undefined>} headers
428
+ * @param {string} statusMessage
429
+ * @returns {void}
430
+ */
431
+ onResponseStart (controller, statusCode, headers, statusMessage) {
432
+ const contentEncoding = headers['content-encoding']
433
+
434
+ // If content encoding is not supported or status code is in skip list
435
+ if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
436
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
437
+ }
438
+
439
+ const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
440
+
441
+ if (decompressors.length === 0) {
442
+ this.#cleanupDecompressors()
443
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
444
+ }
445
+
446
+ this.#decompressors = decompressors
447
+
448
+ // Remove compression headers since we're decompressing
449
+ const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
450
+
451
+ if (this.#controller.rawHeaders) {
452
+ const rawHeaders = this.#controller.rawHeaders
453
+
454
+ if (Array.isArray(rawHeaders)) {
455
+ const filteredHeaders = []
456
+ for (let i = 0; i < rawHeaders.length; i += 2) {
457
+ const headerName = rawHeaders[i]
458
+ const name = Buffer.isBuffer(headerName) ? headerName.toString('latin1') : `${headerName}`
459
+ const lowerName = name.toLowerCase()
460
+
461
+ if (lowerName === 'content-encoding' || lowerName === 'content-length') {
462
+ continue
463
+ }
464
+
465
+ filteredHeaders.push(rawHeaders[i], rawHeaders[i + 1])
466
+ }
467
+ rawHeaders.splice(0, rawHeaders.length, ...filteredHeaders)
468
+ } else if (typeof rawHeaders === 'object') {
469
+ for (const name of Object.keys(rawHeaders)) {
470
+ const lowerName = name.toLowerCase()
471
+ if (lowerName === 'content-encoding' || lowerName === 'content-length') {
472
+ delete rawHeaders[name]
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ this.#setupInputBackpressure()
479
+ if (this.#decompressors.length === 1) {
480
+ this.#setupSingleDecompressor()
481
+ } else {
482
+ this.#setupMultipleDecompressors()
483
+ }
484
+
485
+ return super.onResponseStart(this.#controller, statusCode, newHeaders, statusMessage)
486
+ }
487
+
488
+ /**
489
+ * @param {Controller} controller
490
+ * @param {Buffer} chunk
491
+ * @returns {void}
492
+ */
493
+ onResponseData (controller, chunk) {
494
+ if (this.#decompressors.length > 0) {
495
+ if (!this.#decompressors[0].write(chunk)) {
496
+ this.#inputBackpressured = true
497
+ this.#pauseUpstream()
498
+ }
499
+ return
500
+ }
501
+ return super.onResponseData(this.#controller, chunk)
502
+ }
503
+
504
+ /**
505
+ * @param {Controller} controller
506
+ * @param {Record<string, string | string[]> | undefined} trailers
507
+ * @returns {void}
508
+ */
509
+ onResponseEnd (controller, trailers) {
510
+ if (this.#decompressors.length > 0) {
511
+ this.#inputEnded = true
512
+ this.#trailers = trailers
513
+ this.#decompressors[0].end()
514
+ return
515
+ }
516
+ return super.onResponseEnd(this.#controller, trailers)
517
+ }
518
+
519
+ /**
520
+ * @param {Controller} controller
521
+ * @param {Error} err
522
+ * @returns {void}
523
+ */
524
+ onResponseError (controller, err) {
525
+ if (this.#terminated) {
526
+ return
527
+ }
528
+
529
+ this.#terminated = true
530
+ for (const decompressor of this.#decompressors) {
531
+ decompressor.destroy()
532
+ }
533
+ this.#cleanupDecompressors()
534
+ super.onResponseError(this.#controller, err)
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Creates a decompression interceptor for HTTP responses
540
+ * @param {DecompressHandlerOptions} [options] - Options for the interceptor
541
+ * @returns {Function} - Interceptor function
542
+ */
543
+ function createDecompressInterceptor (options = {}) {
544
+ // Emit experimental warning only once
545
+ if (!warningEmitted) {
546
+ process.emitWarning(
547
+ 'DecompressInterceptor is experimental and subject to change',
548
+ 'ExperimentalWarning'
549
+ )
550
+ warningEmitted = true
551
+ }
552
+
553
+ return (dispatch) => {
554
+ return (opts, handler) => {
555
+ const decompressHandler = new DecompressHandler(handler, options)
556
+ return dispatch(opts, decompressHandler)
557
+ }
558
+ }
559
+ }
560
+
561
+ module.exports = createDecompressInterceptor
@@ -0,0 +1,117 @@
1
+ 'use strict'
2
+
3
+ const diagnosticsChannel = require('node:diagnostics_channel')
4
+ const util = require('../core/util')
5
+ const DeduplicationHandler = require('../handler/deduplication-handler')
6
+ const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
7
+
8
+ const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')
9
+
10
+ /**
11
+ * @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts]
12
+ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
13
+ */
14
+ module.exports = (opts = {}) => {
15
+ const {
16
+ methods = ['GET'],
17
+ skipHeaderNames = [],
18
+ excludeHeaderNames = [],
19
+ maxBufferSize = 5 * 1024 * 1024
20
+ } = opts
21
+
22
+ if (typeof opts !== 'object' || opts === null) {
23
+ throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
24
+ }
25
+
26
+ if (!Array.isArray(methods)) {
27
+ throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)
28
+ }
29
+
30
+ for (const method of methods) {
31
+ if (!util.safeHTTPMethods.includes(method)) {
32
+ throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)
33
+ }
34
+ }
35
+
36
+ if (!Array.isArray(skipHeaderNames)) {
37
+ throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)
38
+ }
39
+
40
+ if (!Array.isArray(excludeHeaderNames)) {
41
+ throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
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
+
48
+ // Convert to lowercase Set for case-insensitive header matching
49
+ const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
50
+
51
+ // Convert to lowercase Set for case-insensitive header exclusion from deduplication key
52
+ const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
53
+
54
+ /**
55
+ * Map of pending requests for deduplication
56
+ * @type {Map<string, DeduplicationHandler>}
57
+ */
58
+ const pendingRequests = new Map()
59
+
60
+ return dispatch => {
61
+ return (opts, handler) => {
62
+ if (!opts.origin || methods.includes(opts.method) === false) {
63
+ return dispatch(opts, handler)
64
+ }
65
+
66
+ opts = {
67
+ ...opts,
68
+ headers: normalizeHeaders(opts)
69
+ }
70
+
71
+ // Skip deduplication if request contains any of the specified headers
72
+ if (skipHeaderNamesSet.size > 0) {
73
+ for (const headerName of Object.keys(opts.headers)) {
74
+ if (skipHeaderNamesSet.has(headerName.toLowerCase())) {
75
+ return dispatch(opts, handler)
76
+ }
77
+ }
78
+ }
79
+
80
+ const cacheKey = makeCacheKey(opts)
81
+ const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet)
82
+
83
+ // Check if there's already a pending request for this key
84
+ const pendingHandler = pendingRequests.get(dedupeKey)
85
+ if (pendingHandler) {
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)
93
+ }
94
+
95
+ // Create a new deduplication handler
96
+ const deduplicationHandler = new DeduplicationHandler(
97
+ handler,
98
+ () => {
99
+ // Clean up when request completes
100
+ pendingRequests.delete(dedupeKey)
101
+ if (pendingRequestsChannel.hasSubscribers) {
102
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
103
+ }
104
+ },
105
+ maxBufferSize
106
+ )
107
+
108
+ // Register the pending request
109
+ pendingRequests.set(dedupeKey, deduplicationHandler)
110
+ if (pendingRequestsChannel.hasSubscribers) {
111
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' })
112
+ }
113
+
114
+ return dispatch(opts, deduplicationHandler)
115
+ }
116
+ }
117
+ }