httpx-patched 1.6.2.1

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 (336) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +191 -0
  3. data/README.md +162 -0
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_10_1.md +37 -0
  11. data/doc/release_notes/0_10_2.md +5 -0
  12. data/doc/release_notes/0_11_0.md +74 -0
  13. data/doc/release_notes/0_11_1.md +5 -0
  14. data/doc/release_notes/0_11_2.md +5 -0
  15. data/doc/release_notes/0_11_3.md +5 -0
  16. data/doc/release_notes/0_12_0.md +55 -0
  17. data/doc/release_notes/0_13_0.md +58 -0
  18. data/doc/release_notes/0_13_1.md +5 -0
  19. data/doc/release_notes/0_13_2.md +9 -0
  20. data/doc/release_notes/0_14_0.md +79 -0
  21. data/doc/release_notes/0_14_1.md +7 -0
  22. data/doc/release_notes/0_14_2.md +6 -0
  23. data/doc/release_notes/0_14_3.md +5 -0
  24. data/doc/release_notes/0_14_4.md +5 -0
  25. data/doc/release_notes/0_14_5.md +11 -0
  26. data/doc/release_notes/0_15_0.md +53 -0
  27. data/doc/release_notes/0_15_1.md +8 -0
  28. data/doc/release_notes/0_15_2.md +9 -0
  29. data/doc/release_notes/0_15_3.md +5 -0
  30. data/doc/release_notes/0_15_4.md +5 -0
  31. data/doc/release_notes/0_16_0.md +93 -0
  32. data/doc/release_notes/0_16_1.md +5 -0
  33. data/doc/release_notes/0_17_0.md +49 -0
  34. data/doc/release_notes/0_18_0.md +69 -0
  35. data/doc/release_notes/0_18_1.md +12 -0
  36. data/doc/release_notes/0_18_2.md +10 -0
  37. data/doc/release_notes/0_18_3.md +7 -0
  38. data/doc/release_notes/0_18_4.md +14 -0
  39. data/doc/release_notes/0_18_5.md +10 -0
  40. data/doc/release_notes/0_18_6.md +5 -0
  41. data/doc/release_notes/0_18_7.md +5 -0
  42. data/doc/release_notes/0_19_0.md +39 -0
  43. data/doc/release_notes/0_19_1.md +5 -0
  44. data/doc/release_notes/0_19_2.md +7 -0
  45. data/doc/release_notes/0_19_3.md +6 -0
  46. data/doc/release_notes/0_19_4.md +14 -0
  47. data/doc/release_notes/0_19_5.md +13 -0
  48. data/doc/release_notes/0_19_6.md +5 -0
  49. data/doc/release_notes/0_19_7.md +5 -0
  50. data/doc/release_notes/0_19_8.md +5 -0
  51. data/doc/release_notes/0_1_0.md +9 -0
  52. data/doc/release_notes/0_20_0.md +36 -0
  53. data/doc/release_notes/0_20_1.md +5 -0
  54. data/doc/release_notes/0_20_2.md +7 -0
  55. data/doc/release_notes/0_20_3.md +6 -0
  56. data/doc/release_notes/0_20_4.md +17 -0
  57. data/doc/release_notes/0_20_5.md +3 -0
  58. data/doc/release_notes/0_21_0.md +96 -0
  59. data/doc/release_notes/0_21_1.md +12 -0
  60. data/doc/release_notes/0_22_0.md +13 -0
  61. data/doc/release_notes/0_22_1.md +11 -0
  62. data/doc/release_notes/0_22_2.md +5 -0
  63. data/doc/release_notes/0_22_3.md +55 -0
  64. data/doc/release_notes/0_22_4.md +6 -0
  65. data/doc/release_notes/0_22_5.md +6 -0
  66. data/doc/release_notes/0_23_0.md +42 -0
  67. data/doc/release_notes/0_23_1.md +5 -0
  68. data/doc/release_notes/0_23_2.md +5 -0
  69. data/doc/release_notes/0_23_3.md +6 -0
  70. data/doc/release_notes/0_23_4.md +5 -0
  71. data/doc/release_notes/0_24_0.md +48 -0
  72. data/doc/release_notes/0_24_1.md +12 -0
  73. data/doc/release_notes/0_24_2.md +12 -0
  74. data/doc/release_notes/0_24_3.md +12 -0
  75. data/doc/release_notes/0_24_4.md +18 -0
  76. data/doc/release_notes/0_24_5.md +6 -0
  77. data/doc/release_notes/0_24_6.md +5 -0
  78. data/doc/release_notes/0_24_7.md +10 -0
  79. data/doc/release_notes/0_2_0.md +5 -0
  80. data/doc/release_notes/0_2_1.md +16 -0
  81. data/doc/release_notes/0_3_0.md +12 -0
  82. data/doc/release_notes/0_3_1.md +6 -0
  83. data/doc/release_notes/0_4_0.md +51 -0
  84. data/doc/release_notes/0_4_1.md +3 -0
  85. data/doc/release_notes/0_5_0.md +15 -0
  86. data/doc/release_notes/0_5_1.md +14 -0
  87. data/doc/release_notes/0_6_0.md +5 -0
  88. data/doc/release_notes/0_6_1.md +6 -0
  89. data/doc/release_notes/0_6_2.md +6 -0
  90. data/doc/release_notes/0_6_3.md +13 -0
  91. data/doc/release_notes/0_6_4.md +21 -0
  92. data/doc/release_notes/0_6_5.md +22 -0
  93. data/doc/release_notes/0_6_6.md +19 -0
  94. data/doc/release_notes/0_6_7.md +5 -0
  95. data/doc/release_notes/0_7_0.md +46 -0
  96. data/doc/release_notes/0_8_0.md +27 -0
  97. data/doc/release_notes/0_8_1.md +8 -0
  98. data/doc/release_notes/0_8_2.md +7 -0
  99. data/doc/release_notes/0_9_0.md +38 -0
  100. data/doc/release_notes/1_0_0.md +60 -0
  101. data/doc/release_notes/1_0_1.md +5 -0
  102. data/doc/release_notes/1_0_2.md +7 -0
  103. data/doc/release_notes/1_1_0.md +32 -0
  104. data/doc/release_notes/1_1_1.md +17 -0
  105. data/doc/release_notes/1_1_2.md +12 -0
  106. data/doc/release_notes/1_1_3.md +18 -0
  107. data/doc/release_notes/1_1_4.md +6 -0
  108. data/doc/release_notes/1_1_5.md +12 -0
  109. data/doc/release_notes/1_2_0.md +49 -0
  110. data/doc/release_notes/1_2_1.md +6 -0
  111. data/doc/release_notes/1_2_2.md +10 -0
  112. data/doc/release_notes/1_2_3.md +16 -0
  113. data/doc/release_notes/1_2_4.md +8 -0
  114. data/doc/release_notes/1_2_5.md +7 -0
  115. data/doc/release_notes/1_2_6.md +13 -0
  116. data/doc/release_notes/1_3_0.md +18 -0
  117. data/doc/release_notes/1_3_1.md +17 -0
  118. data/doc/release_notes/1_3_2.md +6 -0
  119. data/doc/release_notes/1_3_3.md +5 -0
  120. data/doc/release_notes/1_3_4.md +6 -0
  121. data/doc/release_notes/1_4_0.md +43 -0
  122. data/doc/release_notes/1_4_1.md +19 -0
  123. data/doc/release_notes/1_4_2.md +20 -0
  124. data/doc/release_notes/1_4_3.md +11 -0
  125. data/doc/release_notes/1_4_4.md +14 -0
  126. data/doc/release_notes/1_5_0.md +126 -0
  127. data/doc/release_notes/1_5_1.md +6 -0
  128. data/doc/release_notes/1_6_0.md +50 -0
  129. data/doc/release_notes/1_6_1.md +17 -0
  130. data/doc/release_notes/1_6_2.md +11 -0
  131. data/lib/httpx/adapters/datadog.rb +359 -0
  132. data/lib/httpx/adapters/faraday.rb +303 -0
  133. data/lib/httpx/adapters/sentry.rb +121 -0
  134. data/lib/httpx/adapters/webmock.rb +175 -0
  135. data/lib/httpx/altsvc.rb +163 -0
  136. data/lib/httpx/base64.rb +27 -0
  137. data/lib/httpx/buffer.rb +61 -0
  138. data/lib/httpx/callbacks.rb +35 -0
  139. data/lib/httpx/chainable.rb +106 -0
  140. data/lib/httpx/connection/http1.rb +399 -0
  141. data/lib/httpx/connection/http2.rb +468 -0
  142. data/lib/httpx/connection.rb +954 -0
  143. data/lib/httpx/domain_name.rb +145 -0
  144. data/lib/httpx/errors.rb +111 -0
  145. data/lib/httpx/extensions.rb +59 -0
  146. data/lib/httpx/headers.rb +176 -0
  147. data/lib/httpx/io/ssl.rb +163 -0
  148. data/lib/httpx/io/tcp.rb +239 -0
  149. data/lib/httpx/io/udp.rb +62 -0
  150. data/lib/httpx/io/unix.rb +71 -0
  151. data/lib/httpx/io.rb +11 -0
  152. data/lib/httpx/loggable.rb +56 -0
  153. data/lib/httpx/options.rb +463 -0
  154. data/lib/httpx/parser/http1.rb +186 -0
  155. data/lib/httpx/plugins/auth/basic.rb +20 -0
  156. data/lib/httpx/plugins/auth/digest.rb +102 -0
  157. data/lib/httpx/plugins/auth/ntlm.rb +35 -0
  158. data/lib/httpx/plugins/auth/socks5.rb +22 -0
  159. data/lib/httpx/plugins/auth.rb +25 -0
  160. data/lib/httpx/plugins/aws_sdk_authentication.rb +111 -0
  161. data/lib/httpx/plugins/aws_sigv4.rb +239 -0
  162. data/lib/httpx/plugins/basic_auth.rb +29 -0
  163. data/lib/httpx/plugins/brotli.rb +50 -0
  164. data/lib/httpx/plugins/callbacks.rb +127 -0
  165. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  166. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  167. data/lib/httpx/plugins/circuit_breaker.rb +147 -0
  168. data/lib/httpx/plugins/content_digest.rb +204 -0
  169. data/lib/httpx/plugins/cookies/cookie.rb +174 -0
  170. data/lib/httpx/plugins/cookies/jar.rb +95 -0
  171. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +143 -0
  172. data/lib/httpx/plugins/cookies.rb +107 -0
  173. data/lib/httpx/plugins/digest_auth.rb +67 -0
  174. data/lib/httpx/plugins/expect.rb +120 -0
  175. data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
  176. data/lib/httpx/plugins/follow_redirects.rb +233 -0
  177. data/lib/httpx/plugins/grpc/call.rb +63 -0
  178. data/lib/httpx/plugins/grpc/grpc_encoding.rb +90 -0
  179. data/lib/httpx/plugins/grpc/message.rb +55 -0
  180. data/lib/httpx/plugins/grpc.rb +282 -0
  181. data/lib/httpx/plugins/h2c.rb +127 -0
  182. data/lib/httpx/plugins/internal_telemetry.rb +107 -0
  183. data/lib/httpx/plugins/ntlm_auth.rb +62 -0
  184. data/lib/httpx/plugins/oauth.rb +183 -0
  185. data/lib/httpx/plugins/persistent.rb +82 -0
  186. data/lib/httpx/plugins/proxy/http.rb +184 -0
  187. data/lib/httpx/plugins/proxy/socks4.rb +135 -0
  188. data/lib/httpx/plugins/proxy/socks5.rb +194 -0
  189. data/lib/httpx/plugins/proxy/ssh.rb +94 -0
  190. data/lib/httpx/plugins/proxy.rb +349 -0
  191. data/lib/httpx/plugins/push_promise.rb +81 -0
  192. data/lib/httpx/plugins/query.rb +35 -0
  193. data/lib/httpx/plugins/rate_limiter.rb +55 -0
  194. data/lib/httpx/plugins/response_cache/file_store.rb +140 -0
  195. data/lib/httpx/plugins/response_cache/store.rb +33 -0
  196. data/lib/httpx/plugins/response_cache.rb +333 -0
  197. data/lib/httpx/plugins/retries.rb +230 -0
  198. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  199. data/lib/httpx/plugins/stream.rb +183 -0
  200. data/lib/httpx/plugins/stream_bidi.rb +315 -0
  201. data/lib/httpx/plugins/upgrade/h2.rb +64 -0
  202. data/lib/httpx/plugins/upgrade.rb +86 -0
  203. data/lib/httpx/plugins/webdav.rb +86 -0
  204. data/lib/httpx/plugins/xml.rb +76 -0
  205. data/lib/httpx/pmatch_extensions.rb +33 -0
  206. data/lib/httpx/pool.rb +190 -0
  207. data/lib/httpx/punycode.rb +22 -0
  208. data/lib/httpx/request/body.rb +158 -0
  209. data/lib/httpx/request.rb +328 -0
  210. data/lib/httpx/resolver/entry.rb +30 -0
  211. data/lib/httpx/resolver/https.rb +256 -0
  212. data/lib/httpx/resolver/multi.rb +102 -0
  213. data/lib/httpx/resolver/native.rb +547 -0
  214. data/lib/httpx/resolver/resolver.rb +173 -0
  215. data/lib/httpx/resolver/system.rb +255 -0
  216. data/lib/httpx/resolver.rb +189 -0
  217. data/lib/httpx/response/body.rb +242 -0
  218. data/lib/httpx/response/buffer.rb +115 -0
  219. data/lib/httpx/response.rb +304 -0
  220. data/lib/httpx/selector.rb +282 -0
  221. data/lib/httpx/session.rb +612 -0
  222. data/lib/httpx/session_extensions.rb +30 -0
  223. data/lib/httpx/timers.rb +133 -0
  224. data/lib/httpx/transcoder/body.rb +43 -0
  225. data/lib/httpx/transcoder/chunker.rb +115 -0
  226. data/lib/httpx/transcoder/deflate.rb +37 -0
  227. data/lib/httpx/transcoder/form.rb +68 -0
  228. data/lib/httpx/transcoder/gzip.rb +71 -0
  229. data/lib/httpx/transcoder/json.rb +71 -0
  230. data/lib/httpx/transcoder/multipart/decoder.rb +141 -0
  231. data/lib/httpx/transcoder/multipart/encoder.rb +120 -0
  232. data/lib/httpx/transcoder/multipart/mime_type_detector.rb +78 -0
  233. data/lib/httpx/transcoder/multipart/part.rb +35 -0
  234. data/lib/httpx/transcoder/multipart.rb +31 -0
  235. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  236. data/lib/httpx/transcoder/utils/deflater.rb +75 -0
  237. data/lib/httpx/transcoder.rb +91 -0
  238. data/lib/httpx/utils.rb +75 -0
  239. data/lib/httpx/version.rb +5 -0
  240. data/lib/httpx.rb +66 -0
  241. data/sig/altsvc.rbs +33 -0
  242. data/sig/buffer.rbs +27 -0
  243. data/sig/callbacks.rbs +15 -0
  244. data/sig/chainable.rbs +55 -0
  245. data/sig/connection/http1.rbs +85 -0
  246. data/sig/connection/http2.rbs +116 -0
  247. data/sig/connection.rbs +169 -0
  248. data/sig/domain_name.rbs +17 -0
  249. data/sig/errors.rbs +69 -0
  250. data/sig/headers.rbs +49 -0
  251. data/sig/httpx.rbs +27 -0
  252. data/sig/io/ssl.rbs +27 -0
  253. data/sig/io/tcp.rbs +72 -0
  254. data/sig/io/udp.rbs +25 -0
  255. data/sig/io/unix.rbs +26 -0
  256. data/sig/io.rbs +3 -0
  257. data/sig/loggable.rbs +17 -0
  258. data/sig/options.rbs +202 -0
  259. data/sig/parser/http1.rbs +59 -0
  260. data/sig/plugins/auth/basic.rbs +17 -0
  261. data/sig/plugins/auth/digest.rbs +25 -0
  262. data/sig/plugins/auth/ntlm.rbs +20 -0
  263. data/sig/plugins/auth/socks5.rbs +18 -0
  264. data/sig/plugins/auth.rbs +13 -0
  265. data/sig/plugins/aws_sdk_authentication.rbs +43 -0
  266. data/sig/plugins/aws_sigv4.rbs +78 -0
  267. data/sig/plugins/basic_auth.rbs +15 -0
  268. data/sig/plugins/brotli.rbs +22 -0
  269. data/sig/plugins/callbacks.rbs +38 -0
  270. data/sig/plugins/circuit_breaker.rbs +71 -0
  271. data/sig/plugins/compression.rbs +57 -0
  272. data/sig/plugins/content_digest.rbs +51 -0
  273. data/sig/plugins/cookies/cookie.rbs +55 -0
  274. data/sig/plugins/cookies/jar.rbs +26 -0
  275. data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
  276. data/sig/plugins/cookies.rbs +28 -0
  277. data/sig/plugins/digest_auth.rbs +21 -0
  278. data/sig/plugins/expect.rbs +15 -0
  279. data/sig/plugins/fiber_concurrency.rbs +51 -0
  280. data/sig/plugins/follow_redirects.rbs +47 -0
  281. data/sig/plugins/grpc/call.rbs +23 -0
  282. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  283. data/sig/plugins/grpc/message.rbs +17 -0
  284. data/sig/plugins/grpc.rbs +65 -0
  285. data/sig/plugins/h2c.rbs +27 -0
  286. data/sig/plugins/ntlm_auth.rbs +21 -0
  287. data/sig/plugins/oauth.rbs +68 -0
  288. data/sig/plugins/persistent.rbs +14 -0
  289. data/sig/plugins/proxy/http.rbs +30 -0
  290. data/sig/plugins/proxy/socks4.rbs +37 -0
  291. data/sig/plugins/proxy/socks5.rbs +49 -0
  292. data/sig/plugins/proxy/ssh.rbs +18 -0
  293. data/sig/plugins/proxy.rbs +70 -0
  294. data/sig/plugins/push_promise.rbs +23 -0
  295. data/sig/plugins/query.rbs +18 -0
  296. data/sig/plugins/rate_limiter.rbs +13 -0
  297. data/sig/plugins/response_cache/file_store.rbs +19 -0
  298. data/sig/plugins/response_cache/store.rbs +13 -0
  299. data/sig/plugins/response_cache.rbs +86 -0
  300. data/sig/plugins/retries.rbs +66 -0
  301. data/sig/plugins/ssrf_filter.rbs +26 -0
  302. data/sig/plugins/stream.rbs +54 -0
  303. data/sig/plugins/stream_bidi.rbs +68 -0
  304. data/sig/plugins/upgrade/h2.rbs +9 -0
  305. data/sig/plugins/upgrade.rbs +29 -0
  306. data/sig/plugins/webdav.rbs +23 -0
  307. data/sig/plugins/xml.rbs +37 -0
  308. data/sig/pool.rbs +51 -0
  309. data/sig/punycode.rbs +5 -0
  310. data/sig/request/body.rbs +34 -0
  311. data/sig/request.rbs +88 -0
  312. data/sig/resolver/entry.rbs +13 -0
  313. data/sig/resolver/https.rbs +45 -0
  314. data/sig/resolver/multi.rbs +32 -0
  315. data/sig/resolver/native.rbs +74 -0
  316. data/sig/resolver/resolver.rbs +64 -0
  317. data/sig/resolver/system.rbs +34 -0
  318. data/sig/resolver.rbs +48 -0
  319. data/sig/response/body.rbs +52 -0
  320. data/sig/response/buffer.rbs +23 -0
  321. data/sig/response.rbs +103 -0
  322. data/sig/selector.rbs +68 -0
  323. data/sig/session.rbs +104 -0
  324. data/sig/timers.rbs +54 -0
  325. data/sig/transcoder/body.rbs +24 -0
  326. data/sig/transcoder/chunker.rbs +49 -0
  327. data/sig/transcoder/deflate.rbs +12 -0
  328. data/sig/transcoder/form.rbs +34 -0
  329. data/sig/transcoder/gzip.rbs +27 -0
  330. data/sig/transcoder/json.rbs +28 -0
  331. data/sig/transcoder/multipart.rbs +103 -0
  332. data/sig/transcoder/utils/body_reader.rbs +15 -0
  333. data/sig/transcoder/utils/deflater.rbs +28 -0
  334. data/sig/transcoder.rbs +43 -0
  335. data/sig/utils.rbs +19 -0
  336. metadata +518 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds suppoort for callbacks around the request/response lifecycle.
