@mswjs/interceptors 0.41.8 → 0.42.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 (335) hide show
  1. package/README.md +274 -228
  2. package/lib/browser/{bufferUtils-BiiO6HZv.mjs → buffer-utils-DJj7YzLG.js} +3 -3
  3. package/lib/browser/buffer-utils-DJj7YzLG.js.map +1 -0
  4. package/lib/browser/create-request-id-DlEd4GOA.js +184 -0
  5. package/lib/browser/create-request-id-DlEd4GOA.js.map +1 -0
  6. package/lib/browser/{getRawRequest-B1BqgWG6.mjs → fetch-utils-zxA_SD66.js} +123 -52
  7. package/lib/browser/fetch-utils-zxA_SD66.js.map +1 -0
  8. package/lib/browser/handle-request-CIOa9O-N.js +167 -0
  9. package/lib/browser/handle-request-CIOa9O-N.js.map +1 -0
  10. package/lib/{node/hasConfigurableGlobal-BiTmog1u.mjs → browser/has-configurable-global-Cew-dYqk.js} +5 -7
  11. package/lib/browser/has-configurable-global-Cew-dYqk.js.map +1 -0
  12. package/lib/browser/http-CB9rQ7i_.d.ts +105 -0
  13. package/lib/browser/{index.d.cts → index.d.ts} +33 -38
  14. package/lib/browser/index.js +130 -0
  15. package/lib/browser/index.js.map +1 -0
  16. package/lib/browser/interceptor-dtJZ_9QT.d.ts +50 -0
  17. package/lib/browser/interceptors/WebSocket/index.d.ts +2 -0
  18. package/lib/browser/interceptors/WebSocket/{index.mjs → index.js} +40 -31
  19. package/lib/browser/interceptors/WebSocket/index.js.map +1 -0
  20. package/lib/browser/interceptors/XMLHttpRequest/web.d.ts +11 -0
  21. package/lib/browser/interceptors/XMLHttpRequest/web.js +2 -0
  22. package/lib/browser/interceptors/fetch/web.d.ts +14 -0
  23. package/lib/browser/interceptors/fetch/web.js +2 -0
  24. package/lib/browser/presets/browser.d.ts +11 -0
  25. package/lib/browser/presets/browser.js +12 -0
  26. package/lib/browser/presets/browser.js.map +1 -0
  27. package/lib/browser/{resolveWebSocketUrl-C83-x9iE.mjs → resolve-web-socket-url-CSvNPLGi.js} +3 -3
  28. package/lib/browser/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
  29. package/lib/browser/web-BjVDkiX0.js +248 -0
  30. package/lib/browser/web-BjVDkiX0.js.map +1 -0
  31. package/lib/{node/XMLHttpRequest-DhupxLHX.mjs → browser/web-C2oKsT3Q.js} +328 -256
  32. package/lib/browser/web-C2oKsT3Q.js.map +1 -0
  33. package/lib/browser/{interceptors/WebSocket/index.d.cts → websocket-CC0nB0md.d.ts} +41 -33
  34. package/lib/node/batch-interceptor-ByEZVih3.js +119 -0
  35. package/lib/node/batch-interceptor-ByEZVih3.js.map +1 -0
  36. package/lib/node/batch-interceptor-CPjFK2td.d.ts +28 -0
  37. package/lib/node/buffer-utils-BvPY1Tc-.js +187 -0
  38. package/lib/node/buffer-utils-BvPY1Tc-.js.map +1 -0
  39. package/lib/node/fetch-utils-Dw1PsmtX.js +568 -0
  40. package/lib/node/fetch-utils-Dw1PsmtX.js.map +1 -0
  41. package/lib/node/has-configurable-global-CJCh_g1I.js +22 -0
  42. package/lib/node/has-configurable-global-CJCh_g1I.js.map +1 -0
  43. package/lib/node/http-CB9rQ7i_.d.ts +105 -0
  44. package/lib/{browser/interceptors/WebSocket/index.d.mts → node/index.d.ts} +120 -60
  45. package/lib/node/{index.mjs → index.js} +8 -11
  46. package/lib/node/index.js.map +1 -0
  47. package/lib/node/interceptor-dtJZ_9QT.d.ts +50 -0
  48. package/lib/node/interceptors/ClientRequest/index.d.ts +14 -0
  49. package/lib/node/interceptors/ClientRequest/index.js +67 -0
  50. package/lib/node/interceptors/ClientRequest/index.js.map +1 -0
  51. package/lib/node/interceptors/XMLHttpRequest/node.d.ts +12 -0
  52. package/lib/node/interceptors/XMLHttpRequest/node.js +74 -0
  53. package/lib/node/interceptors/XMLHttpRequest/node.js.map +1 -0
  54. package/lib/node/interceptors/fetch/node.d.ts +18 -0
  55. package/lib/node/interceptors/fetch/node.js +83 -0
  56. package/lib/node/interceptors/fetch/node.js.map +1 -0
  57. package/lib/node/interceptors/http/index.d.ts +14 -0
  58. package/lib/node/interceptors/http/index.js +32 -0
  59. package/lib/node/interceptors/http/index.js.map +1 -0
  60. package/lib/node/interceptors/net/index.d.ts +203 -0
  61. package/lib/node/interceptors/net/index.js +2 -0
  62. package/lib/node/llhttp/constants.cjs +485 -0
  63. package/lib/node/llhttp/constants.d.cts +957 -0
  64. package/lib/node/llhttp/llhttp.wasm +0 -0
  65. package/lib/node/net-DtMnyEeg.js +1388 -0
  66. package/lib/node/net-DtMnyEeg.js.map +1 -0
  67. package/lib/node/presets/node.d.ts +12 -0
  68. package/lib/node/presets/node.js +17 -0
  69. package/lib/node/presets/node.js.map +1 -0
  70. package/lib/node/{RemoteHttpInterceptor.d.cts → remote-http-interceptor.d.ts} +12 -11
  71. package/lib/node/{RemoteHttpInterceptor.mjs → remote-http-interceptor.js} +38 -33
  72. package/lib/node/remote-http-interceptor.js.map +1 -0
  73. package/lib/node/source-lP0yyEtA.js +1990 -0
  74. package/lib/node/source-lP0yyEtA.js.map +1 -0
  75. package/package.json +79 -118
  76. package/src/batch-interceptor.test.ts +475 -0
  77. package/src/batch-interceptor.ts +244 -0
  78. package/src/{createRequestId.test.ts → create-request-id.test.ts} +1 -2
  79. package/src/disposable.ts +13 -0
  80. package/src/events/http.ts +92 -0
  81. package/src/events/websocket.ts +47 -0
  82. package/src/glossary.ts +1 -27
  83. package/src/index.ts +18 -10
  84. package/src/interceptor.test.ts +279 -0
  85. package/src/interceptor.ts +139 -0
  86. package/src/interceptors/ClientRequest/index.test.ts +21 -39
  87. package/src/interceptors/ClientRequest/index.ts +65 -180
  88. package/src/interceptors/ClientRequest/utils/{recordRawHeaders.test.ts → record-raw-headers.test.ts} +1 -2
  89. package/src/interceptors/ClientRequest/utils/{recordRawHeaders.ts → record-raw-headers.ts} +29 -2
  90. package/src/interceptors/WebSocket/index.ts +34 -51
  91. package/src/interceptors/WebSocket/utils/{bindEvent.test.ts → bind-event.test.ts} +1 -2
  92. package/src/interceptors/WebSocket/utils/events.test.ts +0 -1
  93. package/src/interceptors/WebSocket/{WebSocketClassTransport.ts → web-socket-class-transport.ts} +3 -3
  94. package/src/interceptors/WebSocket/{WebSocketClientConnection.ts → web-socket-client-connection.ts} +4 -4
  95. package/src/interceptors/WebSocket/{WebSocketOverride.ts → web-socket-override.ts} +6 -7
  96. package/src/interceptors/WebSocket/{WebSocketServerConnection.ts → web-socket-server-connection.ts} +4 -4
  97. package/src/interceptors/WebSocket/{WebSocketTransport.ts → web-socket-transport.ts} +1 -1
  98. package/src/interceptors/XMLHttpRequest/node.ts +104 -0
  99. package/src/interceptors/XMLHttpRequest/polyfills/{ProgressEventPolyfill.ts → progress-event-polyfill.ts} +1 -1
  100. package/src/interceptors/XMLHttpRequest/utils/{concateArrayBuffer.test.ts → concat-array-buffer.test.ts} +1 -2
  101. package/src/interceptors/XMLHttpRequest/utils/{concatArrayBuffer.ts → concat-array-buffer.ts} +1 -1
  102. package/src/interceptors/XMLHttpRequest/utils/{createEvent.test.ts → create-event.test.ts} +3 -4
  103. package/src/interceptors/XMLHttpRequest/utils/{createEvent.ts → create-event.ts} +2 -2
  104. package/src/interceptors/XMLHttpRequest/utils/{createResponse.ts → create-response.ts} +1 -1
  105. package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.test.ts → get-body-byte-length.test.ts} +1 -2
  106. package/src/interceptors/XMLHttpRequest/web.ts +34 -0
  107. package/src/interceptors/XMLHttpRequest/{XMLHttpRequestController.ts → xml-http-request-controller.ts} +363 -204
  108. package/src/interceptors/XMLHttpRequest/{XMLHttpRequestProxy.ts → xml-http-request-proxy.ts} +28 -25
  109. package/src/interceptors/fetch/node.ts +103 -0
  110. package/src/interceptors/fetch/utils/{followRedirect.ts → follow-redirect.ts} +1 -1
  111. package/src/interceptors/fetch/web.ts +217 -0
  112. package/src/interceptors/http/forward-events.test.ts +86 -0
  113. package/src/interceptors/http/forward-events.ts +139 -0
  114. package/src/interceptors/http/http-parser/index.ts +310 -0
  115. package/src/interceptors/http/http-parser/llhttp/constants.cjs +485 -0
  116. package/src/interceptors/http/http-parser/llhttp/constants.d.cts +957 -0
  117. package/src/interceptors/http/http-parser/llhttp/llhttp.wasm +0 -0
  118. package/src/interceptors/http/http-parser.ts +138 -0
  119. package/src/interceptors/http/index.ts +34 -0
  120. package/src/interceptors/http/source.ts +688 -0
  121. package/src/interceptors/net/index.ts +368 -0
  122. package/src/interceptors/net/socket-controller.ts +1476 -0
  123. package/src/interceptors/net/utils/address-info.ts +54 -0
  124. package/src/interceptors/net/utils/connection-options-to-url.ts +56 -0
  125. package/src/interceptors/net/utils/flush-writes.ts +57 -0
  126. package/src/interceptors/net/utils/get-tls-connect-options.ts +23 -0
  127. package/src/interceptors/net/utils/normalize-net-connect-args.test.ts +233 -0
  128. package/src/interceptors/net/utils/normalize-net-connect-args.ts +121 -0
  129. package/src/interceptors/net/utils/normalize-tls-connect-args.ts +50 -0
  130. package/src/presets/browser.ts +4 -4
  131. package/src/presets/node.ts +4 -4
  132. package/src/{RemoteHttpInterceptor.ts → remote-http-interceptor.ts} +55 -42
  133. package/src/request-context.ts +40 -0
  134. package/src/{RequestController.test.ts → request-controller.test.ts} +37 -3
  135. package/src/{RequestController.ts → request-controller.ts} +33 -11
  136. package/src/utils/{bufferUtils.test.ts → buffer-utils.test.ts} +1 -2
  137. package/src/utils/buffer-utils.ts +37 -0
  138. package/src/utils/{cloneObject.test.ts → clone-object.test.ts} +1 -2
  139. package/src/utils/{cloneObject.ts → clone-object.ts} +8 -8
  140. package/src/utils/{createProxy.test.ts → create-proxy.test.ts} +6 -4
  141. package/src/utils/{createProxy.ts → create-proxy.ts} +1 -1
  142. package/src/utils/{fetchUtils.test.ts → fetch-utils.test.ts} +1 -1
  143. package/src/utils/{fetchUtils.ts → fetch-utils.ts} +28 -3
  144. package/src/utils/{findPropertySource.test.ts → find-property-source.test.ts} +1 -2
  145. package/src/utils/{getCleanUrl.test.ts → get-clean-url.test.ts} +1 -2
  146. package/src/utils/{getUrlByRequestOptions.test.ts → get-url-by-request-options.test.ts} +4 -8
  147. package/src/utils/{getUrlByRequestOptions.ts → get-url-by-request-options.ts} +16 -16
  148. package/src/utils/{getValueBySymbol.test.ts → get-value-by-symbol.test.ts} +1 -2
  149. package/src/utils/{handleRequest.ts → handle-request.ts} +66 -47
  150. package/src/utils/{hasConfigurableGlobal.test.ts → has-configurable-global.test.ts} +1 -2
  151. package/src/utils/{hasConfigurableGlobal.ts → has-configurable-global.ts} +2 -4
  152. package/src/utils/{isObject.test.ts → is-object.test.ts} +1 -2
  153. package/src/utils/logger.test.ts +154 -0
  154. package/src/utils/logger.ts +154 -0
  155. package/src/utils/{parseJson.test.ts → parse-json.test.ts} +1 -2
  156. package/src/utils/{patchesRegistry.test.ts → patches-registry.test.ts} +1 -1
  157. package/src/utils/{patchesRegistry.ts → patches-registry.ts} +1 -1
  158. package/src/utils/{responseUtils.ts → response-utils.ts} +25 -2
  159. package/ClientRequest/package.json +0 -11
  160. package/RemoteHttpInterceptor/package.json +0 -11
  161. package/WebSocket/package.json +0 -12
  162. package/XMLHttpRequest/package.json +0 -12
  163. package/fetch/package.json +0 -12
  164. package/lib/browser/Interceptor-Deczogc8.d.cts +0 -65
  165. package/lib/browser/Interceptor-gqKgs-aF.d.mts +0 -65
  166. package/lib/browser/XMLHttpRequest-B11I9TBx.cjs +0 -751
  167. package/lib/browser/XMLHttpRequest-B11I9TBx.cjs.map +0 -1
  168. package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs +0 -746
  169. package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs.map +0 -1
  170. package/lib/browser/bufferUtils-BiiO6HZv.mjs.map +0 -1
  171. package/lib/browser/bufferUtils-Uc0eRItL.cjs +0 -38
  172. package/lib/browser/bufferUtils-Uc0eRItL.cjs.map +0 -1
  173. package/lib/browser/createRequestId-DOf8Ktjs.cjs +0 -205
  174. package/lib/browser/createRequestId-DOf8Ktjs.cjs.map +0 -1
  175. package/lib/browser/createRequestId-DYCsFHOi.mjs +0 -170
  176. package/lib/browser/createRequestId-DYCsFHOi.mjs.map +0 -1
  177. package/lib/browser/fetch-BJwfdses.mjs +0 -236
  178. package/lib/browser/fetch-BJwfdses.mjs.map +0 -1
  179. package/lib/browser/fetch-M8iXkZ3L.cjs +0 -241
  180. package/lib/browser/fetch-M8iXkZ3L.cjs.map +0 -1
  181. package/lib/browser/getRawRequest-B1BqgWG6.mjs.map +0 -1
  182. package/lib/browser/getRawRequest-DdfaiPVH.cjs +0 -376
  183. package/lib/browser/getRawRequest-DdfaiPVH.cjs.map +0 -1
  184. package/lib/browser/glossary-1lhZx6Aw.d.cts +0 -69
  185. package/lib/browser/glossary-nkHWcwvY.d.mts +0 -69
  186. package/lib/browser/handleRequest-1Yqu_ho_.mjs +0 -178
  187. package/lib/browser/handleRequest-1Yqu_ho_.mjs.map +0 -1
  188. package/lib/browser/handleRequest-Cz4_wmQ9.cjs +0 -189
  189. package/lib/browser/handleRequest-Cz4_wmQ9.cjs.map +0 -1
  190. package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs +0 -109
  191. package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs.map +0 -1
  192. package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs +0 -126
  193. package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs.map +0 -1
  194. package/lib/browser/index.cjs +0 -70
  195. package/lib/browser/index.cjs.map +0 -1
  196. package/lib/browser/index.d.mts +0 -125
  197. package/lib/browser/index.mjs +0 -56
  198. package/lib/browser/index.mjs.map +0 -1
  199. package/lib/browser/interceptors/WebSocket/index.cjs +0 -618
  200. package/lib/browser/interceptors/WebSocket/index.cjs.map +0 -1
  201. package/lib/browser/interceptors/WebSocket/index.mjs.map +0 -1
  202. package/lib/browser/interceptors/XMLHttpRequest/index.cjs +0 -8
  203. package/lib/browser/interceptors/XMLHttpRequest/index.d.cts +0 -15
  204. package/lib/browser/interceptors/XMLHttpRequest/index.d.mts +0 -15
  205. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +0 -8
  206. package/lib/browser/interceptors/fetch/index.cjs +0 -7
  207. package/lib/browser/interceptors/fetch/index.d.cts +0 -13
  208. package/lib/browser/interceptors/fetch/index.d.mts +0 -13
  209. package/lib/browser/interceptors/fetch/index.mjs +0 -7
  210. package/lib/browser/presets/browser.cjs +0 -18
  211. package/lib/browser/presets/browser.cjs.map +0 -1
  212. package/lib/browser/presets/browser.d.cts +0 -12
  213. package/lib/browser/presets/browser.d.mts +0 -14
  214. package/lib/browser/presets/browser.mjs +0 -18
  215. package/lib/browser/presets/browser.mjs.map +0 -1
  216. package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs +0 -31
  217. package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs.map +0 -1
  218. package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs.map +0 -1
  219. package/lib/node/BatchInterceptor-DdMNfSUY.mjs +0 -44
  220. package/lib/node/BatchInterceptor-DdMNfSUY.mjs.map +0 -1
  221. package/lib/node/BatchInterceptor-KKF8Q47I.d.cts +0 -26
  222. package/lib/node/BatchInterceptor-Z1lwK23r.cjs +0 -49
  223. package/lib/node/BatchInterceptor-Z1lwK23r.cjs.map +0 -1
  224. package/lib/node/BatchInterceptor-rSr-NH_H.d.mts +0 -26
  225. package/lib/node/ClientRequest-DFs7FPNi.cjs +0 -1058
  226. package/lib/node/ClientRequest-DFs7FPNi.cjs.map +0 -1
  227. package/lib/node/ClientRequest-dRpI9v1o.mjs +0 -1049
  228. package/lib/node/ClientRequest-dRpI9v1o.mjs.map +0 -1
  229. package/lib/node/Interceptor-BsZ21ue0.d.cts +0 -132
  230. package/lib/node/Interceptor-soD-WpQO.d.mts +0 -132
  231. package/lib/node/RemoteHttpInterceptor.cjs +0 -155
  232. package/lib/node/RemoteHttpInterceptor.cjs.map +0 -1
  233. package/lib/node/RemoteHttpInterceptor.d.mts +0 -39
  234. package/lib/node/RemoteHttpInterceptor.mjs.map +0 -1
  235. package/lib/node/XMLHttpRequest-DhupxLHX.mjs.map +0 -1
  236. package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs +0 -752
  237. package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs.map +0 -1
  238. package/lib/node/bufferUtils-DxPxwff_.mjs +0 -20
  239. package/lib/node/bufferUtils-DxPxwff_.mjs.map +0 -1
  240. package/lib/node/bufferUtils-S5_-2eN4.cjs +0 -38
  241. package/lib/node/bufferUtils-S5_-2eN4.cjs.map +0 -1
  242. package/lib/node/chunk-CbDLau6x.cjs +0 -34
  243. package/lib/node/fetch-BqMcbuYR.mjs +0 -252
  244. package/lib/node/fetch-BqMcbuYR.mjs.map +0 -1
  245. package/lib/node/fetch-aQJu1Gf_.cjs +0 -259
  246. package/lib/node/fetch-aQJu1Gf_.cjs.map +0 -1
  247. package/lib/node/fetchUtils-BKJ1XmiO.mjs +0 -480
  248. package/lib/node/fetchUtils-BKJ1XmiO.mjs.map +0 -1
  249. package/lib/node/fetchUtils-umV5xXBy.cjs +0 -546
  250. package/lib/node/fetchUtils-umV5xXBy.cjs.map +0 -1
  251. package/lib/node/getRawRequest-B6znU6il.cjs +0 -36
  252. package/lib/node/getRawRequest-B6znU6il.cjs.map +0 -1
  253. package/lib/node/getRawRequest-C2-1urzA.mjs +0 -24
  254. package/lib/node/getRawRequest-C2-1urzA.mjs.map +0 -1
  255. package/lib/node/handleRequest-DCLzePtS.mjs +0 -190
  256. package/lib/node/handleRequest-DCLzePtS.mjs.map +0 -1
  257. package/lib/node/handleRequest-DVOthWJo.cjs +0 -220
  258. package/lib/node/handleRequest-DVOthWJo.cjs.map +0 -1
  259. package/lib/node/hasConfigurableGlobal-BiTmog1u.mjs.map +0 -1
  260. package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs +0 -108
  261. package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs.map +0 -1
  262. package/lib/node/index-BJ5E5aW-.d.cts +0 -113
  263. package/lib/node/index-DfoF45ih.d.mts +0 -113
  264. package/lib/node/index.cjs +0 -53
  265. package/lib/node/index.cjs.map +0 -1
  266. package/lib/node/index.d.cts +0 -104
  267. package/lib/node/index.d.mts +0 -104
  268. package/lib/node/index.mjs.map +0 -1
  269. package/lib/node/interceptors/ClientRequest/index.cjs +0 -6
  270. package/lib/node/interceptors/ClientRequest/index.d.cts +0 -2
  271. package/lib/node/interceptors/ClientRequest/index.d.mts +0 -3
  272. package/lib/node/interceptors/ClientRequest/index.mjs +0 -6
  273. package/lib/node/interceptors/XMLHttpRequest/index.cjs +0 -7
  274. package/lib/node/interceptors/XMLHttpRequest/index.d.cts +0 -14
  275. package/lib/node/interceptors/XMLHttpRequest/index.d.mts +0 -14
  276. package/lib/node/interceptors/XMLHttpRequest/index.mjs +0 -7
  277. package/lib/node/interceptors/fetch/index.cjs +0 -6
  278. package/lib/node/interceptors/fetch/index.d.cts +0 -12
  279. package/lib/node/interceptors/fetch/index.d.mts +0 -12
  280. package/lib/node/interceptors/fetch/index.mjs +0 -6
  281. package/lib/node/node-DIKcnzhK.cjs +0 -39
  282. package/lib/node/node-DIKcnzhK.cjs.map +0 -1
  283. package/lib/node/node-lsdNwZEW.mjs +0 -27
  284. package/lib/node/node-lsdNwZEW.mjs.map +0 -1
  285. package/lib/node/presets/node.cjs +0 -23
  286. package/lib/node/presets/node.cjs.map +0 -1
  287. package/lib/node/presets/node.d.cts +0 -13
  288. package/lib/node/presets/node.d.mts +0 -15
  289. package/lib/node/presets/node.mjs +0 -23
  290. package/lib/node/presets/node.mjs.map +0 -1
  291. package/lib/node/utils/node/index.cjs +0 -4
  292. package/lib/node/utils/node/index.d.cts +0 -16
  293. package/lib/node/utils/node/index.d.mts +0 -16
  294. package/lib/node/utils/node/index.mjs +0 -3
  295. package/presets/browser/package.json +0 -5
  296. package/presets/node/package.json +0 -11
  297. package/src/BatchInterceptor.test.ts +0 -255
  298. package/src/BatchInterceptor.ts +0 -95
  299. package/src/Interceptor.test.ts +0 -205
  300. package/src/Interceptor.ts +0 -249
  301. package/src/getRawRequest.ts +0 -21
  302. package/src/interceptors/ClientRequest/MockHttpSocket.ts +0 -724
  303. package/src/interceptors/ClientRequest/agents.ts +0 -110
  304. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +0 -54
  305. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +0 -45
  306. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +0 -427
  307. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +0 -268
  308. package/src/interceptors/ClientRequest/utils/parserUtils.ts +0 -48
  309. package/src/interceptors/Socket/MockSocket.test.ts +0 -264
  310. package/src/interceptors/Socket/MockSocket.ts +0 -58
  311. package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +0 -26
  312. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +0 -52
  313. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +0 -33
  314. package/src/interceptors/XMLHttpRequest/index.ts +0 -40
  315. package/src/interceptors/fetch/index.ts +0 -203
  316. package/src/utils/bufferUtils.ts +0 -22
  317. package/src/utils/canParseUrl.ts +0 -13
  318. package/src/utils/emitAsync.ts +0 -25
  319. package/src/utils/nextTick.ts +0 -11
  320. package/src/utils/node/index.ts +0 -39
  321. /package/src/{createRequestId.ts → create-request-id.ts} +0 -0
  322. /package/src/{InterceptorError.ts → interceptor-error.ts} +0 -0
  323. /package/src/interceptors/WebSocket/utils/{bindEvent.ts → bind-event.ts} +0 -0
  324. /package/src/interceptors/XMLHttpRequest/polyfills/{EventPolyfill.ts → event-polyfill.ts} +0 -0
  325. /package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.ts → get-body-byte-length.ts} +0 -0
  326. /package/src/interceptors/XMLHttpRequest/utils/{isDomParserSupportedType.ts → is-dom-parser-supported-type.ts} +0 -0
  327. /package/src/interceptors/fetch/utils/{createNetworkError.ts → create-network-error.ts} +0 -0
  328. /package/src/utils/{findPropertySource.ts → find-property-source.ts} +0 -0
  329. /package/src/utils/{getCleanUrl.ts → get-clean-url.ts} +0 -0
  330. /package/src/utils/{getValueBySymbol.ts → get-value-by-symbol.ts} +0 -0
  331. /package/src/utils/{isNodeLikeError.ts → is-node-like-error.ts} +0 -0
  332. /package/src/utils/{isObject.ts → is-object.ts} +0 -0
  333. /package/src/utils/{isPropertyAccessible.ts → is-property-accessible.ts} +0 -0
  334. /package/src/utils/{parseJson.ts → parse-json.ts} +0 -0
  335. /package/src/utils/{resolveWebSocketUrl.ts → resolve-web-socket-url.ts} +0 -0
