@depup/mswjs__interceptors 0.41.3-depup.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 (248) hide show
  1. package/ClientRequest/package.json +11 -0
  2. package/LICENSE.md +9 -0
  3. package/README.md +31 -0
  4. package/RemoteHttpInterceptor/package.json +11 -0
  5. package/WebSocket/package.json +12 -0
  6. package/XMLHttpRequest/package.json +12 -0
  7. package/changes.json +10 -0
  8. package/fetch/package.json +12 -0
  9. package/lib/browser/Interceptor-Deczogc8.d.cts +65 -0
  10. package/lib/browser/Interceptor-gqKgs-aF.d.mts +65 -0
  11. package/lib/browser/XMLHttpRequest-BACqefB-.cjs +761 -0
  12. package/lib/browser/XMLHttpRequest-BACqefB-.cjs.map +1 -0
  13. package/lib/browser/XMLHttpRequest-BvxZV0WU.mjs +756 -0
  14. package/lib/browser/XMLHttpRequest-BvxZV0WU.mjs.map +1 -0
  15. package/lib/browser/bufferUtils-BiiO6HZv.mjs +20 -0
  16. package/lib/browser/bufferUtils-BiiO6HZv.mjs.map +1 -0
  17. package/lib/browser/bufferUtils-Uc0eRItL.cjs +38 -0
  18. package/lib/browser/bufferUtils-Uc0eRItL.cjs.map +1 -0
  19. package/lib/browser/createRequestId-Cs4oXfa1.cjs +205 -0
  20. package/lib/browser/createRequestId-Cs4oXfa1.cjs.map +1 -0
  21. package/lib/browser/createRequestId-DQcIlohW.mjs +170 -0
  22. package/lib/browser/createRequestId-DQcIlohW.mjs.map +1 -0
  23. package/lib/browser/fetch-DdKEdDOR.mjs +248 -0
  24. package/lib/browser/fetch-DdKEdDOR.mjs.map +1 -0
  25. package/lib/browser/fetch-U3v3Y4ap.cjs +253 -0
  26. package/lib/browser/fetch-U3v3Y4ap.cjs.map +1 -0
  27. package/lib/browser/getRawRequest-BTaNLFr0.mjs +218 -0
  28. package/lib/browser/getRawRequest-BTaNLFr0.mjs.map +1 -0
  29. package/lib/browser/getRawRequest-zx8rUJL2.cjs +259 -0
  30. package/lib/browser/getRawRequest-zx8rUJL2.cjs.map +1 -0
  31. package/lib/browser/glossary-BdLS4k1H.d.cts +70 -0
  32. package/lib/browser/glossary-DYwOrogs.d.mts +70 -0
  33. package/lib/browser/handleRequest-CvX2G-Lz.cjs +189 -0
  34. package/lib/browser/handleRequest-CvX2G-Lz.cjs.map +1 -0
  35. package/lib/browser/handleRequest-D7kpTI5U.mjs +178 -0
  36. package/lib/browser/handleRequest-D7kpTI5U.mjs.map +1 -0
  37. package/lib/browser/hasConfigurableGlobal-BvCTG97d.cjs +45 -0
  38. package/lib/browser/hasConfigurableGlobal-BvCTG97d.cjs.map +1 -0
  39. package/lib/browser/hasConfigurableGlobal-npXitu1-.mjs +33 -0
  40. package/lib/browser/hasConfigurableGlobal-npXitu1-.mjs.map +1 -0
  41. package/lib/browser/index.cjs +70 -0
  42. package/lib/browser/index.cjs.map +1 -0
  43. package/lib/browser/index.d.cts +96 -0
  44. package/lib/browser/index.d.mts +96 -0
  45. package/lib/browser/index.mjs +56 -0
  46. package/lib/browser/index.mjs.map +1 -0
  47. package/lib/browser/interceptors/WebSocket/index.cjs +622 -0
  48. package/lib/browser/interceptors/WebSocket/index.cjs.map +1 -0
  49. package/lib/browser/interceptors/WebSocket/index.d.cts +277 -0
  50. package/lib/browser/interceptors/WebSocket/index.d.mts +277 -0
  51. package/lib/browser/interceptors/WebSocket/index.mjs +615 -0
  52. package/lib/browser/interceptors/WebSocket/index.mjs.map +1 -0
  53. package/lib/browser/interceptors/XMLHttpRequest/index.cjs +7 -0
  54. package/lib/browser/interceptors/XMLHttpRequest/index.d.cts +15 -0
  55. package/lib/browser/interceptors/XMLHttpRequest/index.d.mts +15 -0
  56. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +7 -0
  57. package/lib/browser/interceptors/fetch/index.cjs +6 -0
  58. package/lib/browser/interceptors/fetch/index.d.cts +13 -0
  59. package/lib/browser/interceptors/fetch/index.d.mts +13 -0
  60. package/lib/browser/interceptors/fetch/index.mjs +6 -0
  61. package/lib/browser/presets/browser.cjs +17 -0
  62. package/lib/browser/presets/browser.cjs.map +1 -0
  63. package/lib/browser/presets/browser.d.cts +12 -0
  64. package/lib/browser/presets/browser.d.mts +14 -0
  65. package/lib/browser/presets/browser.mjs +17 -0
  66. package/lib/browser/presets/browser.mjs.map +1 -0
  67. package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs +31 -0
  68. package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs.map +1 -0
  69. package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs +25 -0
  70. package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs.map +1 -0
  71. package/lib/node/BatchInterceptor-3LnAnLTx.cjs +49 -0
  72. package/lib/node/BatchInterceptor-3LnAnLTx.cjs.map +1 -0
  73. package/lib/node/BatchInterceptor-D7mXzHcQ.d.mts +26 -0
  74. package/lib/node/BatchInterceptor-DFaBPilf.mjs +44 -0
  75. package/lib/node/BatchInterceptor-DFaBPilf.mjs.map +1 -0
  76. package/lib/node/BatchInterceptor-D_YqR8qU.d.cts +26 -0
  77. package/lib/node/ClientRequest-2rDe54Ui.cjs +1043 -0
  78. package/lib/node/ClientRequest-2rDe54Ui.cjs.map +1 -0
  79. package/lib/node/ClientRequest-Ca8Qykuv.mjs +1034 -0
  80. package/lib/node/ClientRequest-Ca8Qykuv.mjs.map +1 -0
  81. package/lib/node/Interceptor-DEazpLJd.d.mts +133 -0
  82. package/lib/node/Interceptor-DJ2akVWI.d.cts +133 -0
  83. package/lib/node/RemoteHttpInterceptor.cjs +154 -0
  84. package/lib/node/RemoteHttpInterceptor.cjs.map +1 -0
  85. package/lib/node/RemoteHttpInterceptor.d.cts +39 -0
  86. package/lib/node/RemoteHttpInterceptor.d.mts +39 -0
  87. package/lib/node/RemoteHttpInterceptor.mjs +152 -0
  88. package/lib/node/RemoteHttpInterceptor.mjs.map +1 -0
  89. package/lib/node/XMLHttpRequest-B7kJdYYI.cjs +763 -0
  90. package/lib/node/XMLHttpRequest-B7kJdYYI.cjs.map +1 -0
  91. package/lib/node/XMLHttpRequest-C8dIZpds.mjs +757 -0
  92. package/lib/node/XMLHttpRequest-C8dIZpds.mjs.map +1 -0
  93. package/lib/node/bufferUtils-DiCTqG-7.cjs +38 -0
  94. package/lib/node/bufferUtils-DiCTqG-7.cjs.map +1 -0
  95. package/lib/node/bufferUtils-_8XfKIfX.mjs +20 -0
  96. package/lib/node/bufferUtils-_8XfKIfX.mjs.map +1 -0
  97. package/lib/node/chunk-CbDLau6x.cjs +34 -0
  98. package/lib/node/fetch-BmXpK10r.cjs +272 -0
  99. package/lib/node/fetch-BmXpK10r.cjs.map +1 -0
  100. package/lib/node/fetch-G1DVwDKG.mjs +265 -0
  101. package/lib/node/fetch-G1DVwDKG.mjs.map +1 -0
  102. package/lib/node/fetchUtils-BaY5iWXw.cjs +419 -0
  103. package/lib/node/fetchUtils-BaY5iWXw.cjs.map +1 -0
  104. package/lib/node/fetchUtils-CoU35g3M.mjs +359 -0
  105. package/lib/node/fetchUtils-CoU35g3M.mjs.map +1 -0
  106. package/lib/node/getRawRequest-BavnMWh_.cjs +36 -0
  107. package/lib/node/getRawRequest-BavnMWh_.cjs.map +1 -0
  108. package/lib/node/getRawRequest-DnwmXyOW.mjs +24 -0
  109. package/lib/node/getRawRequest-DnwmXyOW.mjs.map +1 -0
  110. package/lib/node/glossary-BLKRyLBd.cjs +12 -0
  111. package/lib/node/glossary-BLKRyLBd.cjs.map +1 -0
  112. package/lib/node/glossary-glQBRnVD.mjs +6 -0
  113. package/lib/node/glossary-glQBRnVD.mjs.map +1 -0
  114. package/lib/node/handleRequest-Bb7Y-XLw.cjs +220 -0
  115. package/lib/node/handleRequest-Bb7Y-XLw.cjs.map +1 -0
  116. package/lib/node/handleRequest-Y97UwBbF.mjs +190 -0
  117. package/lib/node/handleRequest-Y97UwBbF.mjs.map +1 -0
  118. package/lib/node/hasConfigurableGlobal-C97fWuaA.cjs +26 -0
  119. package/lib/node/hasConfigurableGlobal-C97fWuaA.cjs.map +1 -0
  120. package/lib/node/hasConfigurableGlobal-DBJA0vjm.mjs +20 -0
  121. package/lib/node/hasConfigurableGlobal-DBJA0vjm.mjs.map +1 -0
  122. package/lib/node/index-BMbJ8FXL.d.cts +113 -0
  123. package/lib/node/index-C0YAQ36w.d.mts +113 -0
  124. package/lib/node/index.cjs +54 -0
  125. package/lib/node/index.cjs.map +1 -0
  126. package/lib/node/index.d.cts +75 -0
  127. package/lib/node/index.d.mts +75 -0
  128. package/lib/node/index.mjs +40 -0
  129. package/lib/node/index.mjs.map +1 -0
  130. package/lib/node/interceptors/ClientRequest/index.cjs +6 -0
  131. package/lib/node/interceptors/ClientRequest/index.d.cts +2 -0
  132. package/lib/node/interceptors/ClientRequest/index.d.mts +3 -0
  133. package/lib/node/interceptors/ClientRequest/index.mjs +6 -0
  134. package/lib/node/interceptors/XMLHttpRequest/index.cjs +6 -0
  135. package/lib/node/interceptors/XMLHttpRequest/index.d.cts +14 -0
  136. package/lib/node/interceptors/XMLHttpRequest/index.d.mts +14 -0
  137. package/lib/node/interceptors/XMLHttpRequest/index.mjs +6 -0
  138. package/lib/node/interceptors/fetch/index.cjs +5 -0
  139. package/lib/node/interceptors/fetch/index.d.cts +12 -0
  140. package/lib/node/interceptors/fetch/index.d.mts +12 -0
  141. package/lib/node/interceptors/fetch/index.mjs +5 -0
  142. package/lib/node/node-DwCc6iuP.mjs +27 -0
  143. package/lib/node/node-DwCc6iuP.mjs.map +1 -0
  144. package/lib/node/node-dKdAf3tC.cjs +39 -0
  145. package/lib/node/node-dKdAf3tC.cjs.map +1 -0
  146. package/lib/node/presets/node.cjs +22 -0
  147. package/lib/node/presets/node.cjs.map +1 -0
  148. package/lib/node/presets/node.d.cts +13 -0
  149. package/lib/node/presets/node.d.mts +15 -0
  150. package/lib/node/presets/node.mjs +22 -0
  151. package/lib/node/presets/node.mjs.map +1 -0
  152. package/lib/node/utils/node/index.cjs +4 -0
  153. package/lib/node/utils/node/index.d.cts +16 -0
  154. package/lib/node/utils/node/index.d.mts +16 -0
  155. package/lib/node/utils/node/index.mjs +3 -0
  156. package/package.json +204 -0
  157. package/presets/browser/package.json +5 -0
  158. package/presets/node/package.json +11 -0
  159. package/src/BatchInterceptor.test.ts +255 -0
  160. package/src/BatchInterceptor.ts +95 -0
  161. package/src/Interceptor.test.ts +205 -0
  162. package/src/Interceptor.ts +249 -0
  163. package/src/InterceptorError.ts +7 -0
  164. package/src/RemoteHttpInterceptor.ts +251 -0
  165. package/src/RequestController.test.ts +104 -0
  166. package/src/RequestController.ts +109 -0
  167. package/src/createRequestId.test.ts +7 -0
  168. package/src/createRequestId.ts +9 -0
  169. package/src/getRawRequest.ts +21 -0
  170. package/src/glossary.ts +37 -0
  171. package/src/index.ts +15 -0
  172. package/src/interceptors/ClientRequest/MockHttpSocket.ts +724 -0
  173. package/src/interceptors/ClientRequest/agents.ts +110 -0
  174. package/src/interceptors/ClientRequest/index.test.ts +75 -0
  175. package/src/interceptors/ClientRequest/index.ts +193 -0
  176. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +54 -0
  177. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +45 -0
  178. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +427 -0
  179. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +268 -0
  180. package/src/interceptors/ClientRequest/utils/parserUtils.ts +48 -0
  181. package/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts +258 -0
  182. package/src/interceptors/ClientRequest/utils/recordRawHeaders.ts +262 -0
  183. package/src/interceptors/Socket/MockSocket.test.ts +264 -0
  184. package/src/interceptors/Socket/MockSocket.ts +58 -0
  185. package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +26 -0
  186. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +52 -0
  187. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +33 -0
  188. package/src/interceptors/WebSocket/WebSocketClassTransport.ts +116 -0
  189. package/src/interceptors/WebSocket/WebSocketClientConnection.ts +152 -0
  190. package/src/interceptors/WebSocket/WebSocketOverride.ts +252 -0
  191. package/src/interceptors/WebSocket/WebSocketServerConnection.ts +420 -0
  192. package/src/interceptors/WebSocket/WebSocketTransport.ts +39 -0
  193. package/src/interceptors/WebSocket/index.ts +191 -0
  194. package/src/interceptors/WebSocket/utils/bindEvent.test.ts +27 -0
  195. package/src/interceptors/WebSocket/utils/bindEvent.ts +21 -0
  196. package/src/interceptors/WebSocket/utils/events.test.ts +101 -0
  197. package/src/interceptors/WebSocket/utils/events.ts +94 -0
  198. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +746 -0
  199. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +121 -0
  200. package/src/interceptors/XMLHttpRequest/index.ts +61 -0
  201. package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
  202. package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
  203. package/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts +12 -0
  204. package/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts +12 -0
  205. package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +26 -0
  206. package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
  207. package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +49 -0
  208. package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.test.ts +164 -0
  209. package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts +16 -0
  210. package/src/interceptors/XMLHttpRequest/utils/isDomParserSupportedType.ts +14 -0
  211. package/src/interceptors/fetch/index.ts +214 -0
  212. package/src/interceptors/fetch/utils/brotli-decompress.browser.ts +14 -0
  213. package/src/interceptors/fetch/utils/brotli-decompress.ts +31 -0
  214. package/src/interceptors/fetch/utils/createNetworkError.ts +5 -0
  215. package/src/interceptors/fetch/utils/decompression.ts +85 -0
  216. package/src/interceptors/fetch/utils/followRedirect.ts +114 -0
  217. package/src/presets/browser.ts +11 -0
  218. package/src/presets/node.ts +13 -0
  219. package/src/utils/bufferUtils.test.ts +21 -0
  220. package/src/utils/bufferUtils.ts +22 -0
  221. package/src/utils/canParseUrl.ts +13 -0
  222. package/src/utils/cloneObject.test.ts +94 -0
  223. package/src/utils/cloneObject.ts +36 -0
  224. package/src/utils/createProxy.test.ts +164 -0
  225. package/src/utils/createProxy.ts +104 -0
  226. package/src/utils/emitAsync.ts +25 -0
  227. package/src/utils/fetchUtils.ts +119 -0
  228. package/src/utils/findPropertySource.test.ts +27 -0
  229. package/src/utils/findPropertySource.ts +20 -0
  230. package/src/utils/getCleanUrl.test.ts +32 -0
  231. package/src/utils/getCleanUrl.ts +6 -0
  232. package/src/utils/getUrlByRequestOptions.test.ts +163 -0
  233. package/src/utils/getUrlByRequestOptions.ts +152 -0
  234. package/src/utils/getValueBySymbol.test.ts +14 -0
  235. package/src/utils/getValueBySymbol.ts +19 -0
  236. package/src/utils/handleRequest.ts +205 -0
  237. package/src/utils/hasConfigurableGlobal.test.ts +83 -0
  238. package/src/utils/hasConfigurableGlobal.ts +34 -0
  239. package/src/utils/isNodeLikeError.ts +13 -0
  240. package/src/utils/isObject.test.ts +21 -0
  241. package/src/utils/isObject.ts +8 -0
  242. package/src/utils/isPropertyAccessible.ts +19 -0
  243. package/src/utils/nextTick.ts +11 -0
  244. package/src/utils/node/index.ts +39 -0
  245. package/src/utils/parseJson.test.ts +10 -0
  246. package/src/utils/parseJson.ts +12 -0
  247. package/src/utils/resolveWebSocketUrl.ts +45 -0
  248. package/src/utils/responseUtils.ts +59 -0