7
+ #
8
+ # https://gitlab.com/os85/httpx/-/wikis/Events
9
+ #
10
+ module Callbacks
11
+ CALLBACKS = %i[
12
+ connection_opened connection_closed
13
+ request_error
14
+ request_started request_body_chunk request_completed
15
+ response_started response_body_chunk response_completed
16
+ ].freeze
17
+
18
+ # connection closed user-space errors happen after errors can be surfaced to requests,
19
+ # so they need to pierce through the scheduler, which is only possible by simulating an
20
+ # interrupt.
21
+ class CallbackError < Exception; end # rubocop:disable Lint/InheritException
22
+
23
+ module InstanceMethods
24
+ include HTTPX::Callbacks
25
+
26
+ CALLBACKS.each do |meth|
27
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
28
+ def on_#{meth}(&blk) # def on_connection_opened(&blk)
29
+ on(:#{meth}, &blk) # on(:connection_opened, &blk)
30
+ self # self
31
+ end # end
32
+ MOD
33
+ end
34
+
35
+ def plugin(*args, &blk)
36
+ super(*args).tap do |sess|
37
+ CALLBACKS.each do |cb|
38
+ next unless callbacks_for?(cb)
39
+
40
+ sess.callbacks(cb).concat(callbacks(cb))
41
+ end
42
+
43
+ sess.wrap(&blk) if blk
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def branch(options, &blk)
50
+ super(options).tap do |sess|
51
+ CALLBACKS.each do |cb|
52
+ next unless callbacks_for?(cb)
53
+
54
+ sess.callbacks(cb).concat(callbacks(cb))
55
+ end
56
+ sess.wrap(&blk) if blk
57
+ end
58
+ end
59
+
60
+ def do_init_connection(connection, selector)
61
+ super
62
+ connection.on(:open) do
63
+ next unless connection.current_session == self
64
+
65
+ emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
66
+ end
67
+ connection.on(:close) do
68
+ next unless connection.current_session == self
69
+
70
+ emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
71
+ end
72
+
73
+ connection
74
+ end
75
+
76
+ def set_request_callbacks(request)
77
+ super
78
+
79
+ request.on(:headers) do
80
+ emit_or_callback_error(:request_started, request)
81
+ end
82
+ request.on(:body_chunk) do |chunk|
83
+ emit_or_callback_error(:request_body_chunk, request, chunk)
84
+ end
85
+ request.on(:done) do
86
+ emit_or_callback_error(:request_completed, request)
87
+ end
88
+
89
+ request.on(:response_started) do |res|
90
+ if res.is_a?(Response)
91
+ emit_or_callback_error(:response_started, request, res)
92
+ res.on(:chunk_received) do |chunk|
93
+ emit_or_callback_error(:response_body_chunk, request, res, chunk)
94
+ end
95
+ else
96
+ emit_or_callback_error(:request_error, request, res.error)
97
+ end
98
+ end
99
+ request.on(:response) do |res|
100
+ emit_or_callback_error(:response_completed, request, res) if res.is_a?(Response)
101
+ end
102
+ end
103
+
104
+ def emit_or_callback_error(*args)
105
+ emit(*args)
106
+ rescue StandardError => e
107
+ ex = CallbackError.new(e.message)
108
+ ex.set_backtrace(e.backtrace)
109
+ raise ex
110
+ end
111
+
112
+ def receive_requests(*)
113
+ super
114
+ rescue CallbackError => e
115
+ raise e.cause
116
+ end
117
+
118
+ def close(*)
119
+ super
120
+ rescue CallbackError => e
121
+ raise e.cause
122
+ end
123
+ end
124
+ end
125
+ register_plugin :callbacks, Callbacks
126
+ end
127
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins::CircuitBreaker
5
+ #
6
+ # A circuit is assigned to a given absoolute url or origin.
7
+ #
8
+ # It sets +max_attempts+, the number of attempts the circuit allows, before it is opened.
9
+ # It sets +reset_attempts_in+, the time a circuit stays open at most, before it resets.
10
+ # It sets +break_in+, the time that must elapse before an open circuit can transit to the half-open state.
11
+ # It sets +circuit_breaker_half_open_drip_rate+, the rate of requests a circuit allows to be performed when in an half-open state.
12
+ #
13
+ class Circuit
14
+ def initialize(max_attempts, reset_attempts_in, break_in, circuit_breaker_half_open_drip_rate)
15
+ @max_attempts = max_attempts
16
+ @reset_attempts_in = reset_attempts_in
17
+ @break_in = break_in
18
+ @circuit_breaker_half_open_drip_rate = circuit_breaker_half_open_drip_rate
19
+ @attempts = 0
20
+
21
+ total_real_attempts = @max_attempts * @circuit_breaker_half_open_drip_rate
22
+ @drip_factor = (@max_attempts / total_real_attempts).round
23
+ @state = :closed
24
+ end
25
+
26
+ def respond
27
+ try_close
28
+
29
+ case @state
30
+ when :closed
31
+ nil
32
+ when :half_open
33
+ @attempts += 1
34
+
35
+ # do real requests while drip rate valid
36
+ if (@real_attempts % @drip_factor).zero?
37
+ @real_attempts += 1
38
+ return
39
+ end
40
+
41
+ @response
42
+ when :open
43
+
44
+ @response
45
+ end
46
+ end
47
+
48
+ def try_open(response)
49
+ case @state
50
+ when :closed
51
+ now = Utils.now
52
+
53
+ if @attempts.positive?
54
+ # reset if error happened long ago
55
+ @attempts = 0 if now - @attempted_at > @reset_attempts_in
56
+ else
57
+ @attempted_at = now
58
+ end
59
+
60
+ @attempts += 1
61
+
62
+ return unless @attempts >= @max_attempts
63
+
64
+ @state = :open
65
+ @opened_at = now
66
+ @response = response
67
+ when :half_open
68
+ # open immediately
69
+
70
+ @state = :open
71
+ @attempted_at = @opened_at = Utils.now
72
+ @response = response
73
+ end
74
+ end
75
+
76
+ def try_close
77
+ case @state
78
+ when :closed
79
+ nil
80
+ when :half_open
81
+
82
+ # do not close circuit unless attempts exhausted
83
+ return unless @attempts >= @max_attempts
84
+
85
+ # reset!
86
+ @attempts = 0
87
+ @opened_at = @attempted_at = @response = nil
88
+ @state = :closed
89
+
90
+ when :open
91
+ if Utils.elapsed_time(@opened_at) > @break_in
92
+ @state = :half_open
93
+ @attempts = 0
94
+ @real_attempts = 0
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX::Plugins::CircuitBreaker
4
+ using HTTPX::URIExtensions
5
+
6
+ class CircuitStore
7
+ def initialize(options)
8
+ @circuits = Hash.new do |h, k|
9
+ h[k] = Circuit.new(
10
+ options.circuit_breaker_max_attempts,
11
+ options.circuit_breaker_reset_attempts_in,
12
+ options.circuit_breaker_break_in,
13
+ options.circuit_breaker_half_open_drip_rate
14
+ )
15
+ end
16
+ @circuits_mutex = Thread::Mutex.new
17
+ end
18
+
19
+ def try_open(uri, response)
20
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(uri) }
21
+
22
+ circuit.try_open(response)
23
+ end
24
+
25
+ def try_close(uri)
26
+ circuit = @circuits_mutex.synchronize do
27
+ return unless @circuits.key?(uri.origin) || @circuits.key?(uri.to_s)
28
+
29
+ get_circuit_for_uri(uri)
30
+ end
31
+
32
+ circuit.try_close
33
+ end
34
+
35
+ # if circuit is open, it'll respond with the stored response.
36
+ # if not, nil.
37
+ def try_respond(request)
38
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(request.uri) }
39
+
40
+ circuit.respond
41
+ end
42
+
43
+ private
44
+
45
+ def get_circuit_for_uri(uri)
46
+ if uri.respond_to?(:origin) && @circuits.key?(uri.origin)
47
+ @circuits[uri.origin]
48
+ else
49
+ @circuits[uri.to_s]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin implements a circuit breaker around connection errors.
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/Circuit-Breaker
9
+ #
10
+ module CircuitBreaker
11
+ using URIExtensions
12
+
13
+ def self.load_dependencies(*)
14
+ require_relative "circuit_breaker/circuit"
15
+ require_relative "circuit_breaker/circuit_store"
16
+ end
17
+
18
+ def self.extra_options(options)
19
+ options.merge(
20
+ circuit_breaker_max_attempts: 3,
21
+ circuit_breaker_reset_attempts_in: 60,
22
+ circuit_breaker_break_in: 60,
23
+ circuit_breaker_half_open_drip_rate: 1
24
+ )
25
+ end
26
+
27
+ module InstanceMethods
28
+ include HTTPX::Callbacks
29
+
30
+ def initialize(*)
31
+ super
32
+ @circuit_store = CircuitStore.new(@options)
33
+ end
34
+
35
+ %i[circuit_open].each do |meth|
36
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
37
+ def on_#{meth}(&blk) # def on_circuit_open(&blk)
38
+ on(:#{meth}, &blk) # on(:circuit_open, &blk)
39
+ self # self
40
+ end # end
41
+ MOD
42
+ end
43
+
44
+ private
45
+
46
+ def send_requests(*requests)
47
+ # @type var short_circuit_responses: Array[response]
48
+ short_circuit_responses = []
49
+
50
+ # run all requests through the circuit breaker, see if the circuit is
51
+ # open for any of them.
52
+ real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
53
+ short_circuit_response = @circuit_store.try_respond(req)
54
+ if short_circuit_response.nil?
55
+ real_reqs << req
56
+ next
57
+ end
58
+ short_circuit_responses[idx] = short_circuit_response
59
+ end
60
+
61
+ # run requests for the remainder
62
+ unless real_requests.empty?
63
+ responses = super(*real_requests)
64
+
65
+ real_requests.each_with_index do |request, idx|
66
+ short_circuit_responses[requests.index(request)] = responses[idx]
67
+ end
68
+ end
69
+
70
+ short_circuit_responses
71
+ end
72
+
73
+ def set_request_callbacks(request)
74
+ super
75
+ request.on(:response) do |response|
76
+ emit(:circuit_open, request) if try_circuit_open(request, response)
77
+ end
78
+ end
79
+
80
+ def try_circuit_open(request, response)
81
+ if response.is_a?(ErrorResponse)
82
+ case response.error
83
+ when RequestTimeoutError
84
+ @circuit_store.try_open(request.uri, response)
85
+ else
86
+ @circuit_store.try_open(request.origin, response)
87
+ end
88
+ elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
89
+ @circuit_store.try_open(request.uri, response)
90
+ else
91
+ @circuit_store.try_close(request.uri)
92
+ nil
93
+ end
94
+ end
95
+ end
96
+
97
+ # adds support for the following options:
98
+ #
99
+ # :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
100
+ # :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
101
+ # :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
102
+ # (i.e. <tt>->(res) { res.status == 429 } </tt>)
103
+ # :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
104
+ # (defaults to <tt><60</tt>).
105
+ # :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
106
+ # (defaults to <tt>1</tt>).
107
+ module OptionsMethods
108
+ private
109
+
110
+ def option_circuit_breaker_max_attempts(value)
111
+ attempts = Integer(value)
112
+ raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
113
+
114
+ attempts
115
+ end
116
+
117
+ def option_circuit_breaker_reset_attempts_in(value)
118
+ timeout = Float(value)
119
+ raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
120
+
121
+ timeout
122
+ end
123
+
124
+ def option_circuit_breaker_break_in(value)
125
+ timeout = Float(value)
126
+ raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
127
+
128
+ timeout
129
+ end
130
+
131
+ def option_circuit_breaker_half_open_drip_rate(value)
132
+ ratio = Float(value)
133
+ raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
134
+
135
+ ratio
136
+ end
137
+
138
+ def option_circuit_breaker_break_on(value)
139
+ raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
140
+
141
+ value
142
+ end
143
+ end
144
+ end
145
+ register_plugin :circuit_breaker, CircuitBreaker
146
+ end
147
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds `Content-Digest` headers to requests
7
+ # and can validate these headers on responses
8
+ #
9
+ # https://datatracker.ietf.org/doc/html/rfc9530
10
+ #
11
+ module ContentDigest
12
+ class Error < HTTPX::Error; end
13
+
14
+ # Error raised on response "content-digest" header validation.
15
+ class ValidationError < Error
16
+ attr_reader :response
17
+
18
+ def initialize(message, response)
19
+ super(message)
20
+ @response = response
21
+ end
22
+ end
23
+
24
+ class MissingContentDigestError < ValidationError; end
25
+ class InvalidContentDigestError < ValidationError; end
26
+
27
+ SUPPORTED_ALGORITHMS = {
28
+ "sha-256" => OpenSSL::Digest::SHA256,
29
+ "sha-512" => OpenSSL::Digest::SHA512,
30
+ }.freeze
31
+
32
+ class << self
33
+ def extra_options(options)
34
+ options.merge(encode_content_digest: true, validate_content_digest: false, content_digest_algorithm: "sha-256")
35
+ end
36
+ end
37
+
38
+ # add support for the following options:
39
+ #
40
+ # :content_digest_algorithm :: the digest algorithm to use. Currently supports `sha-256` and `sha-512`. (defaults to `sha-256`)
41
+ # :encode_content_digest :: whether a <tt>Content-Digest</tt> header should be computed for the request;
42
+ # can also be a callable object (i.e. <tt>->(req) { ... }</tt>, defaults to <tt>true</tt>)
43
+ # :validate_content_digest :: whether a <tt>Content-Digest</tt> header in the response should be validated;
44
+ # can also be a callable object (i.e. <tt>->(res) { ... }</tt>, defaults to <tt>false</tt>)
45
+ module OptionsMethods
46
+ private
47
+
48
+ def option_content_digest_algorithm(value)
49
+ raise TypeError, ":content_digest_algorithm must be one of 'sha-256', 'sha-512'" unless SUPPORTED_ALGORITHMS.key?(value)
50
+
51
+ value
52
+ end
53
+
54
+ def option_encode_content_digest(value)
55
+ value
56
+ end
57
+
58
+ def option_validate_content_digest(value)
59
+ value
60
+ end
61
+ end
62
+
63
+ module ResponseBodyMethods
64
+ attr_reader :content_digest_buffer
65
+
66
+ def initialize(response, options)
67
+ super
68
+
69
+ return unless response.headers.key?("content-digest")
70
+
71
+ should_validate = options.validate_content_digest
72
+ should_validate = should_validate.call(response) if should_validate.respond_to?(:call)
73
+
74
+ return unless should_validate
75
+
76
+ @content_digest_buffer = Response::Buffer.new(
77
+ threshold_size: @options.body_threshold_size,
78
+ bytesize: @length,
79
+ encoding: @encoding
80
+ )
81
+ end
82
+
83
+ def write(chunk)
84
+ @content_digest_buffer.write(chunk) if @content_digest_buffer
85
+ super
86
+ end
87
+
88
+ def close
89
+ if @content_digest_buffer
90
+ @content_digest_buffer.close
91
+ @content_digest_buffer = nil
92
+ end
93
+ super
94
+ end
95
+ end
96
+
97
+ module InstanceMethods
98
+ def build_request(*)
99
+ request = super
100
+
101
+ return request if request.empty?
102
+
103
+ return request if request.headers.key?("content-digest")
104
+
105
+ perform_encoding = @options.encode_content_digest
106
+ perform_encoding = perform_encoding.call(request) if perform_encoding.respond_to?(:call)
107
+
108
+ return request unless perform_encoding
109
+
110
+ digest = base64digest(request.body)
111
+ request.headers.add("content-digest", "#{@options.content_digest_algorithm}=:#{digest}:")
112
+
113
+ request
114
+ end
115
+
116
+ private
117
+
118
+ def fetch_response(request, _, _)
119
+ response = super
120
+ return response unless response.is_a?(Response)
121
+
122
+ perform_validation = @options.validate_content_digest
123
+ perform_validation = perform_validation.call(response) if perform_validation.respond_to?(:call)
124
+
125
+ validate_content_digest(response) if perform_validation
126
+
127
+ response
128
+ rescue ValidationError => e
129
+ ErrorResponse.new(request, e)
130
+ end
131
+
132
+ def validate_content_digest(response)
133
+ content_digest_header = response.headers["content-digest"]
134
+
135
+ raise MissingContentDigestError.new("response is missing a `content-digest` header", response) unless content_digest_header
136
+
137
+ digests = extract_content_digests(content_digest_header)
138
+
139
+ included_algorithms = SUPPORTED_ALGORITHMS.keys & digests.keys
140
+
141
+ raise MissingContentDigestError.new("unsupported algorithms: #{digests.keys.join(", ")}", response) if included_algorithms.empty?
142
+
143
+ content_buffer = response.body.content_digest_buffer
144
+
145
+ included_algorithms.each do |algorithm|
146
+ digest = SUPPORTED_ALGORITHMS.fetch(algorithm).new
147
+ digest_received = digests[algorithm]
148
+ digest_computed =
149
+ if content_buffer.respond_to?(:to_path)
150
+ content_buffer.flush
151
+ digest.file(content_buffer.to_path).base64digest
152
+ else
153
+ digest.base64digest(content_buffer.to_s)
154
+ end
155
+
156
+ raise InvalidContentDigestError.new("#{algorithm} digest does not match content",
157
+ response) unless digest_received == digest_computed
158
+ end
159
+ end
160
+
161
+ def extract_content_digests(header)
162
+ header.split(",").to_h do |entry|
163
+ algorithm, digest = entry.split("=", 2)
164
+ raise Error, "#{entry} is an invalid digest format" unless algorithm && digest
165
+
166
+ [algorithm, digest.byteslice(1..-2)]
167
+ end
168
+ end
169
+
170
+ def base64digest(body)
171
+ digest = SUPPORTED_ALGORITHMS.fetch(@options.content_digest_algorithm).new
172
+
173
+ if body.respond_to?(:read)
174
+ if body.respond_to?(:to_path)
175
+ digest.file(body.to_path).base64digest
176
+ else
177
+ raise ContentDigestError, "request body must be rewindable" unless body.respond_to?(:rewind)
178
+
179
+ buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
180
+ begin
181
+ IO.copy_stream(body, buffer)
182
+ buffer.flush
183
+
184
+ digest.file(buffer.to_path).base64digest
185
+ ensure
186
+ body.rewind
187
+ buffer.close
188
+ buffer.unlink
189
+ end
190
+ end
191
+ else
192
+ raise ContentDigestError, "base64digest for endless enumerators is not supported" if body.unbounded_body?
193
+
194
+ buffer = "".b
195
+ body.each { |chunk| buffer << chunk }
196
+
197
+ digest.base64digest(buffer)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ register_plugin :content_digest, ContentDigest
203
+ end
204
+ end