@@ -0,0 +1,1476 @@
1
+ import net from 'node:net'
2
+ import tls from 'node:tls'
3
+ import { invariant } from 'outvariant'
4
+ import { toBuffer } from '../../utils/buffer-utils'
5
+ import { createLogger } from '../../utils/logger'
6
+ import { unwrapPendingData, writePendingData } from './utils/flush-writes'
7
+ import { NetworkConnectionOptions } from './utils/normalize-net-connect-args'
8
+ import { TlsConnectionOptions } from './utils/normalize-tls-connect-args'
9
+ import { getTlsConnectOptions } from './utils/get-tls-connect-options'
10
+ import {
11
+ getAddressInfoByConnectionOptions,
12
+ getLocalAddressInfoByConnectionOptions,
13
+ } from './utils/address-info'
14
+
15
+ const kListenerWrap = Symbol('kListenerWrap')
16
+ export const kRawSocket = Symbol('kRawSocket')
17
+ export const kPatched = Symbol('kPatched')
18
+
19
+ const logger = createLogger('socket')
20
+
21
+ // Internally, Node.js represents the result of various operations
22
+ // by the number they return: 0 (error), 1 (success).
23
+ type OperationStatus = 0 | 1
24
+
25
+ declare module 'node:net' {
26
+ interface Socket {
27
+ [kPatched]?: boolean
28
+ _httpMessage?: object | null
29
+ _pendingData:
30
+ | string
31
+ | Buffer
32
+ | Array<{
33
+ chunk: string | Buffer
34
+ encoding?: BufferEncoding
35
+ callback?: (error?: Error | null) => void
36
+ }>
37
+ | null
38
+ _pendingEncoding: BufferEncoding | ''
39
+ _bytesDispatched: number
40
+ _writeGeneric(
41
+ writev: boolean,
42
+ data: NonNullable<net.Socket['_pendingData']>,
43
+ encoding: BufferEncoding,
44
+ callback?: (error?: Error | null) => void
45
+ ): void
46
+ _handle: TcpHandle
47
+ _start: () => void
48
+ _unrefTimer: () => void
49
+ }
50
+ }
51
+
52
+ declare module 'node:tls' {
53
+ interface TLSSocket {
54
+ _handle: TcpHandle & {
55
+ start: () => void
56
+ onhandshakedone: () => void
57
+ onnewsession: (sessionId: unknown, session: Buffer) => void
58
+ getSession: () => Buffer
59
+ isSessionReused: () => boolean
60
+ getServername: () => string
61
+ getALPNNegotiatedProtocol: () => string | false
62
+ getCipher: () => { name: string; standardName: string; version: string }
63
+ getEphemeralKeyInfo: () => tls.EphemeralKeyInfo
64
+ verifyError: () => void
65
+ }
66
+ }
67
+ }
68
+
69
+ export interface TcpHandle {
70
+ open: (fd: unknown) => OperationStatus
71
+ connect: (request: TcpWrap, address: string, port: number) => void
72
+ connect6: (request: TcpWrap, address: string, port: number) => void
73
+ listen: (backlog: number) => OperationStatus
74
+ onconnection?: () => void
75
+ getpeername?: (
76
+ addressInfo: ReturnType<net.Socket['address']>
77
+ ) => OperationStatus
78
+ getsockname?: (
79
+ addressInfo: ReturnType<net.Socket['address']>
80
+ ) => OperationStatus
81
+ reading: boolean
82
+ onread: () => void
83
+ readStart: () => void
84
+ readStop: () => void
85
+ bytesRead: number
86
+ bytesWritten: number
87
+ ref?: () => void
88
+ unref?: () => void
89
+ hasRef?: () => boolean
90
+ fchmod: (mode: number) => void
91
+ setBlocking: (blocking: boolean) => OperationStatus
92
+ setNoDelay?: (noDelay: boolean) => void
93
+ setKeepAlive?: (keepAlive: boolean, initialDelay: number) => void
94
+ setTypeOfService?: (tos: number) => OperationStatus
95
+ shutdown: (reqest: unknown /* ShutdownWrap */) => OperationStatus
96
+ close: () => void
97
+
98
+ _parent?: TcpHandle
99
+ }
100
+
101
+ export interface TcpWrap {
102
+ oncomplete: (
103
+ status: OperationStatus,
104
+ owner: TcpHandle,
105
+ request: TcpWrap,
106
+ readable?: boolean,
107
+ writable?: boolean
108
+ ) => void
109
+ }
110
+
111
+ /**
112
+ * Create a proxy `net.Socket` instance that represents the intercepted socket server-side.
113
+ * This is the reference exposed as `socket` in the connection listener. This proxy allows
114
+ * the user to interact with `socket` from the server's perspective (e.g. `socket.write()`
115
+ * on the server translates to the `socket.push()` on the client).
116
+ */
117
+ interface PendingServerWrite {
118
+ chunk: string | Uint8Array
119
+ encoding?: BufferEncoding
120
+ callback?: (error?: Error) => void
121
+ }
122
+
123
+ interface PendingServerEnd {
124
+ chunk?: string | Uint8Array
125
+ encoding?: BufferEncoding
126
+ callback?: () => void
127
+ }
128
+
129
+ function toServerSocket<T extends net.Socket>(socket: T): T {
130
+ /**
131
+ * The server-side write buffer. Pushing data to the client honors
132
+ * the client's read backpressure: once "socket.push()" reports a
133
+ * full buffer, subsequent server writes queue here and flush when
134
+ * the client reads again ("_read"). This mirrors how a real server
135
+ * cannot write faster than the client consumes.
136
+ */
137
+ const pendingWrites: Array<PendingServerWrite> = []
138
+ let pendingEnd: PendingServerEnd | undefined
139
+ let isBackpressured = false
140
+ let isFlushScheduled = false
141
+
142
+ const flushPendingWrites = (): boolean => {
143
+ /**
144
+ * @note The mocked connection is not established yet. Flush once
145
+ * it is, the same way a real server cannot write to a client
146
+ * that has not connected.
147
+ */
148
+ if (socket.connecting) {
149
+ isBackpressured = true
150
+
151
+ if (!isFlushScheduled) {
152
+ isFlushScheduled = true
153
+ socket.once('ready', () => {
154
+ isFlushScheduled = false
155
+ flushPendingWrites()
156
+ })
157
+ }
158
+
159
+ return false
160
+ }
161
+
162
+ while (pendingWrites.length > 0) {
163
+ const nextWrite = pendingWrites.shift()!
164
+
165
+ // Receiving mocked data is socket activity: refresh the client's
166
+ // idle timer the same way its own reads would.
167
+ socket._unrefTimer()
168
+
169
+ const canPushMore = socket.push(
170
+ toBuffer(nextWrite.chunk, nextWrite.encoding),
171
+ nextWrite.encoding
172
+ )
173
+ nextWrite.callback?.()
174
+
175
+ if (!canPushMore) {
176
+ isBackpressured = true
177
+ return false
178
+ }
179
+ }
180
+
181
+ const wasBackpressured = isBackpressured
182
+ isBackpressured = false
183
+
184
+ if (pendingEnd) {
185
+ const finalEnd = pendingEnd
186
+ pendingEnd = undefined
187
+
188
+ // Deliver the final chunk passed to "end(chunk)", if any.
189
+ if (finalEnd.chunk != null) {
190
+ socket.push(toBuffer(finalEnd.chunk, finalEnd.encoding), finalEnd.encoding)
191
+ }
192
+
193
+ socket.push(null)
194
+ finalEnd.callback?.()
195
+ }
196
+
197
+ if (wasBackpressured) {
198
+ socket.emit('internal:drain')
199
+ }
200
+
201
+ return true
202
+ }
203
+
204
+ /**
205
+ * @note "_read" is the client asking for more data. Flush the
206
+ * buffered server writes so the delivery resumes as the client
207
+ * reads, completing the backpressure loop.
208
+ */
209
+ const realRead = socket._read.bind(socket)
210
+ socket._read = (size: number) => {
211
+ realRead(size)
212
+
213
+ if (pendingWrites.length > 0 || pendingEnd != null || isBackpressured) {
214
+ flushPendingWrites()
215
+ }
216
+ }
217
+
218
+ return new Proxy(socket, {
219
+ get: (target, property, receiver) => {
220
+ const getRealValue = () => {
221
+ return Reflect.get(target, property, receiver)
222
+ }
223
+
224
+ if (
225
+ property === 'on' ||
226
+ property === 'addListener' ||
227
+ property === 'once' ||
228
+ property === 'prependListener' ||
229
+ property === 'prependOnceListener'
230
+ ) {
231
+ const realAddListener = getRealValue() as net.Socket['addListener']
232
+
233
+ return (event: any, listener: (...args: Array<unknown>) => void) => {
234
+ if (event === 'data') {
235
+ const listenerWrap = (chunk: any, encoding?: BufferEncoding) => {
236
+ listener(toBuffer(chunk, encoding))
237
+ }
238
+
239
+ Object.defineProperty(listener, kListenerWrap, {
240
+ enumerable: false,
241
+ writable: false,
242
+ value: listenerWrap,
243
+ })
244
+
245
+ /**
246
+ * @note Subscribe using the same method (e.g. "once") so its
247
+ * listener semantics apply to the internal channel too.
248
+ */
249
+ Reflect.apply(realAddListener, target, [
250
+ 'internal:write',
251
+ listenerWrap,
252
+ ])
253
+
254
+ return target
255
+ }
256
+
257
+ /**
258
+ * @note The "drain" event on the server socket signals the
259
+ * flush of the server-side write buffer. It is routed through
260
+ * an internal channel so it does not clash with the "drain"
261
+ * event of the underlying client socket.
262
+ */
263
+ if (event === 'drain') {
264
+ Reflect.apply(realAddListener, target, ['internal:drain', listener])
265
+ return target
266
+ }
267
+
268
+ return realAddListener.call(target, event, listener)
269
+ }
270
+ }
271
+
272
+ if (property === 'off' || property === 'removeListener') {
273
+ const realRemoveListener =
274
+ getRealValue() as net.Socket['removeListener']
275
+
276
+ return (event: string, listener: any) => {
277
+ if (event === 'data') {
278
+ const listenerWrap = listener[kListenerWrap]
279
+
280
+ if (listenerWrap) {
281
+ // The wrap is subscribed to the internal channel,
282
+ // not the "data" event (see the listener proxy above).
283
+ return realRemoveListener.call(
284
+ target,
285
+ 'internal:write',
286
+ listenerWrap
287
+ )
288
+ }
289
+ }
290
+
291
+ if (event === 'drain') {
292
+ return realRemoveListener.call(target, 'internal:drain', listener)
293
+ }
294
+
295
+ return realRemoveListener.call(target, event, listener)
296
+ }
297
+ }
298
+
299
+ // Push data to the client socket when server "socket.write()" is called.
300
+ if (property === 'write') {
301
+ return ((chunk, encoding, callback) => {
302
+ if (typeof encoding === 'function') {
303
+ callback = encoding
304
+ encoding = undefined
305
+ }
306
+
307
+ pendingWrites.push({ chunk, encoding, callback })
308
+
309
+ /**
310
+ * @note Do not push more data to a client that is not
311
+ * reading. The write stays buffered until the client reads
312
+ * again, and "drain" signals the flush.
313
+ */
314
+ if (isBackpressured) {
315
+ return false
316
+ }
317
+
318
+ return flushPendingWrites()
319
+ }) as net.Socket['write']
320
+ }
321
+
322
+ // Translate server-side "socket.end()" to client-sode "socket.push(null)".
323
+ if (property === 'end') {
324
+ return ((...args: Parameters<net.Socket['end']>): net.Socket => {
325
+ const callback = args[args.length - 1]
326
+ const chunk = typeof args[0] === 'function' ? undefined : args[0]
327
+ const encoding = typeof args[1] === 'string' ? args[1] : undefined
328
+
329
+ /**
330
+ * @note The end-of-stream is delivered once the buffered
331
+ * writes flush so the client never observes the EOF before
332
+ * the data that preceded it.
333
+ */
334
+ pendingEnd = {
335
+ chunk,
336
+ encoding,
337
+ callback: typeof callback === 'function' ? callback : undefined,
338
+ }
339
+ flushPendingWrites()
340
+
341
+ /**
342
+ * @note Do not end the client socket's writable side.
343
+ * The server ending the connection only signals EOF to the
344
+ * client (the client may keep writing on half-open sockets).
345
+ */
346
+ return target
347
+ }) as net.Socket['end']
348
+ }
349
+
350
+ return getRealValue()
351
+ },
352
+ })
353
+ }
354
+
355
+ export abstract class SocketController {
356
+ static PENDING = 0 as const
357
+ static CLAIMED = 1 as const
358
+ static PASSTHROUGH = 2 as const
359
+
360
+ public readyState:
361
+ | typeof SocketController.PENDING
362
+ | typeof SocketController.CLAIMED
363
+ | typeof SocketController.PASSTHROUGH
364
+
365
+ private [kRawSocket]: net.Socket
366
+
367
+ constructor(socket: net.Socket) {
368
+ this[kRawSocket] = socket
369
+ // Mark this socket as patched so socket-related patches
370
+ // (e.g. the "destroyed" getter) can tell it apart from
371
+ // the sockets created before the interception was applied.
372
+ socket[kPatched] = true
373
+ this.readyState = SocketController.PENDING
374
+ }
375
+
376
+ /**
377
+ * Claim this socket. Once claimed, the connection attempt succeeds
378
+ * regardless of the requested host and the interceptor becomes the
379
+ * mocked server for this connection.
380
+ */
381
+ public claim(): void {
382
+ invariant(
383
+ this.readyState === SocketController.PENDING,
384
+ 'Failed to claim a socket connection: already handled (%s)',
385
+ this.readyState
386
+ )
387
+
388
+ this.readyState = SocketController.CLAIMED
389
+ }
390
+
391
+ /**
392
+ * Establish this socket connection as-is.
393
+ */
394
+ public passthrough(): void {
395
+ invariant(
396
+ this.readyState === SocketController.PENDING,
397
+ 'Failed to passthrough a socket connection: already handled (%s)',
398
+ this.readyState
399
+ )
400
+
401
+ this.readyState = SocketController.PASSTHROUGH
402
+ }
403
+ }
404
+
405
+ export type FlushPendingDataFunction = (
406
+ data: NonNullable<net.Socket['_pendingData']>,
407
+ encoding: BufferEncoding | undefined,
408
+ callback: (data: NonNullable<net.Socket['_pendingData']>) => void
409
+ ) => void
410
+
411
+ type CorkedReadEvent =
412
+ | { type: 'data'; chunk: Buffer }
413
+ | { type: 'end' }
414
+ | { type: 'close'; hadError: boolean }
415
+
416
+ export class TcpSocketController extends SocketController {
417
+ public serverSocket: net.Socket
418
+
419
+ protected pendingConnection: PromiseWithResolvers<[TcpWrap, TcpHandle]>
420
+
421
+ #connectionOptions?: NetworkConnectionOptions
422
+ #realWriteGeneric: net.Socket['_writeGeneric']
423
+ #passthroughSocket: net.Socket | null = null
424
+ #bufferedWrites: Array<Parameters<net.Socket['_writeGeneric']>> = []
425
+ #readsCorked = false
426
+ #corkedReads: Array<CorkedReadEvent> = []
427
+ #realHandleSwapped = false
428
+ #clientCloseEmitted = false
429
+ #clientEndPushed = false
430
+ #connectEmulated = false
431
+
432
+ constructor(
433
+ protected readonly socket: net.Socket,
434
+ protected readonly createConnection: () => net.Socket,
435
+ connectionOptions?: NetworkConnectionOptions
436
+ ) {
437
+ super(socket)
438
+
439
+ /**
440
+ * @note Plain TCP sockets capture the connection options from the
441
+ * "socket.connect()" proxy below. TLS sockets carry additional
442
+ * TLS-level options that never pass through "socket.connect()",
443
+ * so those are provided explicitly (see "TlsSocketController").
444
+ */
445
+ this.#connectionOptions = connectionOptions
446
+
447
+ // Implement the read method to prevent the "Error: read ENOTCONN" errors on non-existing hosts.
448
+ this.socket._read = () => {
449
+ /**
450
+ * @note Resume the passthrough socket when the consumer asks for
451
+ * more data. Node.js calls "_read()" once the consumer drains the
452
+ * read buffer (e.g. after "resume()"). The passthrough socket may
453
+ * have been paused when the consumer's buffer got full, and this
454
+ * is the only signal to continue reading.
455
+ */
456
+ this.#passthroughSocket?.resume()
457
+ }
458
+
459
+ // Store the unpatched write method so passthrough writes can
460
+ // delegate to it once the socket receives the real handle.
461
+ this.#realWriteGeneric = this.socket._writeGeneric
462
+ this.#bufferedWrites = []
463
+
464
+ this.socket._writeGeneric = (...args) => {
465
+ this.#handleWrite(args)
466
+ }
467
+
468
+ this.socket.connect = new Proxy(this.socket.connect, {
469
+ apply: (target, thisArg, args) => {
470
+ logger.verbose('socket.connect() %o', args)
471
+
472
+ this.#connectionOptions = args[0]
473
+
474
+ /**
475
+ * @note Do not bind the intercepted socket to the requested
476
+ * local address/port. Binding reserves the port for real, and
477
+ * the passthrough connection (created with the original options)
478
+ * would then bind the same port again, resulting in a conflict.
479
+ * The requested values are still reflected in the address info
480
+ * of a claimed socket (see "claim()").
481
+ */
482
+ if (
483
+ args[0] != null &&
484
+ typeof args[0] === 'object' &&
485
+ (args[0].localAddress != null || args[0].localPort != null)
486
+ ) {
487
+ args[0] = { ...args[0], localAddress: undefined, localPort: undefined }
488
+ }
489
+
490
+ return Reflect.apply(target, thisArg, args)
491
+ },
492
+ })
493
+
494
+ /**
495
+ * @note A single socket can be reused for connections to the same host.
496
+ * When one connection ends, the Agent frees the socket, then uses it
497
+ * to write the next request's HTTP message immediately. Use the "free"
498
+ * event to transition the controller into the pending state so the
499
+ * next exchange is handled anew.
500
+ */
501
+ socket
502
+ .on('free', () => {
503
+ logger.verbose('client socket freed!')
504
+ this.reset()
505
+ })
506
+ .on('close', () => {
507
+ logger.verbose('client socket closed!')
508
+ this.#clientCloseEmitted = true
509
+
510
+ /**
511
+ * @note Destroy the passthrough socket, if any. The client
512
+ * socket is done, and a passthrough connection left open would
513
+ * linger until the server closes it (or error unhandled if it
514
+ * is still connecting).
515
+ */
516
+ this.#passthroughSocket?.destroy()
517
+ this.#passthroughSocket = null
518
+
519
+ this.#bufferedWrites = []
520
+ this.#readsCorked = false
521
+ this.#corkedReads = []
522
+ })
523
+
524
+ this.serverSocket = toServerSocket(this.socket)
525
+
526
+ this.pendingConnection = Promise.withResolvers()
527
+ this.#reset()
528
+ }
529
+
530
+ /**
531
+ * Reset this controller to the pending state so the next exchange
532
+ * on this socket can be handled anew. This is meant for kept-alive
533
+ * sockets that are reused for multiple exchanges by clients that
534
+ * don't emit the "free" event on the socket (e.g. Undici).
535
+ */
536
+ public reset(): void {
537
+ /**
538
+ * @note Only settled (claimed or passed-through) sockets need a reset.
539
+ * Resetting a pending socket again would discard the writes already
540
+ * buffered for the next exchange (e.g. the Agent "free" event firing
541
+ * after the parser has reset this controller at a message boundary).
542
+ */
543
+ if (this.readyState === SocketController.PENDING) {
544
+ return
545
+ }
546
+
547
+ this.#reset()
548
+ }
549
+
550
+ #reset(): void {
551
+ logger.verbose('resetting the socket...')
552
+
553
+ this.readyState = SocketController.PENDING
554
+ this.pendingConnection = Promise.withResolvers()
555
+ this.#bufferedWrites = []
556
+ this.#connectEmulated = false
557
+
558
+ // Release the pending data of the previous exchange, if any,
559
+ // so kept-alive sockets do not retain every written payload.
560
+ this.socket._pendingData = null
561
+ this.socket._pendingEncoding = ''
562
+
563
+ const wrapHandle = (handle: TcpHandle) => {
564
+ this.pendingConnection.promise.then(() => {
565
+ logger.verbose('connection request resolved!', this.readyState)
566
+
567
+ process.nextTick(() => {
568
+ /**
569
+ * @note If by this point the socket hasn't been handled,
570
+ * is still connecting, doesn't have any writes buffered,
571
+ * and has a "connect" listener, assume it's the "write after connect"
572
+ * scenario (e.g. undici). In that case, auto-claim the socket to
573
+ * transition to the connected state appropriately to its handle.
574
+ */
575
+ if (
576
+ this.readyState === SocketController.PENDING &&
577
+ this.socket.connecting &&
578
+ this.#bufferedWrites.length === 0 &&
579
+ this.socket.listenerCount('connect') > 0
580
+ ) {
581
+ logger.verbose(
582
+ 'assume connect->write socket, calling "connect" listeners...'
583
+ )
584
+ this.emulateConnect()
585
+ }
586
+ })
587
+ })
588
+
589
+ /**
590
+ * Remove the "setTypeOfService" from the handle, if present (Node.js v24+).
591
+ * Removing it has no effect on the socket but prevents the "setTypeOfService EBADF" error.
592
+ * @see https://github.com/nodejs/node/blob/69a970f76814d40f55cf162d0cc3632fe8a7e599/lib/net.js#L661
593
+ * @see https://github.com/nodejs/undici/blob/bf684f7de01616708a33a5d1c092177622394442/lib/dispatcher/client-h1.js#L1136
594
+ */
595
+ if (handle.setTypeOfService) {
596
+ handle.setTypeOfService = undefined
597
+ }
598
+
599
+ handle.connect = handle.connect6 = (request) => {
600
+ logger.verbose('handle.connect()')
601
+ this.pendingConnection.resolve([request, handle])
602
+ }
603
+
604
+ logger.verbose('socket handle wrapped! waiting for connection request...')
605
+ }
606
+
607
+ if (this.socket._handle) {
608
+ wrapHandle(this.socket._handle)
609
+ } else {
610
+ this.socket.prependOnceListener('connectionAttempt', () => {
611
+ wrapHandle(this.socket._handle)
612
+ })
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Handle a write performed on the client socket. Installed once as
618
+ * "_writeGeneric", this is the single write path for every controller
619
+ * state, dispatching on "readyState".
620
+ */
621
+ #handleWrite(args: Parameters<net.Socket['_writeGeneric']>): void {
622
+ const data = args[1]
623
+
624
+ logger.verbose('socket write (state: %d) %o', this.readyState, args)
625
+
626
+ /**
627
+ * @note Buffer the write BEFORE pushing the data to the server socket.
628
+ * Handling the pushed data may transition this controller within the
629
+ * same call stack, and each transition settles the buffered entry:
630
+ * "passthrough()" flushes it to the real socket, "claim()" drops it,
631
+ * and a reset at an HTTP message boundary keeps it for the next exchange.
632
+ */
633
+ this.#bufferedWrites.push(args)
634
+
635
+ if (this.readyState === SocketController.PENDING) {
636
+ /**
637
+ * @note Reflect the buffered write in "_pendingData" so it counts
638
+ * toward "bytesWritten", the same way Node.js counts the pending
639
+ * data of a connecting socket. Flushing the buffered writes resets
640
+ * "_pendingData" (see `passthrough()`).
641
+ */
642
+ const pendingData = Array.isArray(this.socket._pendingData)
643
+ ? this.socket._pendingData
644
+ : []
645
+
646
+ unwrapPendingData(data, (chunk, chunkEncoding) => {
647
+ pendingData.push({ chunk, encoding: chunkEncoding })
648
+ })
649
+
650
+ this.socket._pendingData = pendingData
651
+
652
+ // The server socket will NEVER have any "data" listeners attached
653
+ // on the first write because the "connection" interceptor event
654
+ // emits on the next tick.
655
+ if (this.socket.listenerCount('internal:write') === 0) {
656
+ logger.verbose(
657
+ 'no server data listeners, scheduling to the next tick...'
658
+ )
659
+
660
+ process.nextTick(() => {
661
+ this.#push(data)
662
+ })
663
+ } else {
664
+ this.#push(data)
665
+ }
666
+ } else {
667
+ this.#push(data)
668
+ }
669
+
670
+ /**
671
+ * Dispatch on the state observed AFTER the push since handling the
672
+ * pushed data may have transitioned this controller synchronously.
673
+ */
674
+ switch (this.readyState) {
675
+ case SocketController.PENDING: {
676
+ /**
677
+ * @note The write either awaits the verdict on this exchange or,
678
+ * if the push reset this controller at a message boundary, opens
679
+ * the next exchange (the reset cleared the buffer; re-buffer it).
680
+ */
681
+ if (!this.#bufferedWrites.includes(args)) {
682
+ this.#bufferedWrites.push(args)
683
+ }
684
+
685
+ this.#acknowledgeWrite(args)
686
+ return
687
+ }
688
+
689
+ case SocketController.CLAIMED: {
690
+ /**
691
+ * @note Once claimed, there's nowhere else to write chunks to.
692
+ * The data was delivered to the server socket; complete the write
693
+ * so the socket's writable state settles (enabling "finish").
694
+ */
695
+ this.#removeBufferedWrite(args)
696
+ this.#acknowledgeWrite(args)
697
+ return
698
+ }
699
+
700
+ case SocketController.PASSTHROUGH: {
701
+ /**
702
+ * @note A synchronous "passthrough()" has already flushed this
703
+ * write (with its callback) to the real socket.
704
+ */
705
+ if (!this.#removeBufferedWrite(args)) {
706
+ return
707
+ }
708
+
709
+ /**
710
+ * @note Until the handle swap, the client socket's own handle
711
+ * cannot carry any data (it never actually connects). Write
712
+ * directly to the passthrough socket instead. This also prevents
713
+ * the "connecting" write replay of `Socket.prototype._writeGeneric`
714
+ * from pushing the same data to the server socket twice.
715
+ */
716
+ if (!this.#realHandleSwapped && this.#passthroughSocket) {
717
+ writePendingData(this.#passthroughSocket, data, args[2], args[3])
718
+ return
719
+ }
720
+
721
+ this.#realWriteGeneric.apply(this.socket, args)
722
+ }
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Complete the given write by invoking its callback. The callback is
728
+ * then removed from the write entry so flushing that entry later
729
+ * (e.g. on passthrough) does not invoke it twice.
730
+ */
731
+ #acknowledgeWrite(args: Parameters<net.Socket['_writeGeneric']>): void {
732
+ const callback = args[3]
733
+
734
+ if (typeof callback === 'function') {
735
+ callback()
736
+ args[3] = undefined
737
+ }
738
+ }
739
+
740
+ #removeBufferedWrite(
741
+ args: Parameters<net.Socket['_writeGeneric']>
742
+ ): boolean {
743
+ const index = this.#bufferedWrites.indexOf(args)
744
+
745
+ if (index === -1) {
746
+ return false
747
+ }
748
+
749
+ this.#bufferedWrites.splice(index, 1)
750
+ return true
751
+ }
752
+
753
+ protected emulateConnect() {
754
+ this.#connectEmulated = true
755
+
756
+ /**
757
+ * @note Reflect the connected state before notifying the listeners,
758
+ * the same way Node.js does before emitting "connect".
759
+ */
760
+ Reflect.set(this.socket, 'connecting', false)
761
+
762
+ /**
763
+ * @note Invoke the raw listeners so the "once" wrappers get
764
+ * consumed. This prevents listeners like the connection callback
765
+ * from being invoked again when "connect" is emitted for real
766
+ * (e.g. once the claimed connection completes).
767
+ */
768
+ for (const listener of this.socket.rawListeners('connect')) {
769
+ listener.apply(this.socket)
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Push the given data to the server socket.
775
+ * This has no effect on the public-facing socket and is used
776
+ * only for the interceptors to subscribe to "socket.on('data')"
777
+ * before the data is actually written anywhere.
778
+ */
779
+ #push = (data: net.Socket['_pendingData']) => {
780
+ if (data == null) {
781
+ return
782
+ }
783
+
784
+ logger.verbose('server push %o', data)
785
+
786
+ unwrapPendingData(data, (chunk, encoding) => {
787
+ logger.verbose('server emitting "data" %o', { chunk, encoding })
788
+
789
+ this.socket.emit('internal:write', chunk, encoding)
790
+ })
791
+ }
792
+
793
+ #onRealSocketConnect = () => {
794
+ if (!this.#passthroughSocket) {
795
+ return
796
+ }
797
+
798
+ /**
799
+ * @note The consumer may destroy the socket while the passthrough
800
+ * connection is still being established. The passthrough "connect"
801
+ * (I/O poll phase) can arrive before the client's "close" callback
802
+ * (close phase) within the same event loop iteration. A destroyed
803
+ * socket must not emit "connect"/"ready".
804
+ */
805
+ if (this.socket.destroyed) {
806
+ this.#passthroughSocket.destroy()
807
+ return
808
+ }
809
+
810
+ const replacedHandle = this.socket._handle
811
+ const wasUnrefed =
812
+ replacedHandle != null &&
813
+ typeof replacedHandle.hasRef === 'function' &&
814
+ !replacedHandle.hasRef()
815
+
816
+ this.socket._handle = this.#passthroughSocket._handle
817
+ this.#realHandleSwapped = true
818
+
819
+ /**
820
+ * @note Preserve the ref state across the handle swap. If the
821
+ * consumer unrefed the socket while it was connecting, the swapped
822
+ * handle must not hold the process alive either.
823
+ */
824
+ if (wasUnrefed) {
825
+ this.socket._handle.unref?.()
826
+ }
827
+
828
+ /**
829
+ * @note Close the replaced handle. Nothing references it past this
830
+ * point, and left open, it keeps the process alive indefinitely.
831
+ * For TLS sockets, the replaced handle is a TLSWrap; close its
832
+ * underlying transport too (closing the wrap alone does not close
833
+ * the TCP handle it sits on).
834
+ */
835
+ if (replacedHandle != null) {
836
+ replacedHandle.close()
837
+ replacedHandle._parent?.close()
838
+ }
839
+
840
+ Reflect.set(this.socket, 'connecting', false)
841
+
842
+ /**
843
+ * @note Read the remote address info once so it gets cached on the
844
+ * socket. The passthrough socket controls the swapped handle and may
845
+ * close it at any point (e.g. once the server ends the connection),
846
+ * while Node.js keeps serving the cached info for sockets that
847
+ * have connected. Reading must happen after the socket is no longer
848
+ * connecting (the info of connecting sockets is never cached).
849
+ */
850
+ void this.socket.remoteAddress
851
+
852
+ this.socket.emit('connect')
853
+ this.socket.emit('ready')
854
+ }
855
+
856
+ #onRealSocketConnectionAttemptFailed = (
857
+ address: string,
858
+ port: number,
859
+ family: number,
860
+ error: Error
861
+ ) => {
862
+ this.socket.emit('connectionAttemptFailed', address, port, family, error)
863
+ }
864
+
865
+ #onRealSocketConnectionAttemptTimeout = (
866
+ address: string,
867
+ port: number,
868
+ family: number
869
+ ) => {
870
+ this.socket.emit('connectionAttemptTimeout', address, port, family)
871
+ }
872
+
873
+ #onRealSocketData = (data: Buffer) => {
874
+ logger.verbose('real socket "data" event %o', data)
875
+
876
+ /**
877
+ * @note Receiving data is socket activity. Refresh the idle timer
878
+ * of the client socket the same way its own reads would
879
+ * ("socket.setTimeout()" must not fire during an active transfer).
880
+ * The data arrives on the real socket, which only refreshes the
881
+ * real socket's timer.
882
+ */
883
+ this.socket._unrefTimer()
884
+
885
+ if (this.#readsCorked) {
886
+ logger.verbose('reads are corked, buffering the data...')
887
+ this.#corkedReads.push({ type: 'data', chunk: data })
888
+ return
889
+ }
890
+
891
+ if (!this.socket.push(data)) {
892
+ logger.verbose(
893
+ 'client socket forbade more pushes, pausing the passthrough socket...'
894
+ )
895
+ this.#passthroughSocket?.pause()
896
+ }
897
+ }
898
+
899
+ #onRealSocketError = (error: Error) => {
900
+ logger.verbose('real socket "error" event %o', error)
901
+
902
+ if (this.socket.destroyed) {
903
+ logger.verbose(
904
+ 'real socket errored but client socket already destroyed, skipping...'
905
+ )
906
+ return
907
+ }
908
+
909
+ logger.verbose('real socket errored, forwarding %o', error)
910
+
911
+ this.socket.destroy(error)
912
+
913
+ // The handle swap in passthrough (this.socket._handle = realSocket._handle)
914
+ // breaks Node's internal close machinery—destroy() emits "error" but never
915
+ // emits "close". Consumers like Undici wait for "close" to finalize the
916
+ // request, so we must emit it manually.
917
+ // Before the swap (e.g. a failed connection attempt), the client socket
918
+ // emits "close" on its own, and emitting here would duplicate it.
919
+ if (this.#realHandleSwapped) {
920
+ process.nextTick(() => this.socket.emit('close', true))
921
+ }
922
+ }
923
+
924
+ #onRealSocketEnd = () => {
925
+ // Receiving the end-of-stream is a read, the same as data.
926
+ this.socket._unrefTimer()
927
+
928
+ if (this.#readsCorked) {
929
+ this.#corkedReads.push({ type: 'end' })
930
+ return
931
+ }
932
+
933
+ this.#clientEndPushed = true
934
+ this.socket.push(null)
935
+ }
936
+
937
+ #onRealSocketClose = (hadError: boolean) => {
938
+ /**
939
+ * @note The connection is fully closed and the swapped handle
940
+ * cannot shut down anymore. Report a synchronous shutdown so the
941
+ * automatic "end()" of a half-closed socket ("allowHalfOpen: false")
942
+ * finishes without errors once the consumer reads the end-of-stream.
943
+ */
944
+ if (this.#realHandleSwapped && this.socket._handle) {
945
+ this.socket._handle.shutdown = () => 1
946
+ }
947
+
948
+ if (this.#readsCorked) {
949
+ this.#corkedReads.push({ type: 'close', hadError })
950
+ return
951
+ }
952
+
953
+ // The client socket already emitted "close" (e.g. it was destroyed
954
+ // with an error before the handle swap). Forwarding the real socket
955
+ // "close" would emit it twice.
956
+ if (this.#clientCloseEmitted) {
957
+ return
958
+ }
959
+
960
+ // A destroyed client socket with an intact handle emits "close"
961
+ // through its own machinery. Only forward the real socket "close"
962
+ // when the handle swap suppressed that emission.
963
+ if (this.socket.destroyed && !this.#realHandleSwapped) {
964
+ return
965
+ }
966
+
967
+ this.#emitClientClose(hadError)
968
+ }
969
+
970
+ /**
971
+ * Emit the "close" event on the client socket, honoring the order
972
+ * of the socket teardown events.
973
+ * @note The client may be paused with the received data (and the
974
+ * end-of-stream) still buffered. Node.js never emits "close" before
975
+ * "end" on a gracefully closed connection: the teardown waits until
976
+ * the consumer reads the buffered data.
977
+ */
978
+ #emitClientClose(hadError: boolean): void {
979
+ if (
980
+ this.#clientEndPushed &&
981
+ !this.socket.readableEnded &&
982
+ !this.socket.destroyed
983
+ ) {
984
+ let closeDelivered = false
985
+ const deliverClose = (hadErrorOverride?: boolean) => {
986
+ if (closeDelivered) {
987
+ return
988
+ }
989
+ closeDelivered = true
990
+
991
+ process.nextTick(() => {
992
+ if (!this.#clientCloseEmitted) {
993
+ this.socket.emit('close', hadErrorOverride ?? hadError)
994
+ }
995
+ })
996
+ }
997
+
998
+ this.socket.once('end', () => {
999
+ deliverClose()
1000
+ })
1001
+
1002
+ /**
1003
+ * @note The consumer may also destroy the socket before reading
1004
+ * the buffered data. Node.js still emits "close" for such
1005
+ * sockets, but the destroy machinery of a socket with a swapped
1006
+ * (already closed) handle never completes. Deliver "close" here.
1007
+ */
1008
+ const realDestroy = this.socket._destroy
1009
+ this.socket._destroy = (error, callback) => {
1010
+ deliverClose(error != null)
1011
+ return realDestroy.call(this.socket, error, callback)
1012
+ }
1013
+ return
1014
+ }
1015
+
1016
+ this.socket.emit('close', hadError)
1017
+ }
1018
+
1019
+ #onMockSocketDrain = () => {
1020
+ logger.verbose('client socket drained!')
1021
+ this.#passthroughSocket?.resume()
1022
+ }
1023
+
1024
+ /**
1025
+ * Suspend forwarding of the passthrough socket events ("data", "end", "close")
1026
+ * to the client socket. The events are buffered in order until `uncorkReads()`
1027
+ * is called. This allows the consumer to delay the delivery of the original
1028
+ * response to the client (e.g. until its own asynchronous logic settles).
1029
+ *
1030
+ * @note Pausing the client socket is not enough to delay the delivery.
1031
+ * Consumers like Undici read the pushed data from a paused socket
1032
+ * directly via `socket.read()`, which is unaffected by `socket.pause()`.
1033
+ */
1034
+ public corkReads(): void {
1035
+ this.#readsCorked = true
1036
+ }
1037
+
1038
+ /**
1039
+ * Resume forwarding of the passthrough socket events to the client socket,
1040
+ * replaying any events buffered while the reads were corked.
1041
+ */
1042
+ public uncorkReads(): void {
1043
+ if (!this.#readsCorked) {
1044
+ return
1045
+ }
1046
+
1047
+ this.#readsCorked = false
1048
+
1049
+ for (const corkedRead of this.#corkedReads.splice(0)) {
1050
+ switch (corkedRead.type) {
1051
+ case 'data': {
1052
+ if (!this.socket.push(corkedRead.chunk)) {
1053
+ logger.verbose(
1054
+ 'client socket forbade more pushes, pausing the passthrough socket...'
1055
+ )
1056
+ this.#passthroughSocket?.pause()
1057
+ }
1058
+ break
1059
+ }
1060
+
1061
+ case 'end': {
1062
+ this.#clientEndPushed = true
1063
+ this.socket.push(null)
1064
+ break
1065
+ }
1066
+
1067
+ case 'close': {
1068
+ this.#emitClientClose(corkedRead.hadError)
1069
+ break
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ public claim(): void {
1076
+ super.claim()
1077
+
1078
+ /**
1079
+ * @note The client may destroy the socket before the connection
1080
+ * is claimed (e.g. abort a request in-flight). There is no
1081
+ * connection to mock then, and the destroyed socket has no handle.
1082
+ */
1083
+ if (this.socket.destroyed) {
1084
+ logger.verbose('socket already destroyed, skipping claim...')
1085
+ return
1086
+ }
1087
+
1088
+ /**
1089
+ * @note Skip already connected sockets (e.g. kept-alive sockets
1090
+ * reused for the next exchange). Sockets with an emulated "connect"
1091
+ * only appear connected and must still complete the mock connection.
1092
+ */
1093
+ if (!this.socket.connecting && !this.#connectEmulated) {
1094
+ logger.verbose('socket already connected, skipping claim...')
1095
+ return
1096
+ }
1097
+
1098
+ logger.verbose('-> claim!')
1099
+
1100
+ /**
1101
+ * @note Reflect the local end of the claimed socket, the same way
1102
+ * the operating system reports the bound address of an outgoing
1103
+ * connection via "socket.address()"/"localAddress"/"localPort".
1104
+ * Patching the handle also prevents Node.js from handling the
1105
+ * "getsockname" errors of the never-connected raw handle.
1106
+ * @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/net.js#L971
1107
+ */
1108
+ this.socket._handle.getsockname = (addressInfo) => {
1109
+ Object.assign(
1110
+ addressInfo,
1111
+ getLocalAddressInfoByConnectionOptions(this.#connectionOptions)
1112
+ )
1113
+ return 0
1114
+ }
1115
+
1116
+ /**
1117
+ * @note Reflect the connection target as the peer of the claimed
1118
+ * socket, the same way a connected socket reports the server it
1119
+ * connected to via "remoteAddress"/"remotePort"/"remoteFamily".
1120
+ */
1121
+ this.socket._handle.getpeername = (addressInfo) => {
1122
+ Object.assign(
1123
+ addressInfo,
1124
+ getAddressInfoByConnectionOptions(this.#connectionOptions)
1125
+ )
1126
+ return 0
1127
+ }
1128
+
1129
+ this.#bufferedWrites = []
1130
+
1131
+ // Release the buffered write payloads. They were already delivered
1132
+ // to the server socket, and there is nowhere else to flush them.
1133
+ this.socket._pendingData = null
1134
+ this.socket._pendingEncoding = ''
1135
+
1136
+ this.pendingConnection.promise.then(([request, handle]) => {
1137
+ logger.verbose('connection request resolved, mocking the connection...')
1138
+
1139
+ /**
1140
+ * @note "afterConnect" asserts that the socket is connecting.
1141
+ * Restore the flag if the connect was emulated earlier (emulation
1142
+ * flips it so its listeners observe a connected socket).
1143
+ * "afterConnect" itself sets it back to false.
1144
+ */
1145
+ if (this.#connectEmulated) {
1146
+ Reflect.set(this.socket, 'connecting', true)
1147
+ }
1148
+
1149
+ /**
1150
+ * @see https://github.com/nodejs/node/blob/9cd6630870b776e96c5cf0ac68c31e2f46df3835/lib/net.js#L1142
1151
+ */
1152
+ request.oncomplete(0, handle, request, true, true)
1153
+ })
1154
+ }
1155
+
1156
+ public passthrough(flushPendingData?: FlushPendingDataFunction): net.Socket {
1157
+ super.passthrough()
1158
+
1159
+ logger.verbose('-> passthrough!')
1160
+
1161
+ const createRealSocket = () => {
1162
+ const realSocket = this.createConnection()
1163
+
1164
+ // Mark the passthrough socket as patched so it's exempt from
1165
+ // the unpatched socket detection (it never enters agent pools,
1166
+ // but this skips the detection cost on its every "destroyed" read).
1167
+ realSocket[kPatched] = true
1168
+
1169
+ if (this.socket.timeout != null) {
1170
+ realSocket.setTimeout(this.socket.timeout)
1171
+ }
1172
+
1173
+ return realSocket
1174
+ }
1175
+
1176
+ // If keepalive, reuse the existing real socket.
1177
+ const realSocket =
1178
+ this.#passthroughSocket && !this.#passthroughSocket.destroyed
1179
+ ? this.#passthroughSocket
1180
+ : createRealSocket()
1181
+
1182
+ if (realSocket !== this.#passthroughSocket) {
1183
+ this.#passthroughSocket = realSocket
1184
+ }
1185
+
1186
+ if (this.#bufferedWrites.length === 0) {
1187
+ logger.verbose(
1188
+ 'passthrough with empty writes buffer (state: %d)',
1189
+ this.readyState
1190
+ )
1191
+ }
1192
+
1193
+ /**
1194
+ * Flush any writes during the pending phase to the passthrough socket.
1195
+ * @note These are written directly on the passthrough socket to prevent
1196
+ * them from being forwarded as "data" events on the server (already emitted).
1197
+ */
1198
+ for (let i = 0; i < this.#bufferedWrites.length; i++) {
1199
+ const pendingWrite = this.#bufferedWrites[i]
1200
+
1201
+ if (i === 0 && typeof flushPendingData === 'function') {
1202
+ const data = pendingWrite[1]
1203
+ const encoding = pendingWrite[2]
1204
+ flushPendingData(data, encoding, (nextData) => {
1205
+ pendingWrite[1] = nextData
1206
+ })
1207
+ }
1208
+
1209
+ const [, data, encoding, callback] = pendingWrite
1210
+ writePendingData(realSocket, data, encoding, callback)
1211
+ }
1212
+
1213
+ this.#bufferedWrites = []
1214
+ this.socket._pendingData = null
1215
+ this.socket._pendingEncoding = ''
1216
+
1217
+ this.socket.address = realSocket.address.bind(realSocket)
1218
+
1219
+ this.socket.removeListener('drain', this.#onMockSocketDrain)
1220
+ this.socket.on('drain', this.#onMockSocketDrain)
1221
+
1222
+ realSocket
1223
+ .removeListener('connect', this.#onRealSocketConnect)
1224
+ .removeListener(
1225
+ 'connectionAttemptFailed',
1226
+ this.#onRealSocketConnectionAttemptFailed
1227
+ )
1228
+ .removeListener(
1229
+ 'connectionAttemptTimeout',
1230
+ this.#onRealSocketConnectionAttemptTimeout
1231
+ )
1232
+ .removeListener('data', this.#onRealSocketData)
1233
+ .removeListener('error', this.#onRealSocketError)
1234
+ .removeListener('end', this.#onRealSocketEnd)
1235
+ .removeListener('close', this.#onRealSocketClose)
1236
+
1237
+ realSocket
1238
+ .once('connect', this.#onRealSocketConnect)
1239
+ .on('connectionAttemptFailed', this.#onRealSocketConnectionAttemptFailed)
1240
+ .on(
1241
+ 'connectionAttemptTimeout',
1242
+ this.#onRealSocketConnectionAttemptTimeout
1243
+ )
1244
+ .on('data', this.#onRealSocketData)
1245
+ .on('error', this.#onRealSocketError)
1246
+ .on('end', this.#onRealSocketEnd)
1247
+ .on('close', this.#onRealSocketClose)
1248
+
1249
+ return realSocket
1250
+ }
1251
+ }
1252
+
1253
+ export class TlsSocketController extends TcpSocketController {
1254
+ /**
1255
+ * @note The TLS connection options must be provided explicitly.
1256
+ * They cannot be captured from "socket.connect()" like for plain
1257
+ * TCP sockets because "tls.connect()" fixes them at the TLS socket
1258
+ * construction, before its transport ever connects.
1259
+ */
1260
+ #tlsConnectionOptions?: TlsConnectionOptions
1261
+
1262
+ constructor(
1263
+ protected readonly socket: tls.TLSSocket,
1264
+ protected readonly createConnection: () => tls.TLSSocket,
1265
+ tlsConnectionOptions?: TlsConnectionOptions
1266
+ ) {
1267
+ super(socket, createConnection, tlsConnectionOptions)
1268
+
1269
+ this.#tlsConnectionOptions = tlsConnectionOptions
1270
+
1271
+ socket.prependListener('secureConnect', () => {
1272
+ /**
1273
+ * @note Reflect the negotiated ALPN protocol from the handle that
1274
+ * completed the handshake. The socket sets "alpnProtocol" only in
1275
+ * its own "_finishInit", which never runs for passthrough
1276
+ * connections (the handshake completes on the passthrough socket
1277
+ * whose handle this socket inherits).
1278
+ */
1279
+ socket.alpnProtocol = socket._handle.getALPNNegotiatedProtocol()
1280
+ })
1281
+ }
1282
+
1283
+ protected emulateConnect(): void {
1284
+ super.emulateConnect()
1285
+
1286
+ // For TLS sockets, also invoke the "secureConnect" callbacks since some consumers,
1287
+ // like Undici, listen to those to start writing to the socket.
1288
+ for (const listener of this.socket.rawListeners('secureConnect')) {
1289
+ listener.apply(this.socket)
1290
+ }
1291
+ }
1292
+
1293
+ public claim(): void {
1294
+ /**
1295
+ * @note The client may destroy the socket before the connection
1296
+ * is claimed. Defer to the parent class, which transitions the
1297
+ * ready state and skips the mock connection of destroyed sockets.
1298
+ */
1299
+ if (this.socket.destroyed) {
1300
+ super.claim()
1301
+ return
1302
+ }
1303
+
1304
+ /**
1305
+ * @note Reflect that the mocked connection is not authorized.
1306
+ * There is no real peer certificate to verify. The identity check
1307
+ * itself is skipped for claimed connections (see the
1308
+ * "isSessionReused" mock below).
1309
+ */
1310
+ this.socket.prependOnceListener('secureConnect', () => {
1311
+ Reflect.set(this.socket, 'authorized', false)
1312
+ Reflect.set(
1313
+ this.socket,
1314
+ 'authorizationError',
1315
+ 'MOCKED_CONNECTION_NOT_VERIFIED'
1316
+ )
1317
+ })
1318
+
1319
+ // Run this logic before the parent's class method so it executes first.
1320
+ // TLSWrap methods have to be patched before TCPWrap fires "oncomplete".
1321
+ const handle = this.socket._handle
1322
+
1323
+ handle.start = () => void 0
1324
+
1325
+ /**
1326
+ * Mock this to prevent the "Error: Worker exited unexpectedly" error.
1327
+ * This will trigger when "secure" is emitted.
1328
+ * @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1648
1329
+ */
1330
+ handle.verifyError = () => void 0
1331
+
1332
+ handle.getSession = () => {
1333
+ return Buffer.from('mocked session')
1334
+ }
1335
+
1336
+ /**
1337
+ * @note Skip the server identity check for this mocked connection.
1338
+ * Node.js runs "options.checkServerIdentity" in "onConnectSecure"
1339
+ * against the peer certificate, and a mocked connection has no
1340
+ * real peer certificate — the caller's (or the default) validation
1341
+ * would fail and destroy the socket. The check is overridden on the
1342
+ * socket's internal connect options since the emulated handshake
1343
+ * still completes through the regular "onConnectSecure" path.
1344
+ * @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1621
1345
+ */
1346
+ const realTlsConnectOptions = getTlsConnectOptions(this.socket)
1347
+
1348
+ if (realTlsConnectOptions) {
1349
+ realTlsConnectOptions.checkServerIdentity = () => {
1350
+ return undefined
1351
+ }
1352
+ }
1353
+
1354
+ handle.getCipher = () => {
1355
+ return {
1356
+ name: 'TLS_AES_256_GCM_SHA384',
1357
+ standardName: 'TLS_AES_256_GCM_SHA384',
1358
+ version: 'TLSv1.3',
1359
+ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Mock this to prevent a segfault on Node.js 26+. The native
1364
+ * implementation reads the negotiated group ("SSL_get0_group_name")
1365
+ * of a handshake that never happened. Node.js itself calls this
1366
+ * in "onConnectSecure" to validate the "minDHSize" option.
1367
+ * Reflect the ephemeral key exchange matching the mocked cipher.
1368
+ * @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1636
1369
+ */
1370
+ handle.getEphemeralKeyInfo = () => {
1371
+ return {
1372
+ type: 'ECDH',
1373
+ name: 'X25519',
1374
+ size: 253,
1375
+ }
1376
+ }
1377
+
1378
+ const requestedAlpnProtocols = this.#tlsConnectionOptions?.ALPNProtocols
1379
+
1380
+ if (
1381
+ Array.isArray(requestedAlpnProtocols) &&
1382
+ requestedAlpnProtocols.length > 0
1383
+ ) {
1384
+ const [preferredProtocol] = requestedAlpnProtocols
1385
+
1386
+ /**
1387
+ * @note Reflect the client's preferred ALPN protocol as the
1388
+ * negotiated one. The mocked server accepts whatever the
1389
+ * client prefers.
1390
+ */
1391
+ handle.getALPNNegotiatedProtocol = () => {
1392
+ return typeof preferredProtocol === 'string' ? preferredProtocol : false
1393
+ }
1394
+ }
1395
+
1396
+ this.socket.once('connect', () => {
1397
+ /**
1398
+ * @note A TLS 1.3 handshake derives five secrets, each reported
1399
+ * via a separate "keylog" event before the handshake completes.
1400
+ * Reflect them with mocked key material matching the mocked
1401
+ * cipher (SHA-384 secrets; the format is "LABEL <client random>
1402
+ * <secret>", each value hex-encoded).
1403
+ */
1404
+ const mockedClientRandom = '0'.repeat(64)
1405
+ const mockedSecret = '0'.repeat(96)
1406
+ const keylogLabels = [
1407
+ 'SERVER_HANDSHAKE_TRAFFIC_SECRET',
1408
+ 'EXPORTER_SECRET',
1409
+ 'SERVER_TRAFFIC_SECRET_0',
1410
+ 'CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1411
+ 'CLIENT_TRAFFIC_SECRET_0',
1412
+ ]
1413
+
1414
+ for (const keylogLabel of keylogLabels) {
1415
+ this.socket.emit(
1416
+ 'keylog',
1417
+ Buffer.from(`${keylogLabel} ${mockedClientRandom} ${mockedSecret}\n`)
1418
+ )
1419
+ }
1420
+
1421
+ handle.onhandshakedone()
1422
+
1423
+ /**
1424
+ * @note A TLS 1.3 server issues two session tickets by default,
1425
+ * each emitting a separate "session" event on the client.
1426
+ */
1427
+ handle.onnewsession(1, Buffer.from('mocked session'))
1428
+ handle.onnewsession(2, Buffer.from('mocked session'))
1429
+ })
1430
+
1431
+ super.claim()
1432
+ }
1433
+
1434
+ public passthrough(
1435
+ flushPendingData?: FlushPendingDataFunction
1436
+ ): tls.TLSSocket {
1437
+ const realSocket = super.passthrough(flushPendingData) as tls.TLSSocket
1438
+
1439
+ /**
1440
+ * @note Remove the internal "connect" listener added by the TLS socket.
1441
+ * Normally, that listener manages the SSL handshake. But since we're in passthrough,
1442
+ * we delegate that to the real socket. Leaving the listener on the mock socket while
1443
+ * inheriting the real socket's handle will result in the handshake performed twice, which is a no-op.
1444
+ * @see https://github.com/nodejs/node/blob/abddfc921bf2af02a04a6a5d2bca8e2d91d80958/lib/internal/tls/wrap.js#L1105
1445
+ *
1446
+ * This prevents the following error:
1447
+ * # node (vitest 4)[8686]: static void node::crypto::TLSWrap::Start(const FunctionCallbackInfo<Value> &) at ../src/crypto/crypto_tls.cc:589
1448
+ # Assertion failed: !wrap->started_
1449
+ */
1450
+ for (const connectListener of this.socket.listeners('connect')) {
1451
+ if (
1452
+ connectListener === this.socket._start ||
1453
+ ('listener' in connectListener &&
1454
+ connectListener.listener === this.socket._start)
1455
+ ) {
1456
+ this.socket.removeListener('connect', connectListener as () => void)
1457
+ }
1458
+ }
1459
+
1460
+ realSocket
1461
+ .on('secure', () => {
1462
+ this.socket.emit('secure')
1463
+ })
1464
+ .on('session', (...args) => {
1465
+ this.socket.emit('session', ...args)
1466
+ })
1467
+ .on('keylog', (...args) => {
1468
+ this.socket.emit('keylog', ...args)
1469
+ })
1470
+ .on('OCSPResponse', (...args) => {
1471
+ this.socket.emit('OCSPResponse', ...args)
1472
+ })
1473
+
1474
+ return realSocket
1475
+ }
1476
+ }