@@ -0,0 +1,214 @@
1
+ import { invariant } from 'outvariant'
2
+ import { until } from '@open-draft/until'
3
+ import { DeferredPromise } from '@open-draft/deferred-promise'
4
+ import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
5
+ import { Interceptor } from '../../Interceptor'
6
+ import { RequestController } from '../../RequestController'
7
+ import { emitAsync } from '../../utils/emitAsync'
8
+ import { handleRequest } from '../../utils/handleRequest'
9
+ import { canParseUrl } from '../../utils/canParseUrl'
10
+ import { createRequestId } from '../../createRequestId'
11
+ import { createNetworkError } from './utils/createNetworkError'
12
+ import { followFetchRedirect } from './utils/followRedirect'
13
+ import { decompressResponse } from './utils/decompression'
14
+ import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
15
+ import { FetchResponse } from '../../utils/fetchUtils'
16
+ import { setRawRequest } from '../../getRawRequest'
17
+ import { isResponseError } from '../../utils/responseUtils'
18
+
19
+ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
20
+ static symbol = Symbol('fetch')
21
+
22
+ constructor() {
23
+ super(FetchInterceptor.symbol)
24
+ }
25
+
26
+ protected checkEnvironment() {
27
+ return hasConfigurableGlobal('fetch')
28
+ }
29
+
30
+ protected async setup() {
31
+ const pureFetch = globalThis.fetch
32
+
33
+ invariant(
34
+ !(pureFetch as any)[IS_PATCHED_MODULE],
35
+ 'Failed to patch the "fetch" module: already patched.'
36
+ )
37
+
38
+ globalThis.fetch = async (input, init) => {
39
+ const requestId = createRequestId()
40
+
41
+ /**
42
+ * @note Resolve potentially relative request URL
43
+ * against the present `location`. This is mainly
44
+ * for native `fetch` in JSDOM.
45
+ * @see https://github.com/mswjs/msw/issues/1625
46
+ */
47
+ const resolvedInput =
48
+ typeof input === 'string' &&
49
+ typeof location !== 'undefined' &&
50
+ !canParseUrl(input)
51
+ ? new URL(input, location.href)
52
+ : input
53
+
54
+ const request = new Request(resolvedInput, init)
55
+
56
+ /**
57
+ * @note Set the raw request only if a Request instance was provided to fetch.
58
+ */
59
+ if (input instanceof Request) {
60
+ setRawRequest(request, input)
61
+ }
62
+
63
+ const responsePromise = new DeferredPromise<Response>()
64
+
65
+ const controller = new RequestController(request, {
66
+ passthrough: async () => {
67
+ this.logger.info('request has not been handled, passthrough...')
68
+
69
+ /**
70
+ * @note Clone the request instance right before performing it.
71
+ * This preserves any modifications made to the intercepted request
72
+ * in the "request" listener. This also allows the user to read the
73
+ * request body in the "response" listener (otherwise "unusable").
74
+ */
75
+ const requestCloneForResponseEvent = request.clone()
76
+
77
+ // Perform the intercepted request as-is.
78
+ const { error: responseError, data: originalResponse } = await until(
79
+ () => pureFetch(request)
80
+ )
81
+
82
+ if (responseError) {
83
+ return responsePromise.reject(responseError)
84
+ }
85
+
86
+ this.logger.info('original fetch performed', originalResponse)
87
+
88
+ if (this.emitter.listenerCount('response') > 0) {
89
+ this.logger.info('emitting the "response" event...')
90
+
91
+ const responseClone = originalResponse.clone()
92
+ await emitAsync(this.emitter, 'response', {
93
+ response: responseClone,
94
+ isMockedResponse: false,
95
+ request: requestCloneForResponseEvent,
96
+ requestId,
97
+ })
98
+ }
99
+
100
+ // Resolve the response promise with the original response
101
+ // since the `fetch()` return this internal promise.
102
+ responsePromise.resolve(originalResponse)
103
+ },
104
+ respondWith: async (rawResponse) => {
105
+ // Handle mocked `Response.error()` (i.e. request errors).
106
+ if (isResponseError(rawResponse)) {
107
+ this.logger.info('request has errored!', { response: rawResponse })
108
+ responsePromise.reject(createNetworkError(rawResponse))
109
+ return
110
+ }
111
+
112
+ this.logger.info('received mocked response!', {
113
+ rawResponse,
114
+ })
115
+
116
+ // Decompress the mocked response body, if applicable.
117
+ const decompressedStream = decompressResponse(rawResponse)
118
+ const response =
119
+ decompressedStream === null
120
+ ? rawResponse
121
+ : new FetchResponse(decompressedStream, rawResponse)
122
+
123
+ FetchResponse.setUrl(request.url, response)
124
+
125
+ /**
126
+ * Undici's handling of following redirect responses.
127
+ * Treat the "manual" redirect mode as a regular mocked response.
128
+ * This way, the client can manually follow the redirect it receives.
129
+ * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1173
130
+ */
131
+ if (FetchResponse.isRedirectResponse(response.status)) {
132
+ // Reject the request promise if its `redirect` is set to `error`
133
+ // and it receives a mocked redirect response.
134
+ if (request.redirect === 'error') {
135
+ responsePromise.reject(createNetworkError('unexpected redirect'))
136
+ return
137
+ }
138
+
139
+ if (request.redirect === 'follow') {
140
+ followFetchRedirect(request, response).then(
141
+ (response) => {
142
+ responsePromise.resolve(response)
143
+ },
144
+ (reason) => {
145
+ responsePromise.reject(reason)
146
+ }
147
+ )
148
+ return
149
+ }
150
+ }
151
+
152
+ if (this.emitter.listenerCount('response') > 0) {
153
+ this.logger.info('emitting the "response" event...')
154
+
155
+ // Await the response listeners to finish before resolving
156
+ // the response promise. This ensures all your logic finishes
157
+ // before the interceptor resolves the pending response.
158
+ await emitAsync(this.emitter, 'response', {
159
+ // Clone the mocked response for the "response" event listener.
160
+ // This way, the listener can read the response and not lock its body
161
+ // for the actual fetch consumer.
162
+ response: response.clone(),
163
+ isMockedResponse: true,
164
+ request,
165
+ requestId,
166
+ })
167
+ }
168
+
169
+ responsePromise.resolve(response)
170
+ },
171
+ errorWith: (reason) => {
172
+ this.logger.info('request has been aborted!', { reason })
173
+ responsePromise.reject(reason)
174
+ },
175
+ })
176
+
177
+ this.logger.info('[%s] %s', request.method, request.url)
178
+ this.logger.info('awaiting for the mocked response...')
179
+
180
+ this.logger.info(
181
+ 'emitting the "request" event for %s listener(s)...',
182
+ this.emitter.listenerCount('request')
183
+ )
184
+
185
+ await handleRequest({
186
+ request,
187
+ requestId,
188
+ emitter: this.emitter,
189
+ controller,
190
+ })
191
+
192
+ return responsePromise
193
+ }
194
+
195
+ Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
196
+ enumerable: true,
197
+ configurable: true,
198
+ value: true,
199
+ })
200
+
201
+ this.subscriptions.push(() => {
202
+ Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
203
+ value: undefined,
204
+ })
205
+
206
+ globalThis.fetch = pureFetch
207
+
208
+ this.logger.info(
209
+ 'restored native "globalThis.fetch"!',
210
+ globalThis.fetch.name
211
+ )
212
+ })
213
+ }
214
+ }
@@ -0,0 +1,14 @@
1
+ export class BrotliDecompressionStream extends TransformStream {
2
+ constructor() {
3
+ console.warn(
4
+ '[Interceptors]: Brotli decompression of response streams is not supported in the browser'
5
+ )
6
+
7
+ super({
8
+ transform(chunk, controller) {
9
+ // Keep the stream as passthrough, it does nothing.
10
+ controller.enqueue(chunk)
11
+ },
12
+ })
13
+ }
14
+ }
@@ -0,0 +1,31 @@
1
+ import zlib from 'node:zlib'
2
+
3
+ export class BrotliDecompressionStream extends TransformStream {
4
+ constructor() {
5
+ const decompress = zlib.createBrotliDecompress({
6
+ flush: zlib.constants.BROTLI_OPERATION_FLUSH,
7
+ finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH,
8
+ })
9
+
10
+ super({
11
+ async transform(chunk, controller) {
12
+ const buffer = Buffer.from(chunk)
13
+
14
+ const decompressed = await new Promise<Buffer>((resolve, reject) => {
15
+ decompress.write(buffer, (error) => {
16
+ if (error) reject(error)
17
+ })
18
+
19
+ decompress.flush()
20
+ decompress.once('data', (data) => resolve(data))
21
+ decompress.once('error', (error) => reject(error))
22
+ decompress.once('end', () => controller.terminate())
23
+ }).catch((error) => {
24
+ controller.error(error)
25
+ })
26
+
27
+ controller.enqueue(decompressed)
28
+ },
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,5 @@
1
+ export function createNetworkError(cause?: unknown) {
2
+ return Object.assign(new TypeError('Failed to fetch'), {
3
+ cause,
4
+ })
5
+ }
@@ -0,0 +1,85 @@
1
+ // Import from an internal alias that resolves to different modules
2
+ // depending on the environment. This way, we can keep the fetch interceptor
3
+ // intact while using different strategies for Brotli decompression.
4
+ import { BrotliDecompressionStream } from 'internal:brotli-decompress'
5
+
6
+ class PipelineStream extends TransformStream {
7
+ constructor(
8
+ transformStreams: Array<TransformStream>,
9
+ ...strategies: Array<QueuingStrategy>
10
+ ) {
11
+ super({}, ...strategies)
12
+
13
+ const readable = [super.readable as any, ...transformStreams].reduce(
14
+ (readable, transform) => readable.pipeThrough(transform)
15
+ )
16
+
17
+ Object.defineProperty(this, 'readable', {
18
+ get() {
19
+ return readable
20
+ },
21
+ })
22
+ }
23
+ }
24
+
25
+ export function parseContentEncoding(contentEncoding: string): Array<string> {
26
+ return contentEncoding
27
+ .toLowerCase()
28
+ .split(',')
29
+ .map((coding) => coding.trim())
30
+ }
31
+
32
+ function createDecompressionStream(
33
+ contentEncoding: string
34
+ ): TransformStream | null {
35
+ if (contentEncoding === '') {
36
+ return null
37
+ }
38
+
39
+ const codings = parseContentEncoding(contentEncoding)
40
+
41
+ if (codings.length === 0) {
42
+ return null
43
+ }
44
+
45
+ const transformers = codings.reduceRight<Array<TransformStream>>(
46
+ (transformers, coding) => {
47
+ if (coding === 'gzip' || coding === 'x-gzip') {
48
+ return transformers.concat(new DecompressionStream('gzip'))
49
+ } else if (coding === 'deflate') {
50
+ return transformers.concat(new DecompressionStream('deflate'))
51
+ } else if (coding === 'br') {
52
+ return transformers.concat(new BrotliDecompressionStream())
53
+ } else {
54
+ transformers.length = 0
55
+ }
56
+
57
+ return transformers
58
+ },
59
+ []
60
+ )
61
+
62
+ return new PipelineStream(transformers)
63
+ }
64
+
65
+ export function decompressResponse(
66
+ response: Response
67
+ ): ReadableStream<any> | null {
68
+ if (response.body === null) {
69
+ return null
70
+ }
71
+
72
+ const decompressionStream = createDecompressionStream(
73
+ response.headers.get('content-encoding') || ''
74
+ )
75
+
76
+ if (!decompressionStream) {
77
+ return null
78
+ }
79
+
80
+ // Use `pipeTo` and return the decompression stream's readable
81
+ // instead of `pipeThrough` because that will lock the original
82
+ // response stream, making it unusable as the input to Response.
83
+ response.body.pipeTo(decompressionStream.writable)
84
+ return decompressionStream.readable
85
+ }
@@ -0,0 +1,114 @@
1
+ import { createNetworkError } from './createNetworkError'
2
+
3
+ const REQUEST_BODY_HEADERS = [
4
+ 'content-encoding',
5
+ 'content-language',
6
+ 'content-location',
7
+ 'content-type',
8
+ 'content-length',
9
+ ]
10
+
11
+ const kRedirectCount = Symbol('kRedirectCount')
12
+
13
+ /**
14
+ * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1210
15
+ */
16
+ export async function followFetchRedirect(
17
+ request: Request,
18
+ response: Response
19
+ ): Promise<Response> {
20
+ if (response.status !== 303 && request.body != null) {
21
+ return Promise.reject(createNetworkError())
22
+ }
23
+
24
+ const requestUrl = new URL(request.url)
25
+
26
+ let locationUrl: URL
27
+ try {
28
+ // If the location is a relative URL, use the request URL as the base URL.
29
+ locationUrl = new URL(response.headers.get('location')!, request.url)
30
+ } catch (error) {
31
+ return Promise.reject(createNetworkError(error))
32
+ }
33
+
34
+ if (
35
+ !(locationUrl.protocol === 'http:' || locationUrl.protocol === 'https:')
36
+ ) {
37
+ return Promise.reject(
38
+ createNetworkError('URL scheme must be a HTTP(S) scheme')
39
+ )
40
+ }
41
+
42
+ if (Reflect.get(request, kRedirectCount) > 20) {
43
+ return Promise.reject(createNetworkError('redirect count exceeded'))
44
+ }
45
+
46
+ Object.defineProperty(request, kRedirectCount, {
47
+ value: (Reflect.get(request, kRedirectCount) || 0) + 1,
48
+ })
49
+
50
+ if (
51
+ request.mode === 'cors' &&
52
+ (locationUrl.username || locationUrl.password) &&
53
+ !sameOrigin(requestUrl, locationUrl)
54
+ ) {
55
+ return Promise.reject(
56
+ createNetworkError('cross origin not allowed for request mode "cors"')
57
+ )
58
+ }
59
+
60
+ const requestInit: RequestInit = {}
61
+
62
+ if (
63
+ ([301, 302].includes(response.status) && request.method === 'POST') ||
64
+ (response.status === 303 && !['HEAD', 'GET'].includes(request.method))
65
+ ) {
66
+ requestInit.method = 'GET'
67
+ requestInit.body = null
68
+
69
+ REQUEST_BODY_HEADERS.forEach((headerName) => {
70
+ request.headers.delete(headerName)
71
+ })
72
+ }
73
+
74
+ if (!sameOrigin(requestUrl, locationUrl)) {
75
+ request.headers.delete('authorization')
76
+ request.headers.delete('proxy-authorization')
77
+ request.headers.delete('cookie')
78
+ request.headers.delete('host')
79
+ }
80
+
81
+ /**
82
+ * @note Undici "safely" extracts the request body.
83
+ * I suspect we cannot dispatch this request again
84
+ * since its body has been read and the stream is locked.
85
+ */
86
+
87
+ requestInit.headers = request.headers
88
+ const finalResponse = await fetch(new Request(locationUrl, requestInit))
89
+ Object.defineProperty(finalResponse, 'redirected', {
90
+ value: true,
91
+ configurable: true,
92
+ })
93
+
94
+ return finalResponse
95
+ }
96
+
97
+ /**
98
+ * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/util.js#L761
99
+ */
100
+ function sameOrigin(left: URL, right: URL): boolean {
101
+ if (left.origin === right.origin && left.origin === 'null') {
102
+ return true
103
+ }
104
+
105
+ if (
106
+ left.protocol === right.protocol &&
107
+ left.hostname === right.hostname &&
108
+ left.port === right.port
109
+ ) {
110
+ return true
111
+ }
112
+
113
+ return false
114
+ }
@@ -0,0 +1,11 @@
1
+ import { FetchInterceptor } from '../interceptors/fetch'
2
+ import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
3
+
4
+ /**
5
+ * The default preset provisions the interception of requests
6
+ * regardless of their type (fetch/XMLHttpRequest).
7
+ */
8
+ export default [
9
+ new FetchInterceptor(),
10
+ new XMLHttpRequestInterceptor(),
11
+ ] as const
@@ -0,0 +1,13 @@
1
+ import { ClientRequestInterceptor } from '../interceptors/ClientRequest'
2
+ import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
3
+ import { FetchInterceptor } from '../interceptors/fetch'
4
+
5
+ /**
6
+ * The default preset provisions the interception of requests
7
+ * regardless of their type (http/https/XMLHttpRequest).
8
+ */
9
+ export default [
10
+ new ClientRequestInterceptor(),
11
+ new XMLHttpRequestInterceptor(),
12
+ new FetchInterceptor(),
13
+ ] as const
@@ -0,0 +1,21 @@
1
+ import { it, expect } from 'vitest'
2
+ import { decodeBuffer, encodeBuffer } from './bufferUtils'
3
+
4
+ it('encodes utf-8 string', () => {
5
+ const encoded = encodeBuffer('😁')
6
+ expect(new Uint8Array(encoded)).toEqual(new Uint8Array([240, 159, 152, 129]))
7
+ })
8
+
9
+ it('decodes utf-8 string', () => {
10
+ const array = new Uint8Array([240, 159, 152, 129])
11
+ const decoded = decodeBuffer(array.buffer)
12
+ expect(decoded).toEqual('😁')
13
+ })
14
+
15
+ it('decodes string with custom encoding', () => {
16
+ const array = new Uint8Array([
17
+ 207, 240, 232, 226, 229, 242, 44, 32, 236, 232, 240, 33,
18
+ ])
19
+ const decoded = decodeBuffer(array.buffer, 'windows-1251')
20
+ expect(decoded).toEqual('Привет, мир!')
21
+ })
@@ -0,0 +1,22 @@
1
+ const encoder = new TextEncoder()
2
+
3
+ export function encodeBuffer(text: string): Uint8Array {
4
+ return encoder.encode(text)
5
+ }
6
+
7
+ export function decodeBuffer(buffer: ArrayBuffer, encoding?: string): string {
8
+ const decoder = new TextDecoder(encoding)
9
+ return decoder.decode(buffer)
10
+ }
11
+
12
+ /**
13
+ * Create an `ArrayBuffer` from the given `Uint8Array`.
14
+ * Takes the byte offset into account to produce the right buffer
15
+ * in the case when the buffer is bigger than the data view.
16
+ */
17
+ export function toArrayBuffer(array: Uint8Array): ArrayBuffer {
18
+ return array.buffer.slice(
19
+ array.byteOffset,
20
+ array.byteOffset + array.byteLength
21
+ )
22
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns a boolean indicating whether the given URL string
3
+ * can be parsed into a `URL` instance.
4
+ * A substitute for `URL.canParse()` for Node.js 18.
5
+ */
6
+ export function canParseUrl(url: string): boolean {
7
+ try {
8
+ new URL(url)
9
+ return true
10
+ } catch (_error) {
11
+ return false
12
+ }
13
+ }
@@ -0,0 +1,94 @@
1
+ import { it, expect } from 'vitest'
2
+ import { cloneObject } from './cloneObject'
3
+
4
+ it('clones a shallow object', () => {
5
+ const original = { a: 1, b: 2, c: [1, 2, 3] }
6
+ const clone = cloneObject(original)
7
+
8
+ expect(clone).toEqual(original)
9
+
10
+ clone.a = 5
11
+ clone.b = 6
12
+ clone.c = [5, 6, 7]
13
+
14
+ expect(clone).toHaveProperty('a', 5)
15
+ expect(clone).toHaveProperty('b', 6)
16
+ expect(clone).toHaveProperty('c', [5, 6, 7])
17
+ expect(original).toHaveProperty('a', 1)
18
+ expect(original).toHaveProperty('b', 2)
19
+ expect(original).toHaveProperty('c', [1, 2, 3])
20
+ })
21
+
22
+ it('clones a nested object', () => {
23
+ const original = { a: { b: 1 }, c: { d: { e: 2 } } }
24
+ const clone = cloneObject(original)
25
+
26
+ expect(clone).toEqual(original)
27
+
28
+ clone.a.b = 10
29
+ clone.c.d.e = 20
30
+
31
+ expect(clone).toHaveProperty(['a', 'b'], 10)
32
+ expect(clone).toHaveProperty(['c', 'd', 'e'], 20)
33
+ expect(original).toHaveProperty(['a', 'b'], 1)
34
+ expect(original).toHaveProperty(['c', 'd', 'e'], 2)
35
+ })
36
+
37
+ it('clones a class instance', () => {
38
+ class Car {
39
+ public manufacturer: string
40
+ constructor() {
41
+ this.manufacturer = 'Audi'
42
+ }
43
+ getManufacturer() {
44
+ return this.manufacturer
45
+ }
46
+ }
47
+
48
+ const car = new Car()
49
+ const clone = cloneObject(car)
50
+
51
+ expect(clone).toHaveProperty('manufacturer', 'Audi')
52
+ expect(clone).toHaveProperty('getManufacturer')
53
+ expect(clone.getManufacturer).toBeInstanceOf(Function)
54
+ expect(clone.getManufacturer()).toEqual('Audi')
55
+ })
56
+
57
+ it('ignores nested class instances', () => {
58
+ class Car {
59
+ name: string
60
+ constructor(name: string) {
61
+ this.name = name
62
+ }
63
+ getName() {
64
+ return this.name
65
+ }
66
+ }
67
+ const original = {
68
+ a: 1,
69
+ car: new Car('Audi'),
70
+ }
71
+ const clone = cloneObject(original)
72
+
73
+ expect(clone).toEqual(original)
74
+ expect(clone.car).toBeInstanceOf(Car)
75
+ expect(clone.car.getName()).toEqual('Audi')
76
+
77
+ clone.car = new Car('BMW')
78
+
79
+ expect(clone.car).toBeInstanceOf(Car)
80
+ expect(clone.car.getName()).toEqual('BMW')
81
+ expect(original.car).toBeInstanceOf(Car)
82
+ expect(original.car.getName()).toEqual('Audi')
83
+ })
84
+
85
+ it('clones an object with null prototype', () => {
86
+ const original = {
87
+ key: Object.create(null),
88
+ }
89
+ const clone = cloneObject(original)
90
+
91
+ expect(clone).toEqual({
92
+ key: {},
93
+ })
94
+ })
@@ -0,0 +1,36 @@
1
+ import { Logger } from '@open-draft/logger'
2
+
3
+ const logger = new Logger('cloneObject')
4
+
5
+ function isPlainObject(obj?: Record<string, any>): boolean {
6
+ logger.info('is plain object?', obj)
7
+
8
+ if (obj == null || !obj.constructor?.name) {
9
+ logger.info('given object is undefined, not a plain object...')
10
+ return false
11
+ }
12
+
13
+ logger.info('checking the object constructor:', obj.constructor.name)
14
+ return obj.constructor.name === 'Object'
15
+ }
16
+
17
+ export function cloneObject<ObjectType extends Record<string, any>>(
18
+ obj: ObjectType
19
+ ): ObjectType {
20
+ logger.info('cloning object:', obj)
21
+
22
+ const enumerableProperties = Object.entries(obj).reduce<Record<string, any>>(
23
+ (acc, [key, value]) => {
24
+ logger.info('analyzing key-value pair:', key, value)
25
+
26
+ // Recursively clone only plain objects, omitting class instances.
27
+ acc[key] = isPlainObject(value) ? cloneObject(value) : value
28
+ return acc
29
+ },
30
+ {}
31
+ )
32
+
33
+ return isPlainObject(obj)
34
+ ? enumerableProperties
35
+ : Object.assign(Object.getPrototypeOf(obj), enumerableProperties)
36
+ }