httpx 0.20.0 → 1.3.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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +5 -5
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -5,13 +5,14 @@ module HTTPX
5
5
  #
6
6
  # This plugin adds AWS Sigv4 authentication.
7
7
  #
8
- # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
8
+ # https://docs.aws.amazon.com/IAM/latest/UserGuide/signing-elements.html
9
9
  #
10
- # https://gitlab.com/honeyryderchuck/httpx/wikis/AWS-SigV4
10
+ # https://gitlab.com/os85/httpx/wikis/AWS-SigV4
11
11
  #
12
12
  module AWSSigV4
13
13
  Credentials = Struct.new(:username, :password, :security_token)
14
14
 
15
+ # Signs requests using the AWS sigv4 signing.
15
16
  class Signer
16
17
  def initialize(
17
18
  service:,
@@ -71,7 +72,7 @@ module HTTPX
71
72
  end.join
72
73
 
73
74
  # canonical request
74
- creq = "#{request.verb.to_s.upcase}" \
75
+ creq = "#{request.verb}" \
75
76
  "\n#{request.canonical_path}" \
76
77
  "\n#{request.canonical_query}" \
77
78
  "\n#{canonical_headers}" \
@@ -115,7 +116,7 @@ module HTTPX
115
116
  elsif value.respond_to?(:each)
116
117
  digest = OpenSSL::Digest.new(@algorithm)
117
118
 
118
- mb_buffer = value.each.each_with_object("".b) do |chunk, buffer|
119
+ mb_buffer = value.each.with_object("".b) do |chunk, buffer|
119
120
  buffer << chunk
120
121
  break if buffer.bytesize >= 1024 * 1024
121
122
  end
@@ -146,10 +147,12 @@ module HTTPX
146
147
 
147
148
  def configure(klass)
148
149
  klass.plugin(:expect)
149
- klass.plugin(:compression)
150
150
  end
151
151
  end
152
152
 
153
+ # adds support for the following options:
154
+ #
155
+ # :sigv4_signer :: instance of HTTPX::Plugins::AWSSigV4 used to sign requests.
153
156
  module OptionsMethods
154
157
  def option_sigv4_signer(value)
155
158
  value.is_a?(Signer) ? value : Signer.new(value)
@@ -161,7 +164,7 @@ module HTTPX
161
164
  with(sigv4_signer: Signer.new(**options))
162
165
  end
163
166
 
164
- def build_request(*, _)
167
+ def build_request(*)
165
168
  request = super
166
169
 
167
170
  return request if request.headers.key?("authorization")
@@ -186,7 +189,7 @@ module HTTPX
186
189
  def canonical_query
187
190
  params = query.split("&")
188
191
  # params = params.map { |p| p.match(/=/) ? p : p + '=' }
189
- # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
192
+ # From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
190
193
  # Sort the parameter names by character code point in ascending order.
191
194
  # Parameters with duplicate names should be sorted by value.
192
195
  #
@@ -198,8 +201,8 @@ module HTTPX
198
201
  params.each.with_index.sort do |a, b|
199
202
  a, a_offset = a
200
203
  b, b_offset = b
201
- a_name, a_value = a.split("=")
202
- b_name, b_value = b.split("=")
204
+ a_name, a_value = a.split("=", 2)
205
+ b_name, b_value = b.split("=", 2)
203
206
  if a_name == b_name
204
207
  if a_value == b_value
205
208
  a_offset <=> b_offset
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds helper methods to implement HTTP Basic Auth (https://datatracker.ietf.org/doc/html/rfc7617)
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/Auth#basic-auth
9
+ #
10
+ module BasicAuth
11
+ class << self
12
+ def load_dependencies(_klass)
13
+ require_relative "auth/basic"
14
+ end
15
+
16
+ def configure(klass)
17
+ klass.plugin(:auth)
18
+ end
19
+ end
20
+
21
+ module InstanceMethods
22
+ def basic_auth(user, password)
23
+ authorization(Authentication::Basic.new(user, password).authenticate)
24
+ end
25
+ end
26
+ end
27
+ register_plugin :basic_auth, BasicAuth
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Brotli
6
+ class Deflater < Transcoder::Deflater
7
+ def deflate(chunk)
8
+ return unless chunk
9
+
10
+ ::Brotli.deflate(chunk)
11
+ end
12
+ end
13
+
14
+ module RequestBodyClassMethods
15
+ def initialize_deflater_body(body, encoding)
16
+ return Brotli.encode(body) if encoding == "br"
17
+
18
+ super
19
+ end
20
+ end
21
+
22
+ module ResponseBodyClassMethods
23
+ def initialize_inflater_by_encoding(encoding, response, **kwargs)
24
+ return Brotli.decode(response, **kwargs) if encoding == "br"
25
+
26
+ super
27
+ end
28
+ end
29
+
30
+ module_function
31
+
32
+ def load_dependencies(*)
33
+ require "brotli"
34
+ end
35
+
36
+ def self.extra_options(options)
37
+ options.merge(supported_compression_formats: %w[br] + options.supported_compression_formats)
38
+ end
39
+
40
+ def encode(body)
41
+ Deflater.new(body)
42
+ end
43
+
44
+ def decode(_response, **)
45
+ ::Brotli.method(:inflate)
46
+ end
47
+ end
48
+ register_plugin :brotli, Brotli
49
+ end
50
+ end
@@ -0,0 +1,91 @@
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
+ # connection closed user-space errors happen after errors can be surfaced to requests,
12
+ # so they need to pierce through the scheduler, which is only possible by simulating an
13
+ # interrupt.
14
+ class CallbackError < Exception; end # rubocop:disable Lint/InheritException
15
+
16
+ module InstanceMethods
17
+ include HTTPX::Callbacks
18
+
19
+ %i[
20
+ connection_opened connection_closed
21
+ request_error
22
+ request_started request_body_chunk request_completed
23
+ response_started response_body_chunk response_completed
24
+ ].each do |meth|
25
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
26
+ def on_#{meth}(&blk) # def on_connection_opened(&blk)
27
+ on(:#{meth}, &blk) # on(:connection_opened, &blk)
28
+ end # end
29
+ MOD
30
+ end
31
+
32
+ private
33
+
34
+ def init_connection(uri, options)
35
+ connection = super
36
+ connection.on(:open) do
37
+ emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
38
+ end
39
+ connection.on(:close) do
40
+ emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
41
+ end
42
+
43
+ connection
44
+ end
45
+
46
+ def set_request_callbacks(request)
47
+ super
48
+
49
+ request.on(:headers) do
50
+ emit_or_callback_error(:request_started, request)
51
+ end
52
+ request.on(:body_chunk) do |chunk|
53
+ emit_or_callback_error(:request_body_chunk, request, chunk)
54
+ end
55
+ request.on(:done) do
56
+ emit_or_callback_error(:request_completed, request)
57
+ end
58
+
59
+ request.on(:response_started) do |res|
60
+ if res.is_a?(Response)
61
+ emit_or_callback_error(:response_started, request, res)
62
+ res.on(:chunk_received) do |chunk|
63
+ emit_or_callback_error(:response_body_chunk, request, res, chunk)
64
+ end
65
+ else
66
+ emit_or_callback_error(:request_error, request, res.error)
67
+ end
68
+ end
69
+ request.on(:response) do |res|
70
+ emit_or_callback_error(:response_completed, request, res)
71
+ end
72
+ end
73
+
74
+ def emit_or_callback_error(*args)
75
+ emit(*args)
76
+ rescue StandardError => e
77
+ ex = CallbackError.new(e.message)
78
+ ex.set_backtrace(e.backtrace)
79
+ raise ex
80
+ end
81
+
82
+ def receive_requests(*)
83
+ super
84
+ rescue CallbackError => e
85
+ raise e.cause
86
+ end
87
+ end
88
+ end
89
+ register_plugin :callbacks, Callbacks
90
+ end
91
+ 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,148 @@
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
+ def initialize_dup(orig)
36
+ super
37
+ @circuit_store = orig.instance_variable_get(:@circuit_store).dup
38
+ end
39
+
40
+ %i[circuit_open].each do |meth|
41
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
42
+ def on_#{meth}(&blk) # def on_circuit_open(&blk)
43
+ on(:#{meth}, &blk) # on(:circuit_open, &blk)
44
+ end # end
45
+ MOD
46
+ end
47
+
48
+ private
49
+
50
+ def send_requests(*requests)
51
+ # @type var short_circuit_responses: Array[response]
52
+ short_circuit_responses = []
53
+
54
+ # run all requests through the circuit breaker, see if the circuit is
55
+ # open for any of them.
56
+ real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
57
+ short_circuit_response = @circuit_store.try_respond(req)
58
+ if short_circuit_response.nil?
59
+ real_reqs << req
60
+ next
61
+ end
62
+ short_circuit_responses[idx] = short_circuit_response
63
+ end
64
+
65
+ # run requests for the remainder
66
+ unless real_requests.empty?
67
+ responses = super(*real_requests)
68
+
69
+ real_requests.each_with_index do |request, idx|
70
+ short_circuit_responses[requests.index(request)] = responses[idx]
71
+ end
72
+ end
73
+
74
+ short_circuit_responses
75
+ end
76
+
77
+ def on_response(request, response)
78
+ emit(:circuit_open, request) if try_circuit_open(request, response)
79
+
80
+ super
81
+ end
82
+
83
+ def try_circuit_open(request, response)
84
+ if response.is_a?(ErrorResponse)
85
+ case response.error
86
+ when RequestTimeoutError
87
+ @circuit_store.try_open(request.uri, response)
88
+ else
89
+ @circuit_store.try_open(request.origin, response)
90
+ end
91
+ elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
92
+ @circuit_store.try_open(request.uri, response)
93
+ else
94
+ @circuit_store.try_close(request.uri)
95
+ nil
96
+ end
97
+ end
98
+ end
99
+
100
+ # adds support for the following options:
101
+ #
102
+ # :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
103
+ # :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
104
+ # :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
105
+ # (i.e. <tt>->(res) { res.status == 429 } </tt>)
106
+ # :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
107
+ # (defaults to <tt><60</tt>).
108
+ # :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
109
+ # (defaults to <tt>1</tt>).
110
+ module OptionsMethods
111
+ def option_circuit_breaker_max_attempts(value)
112
+ attempts = Integer(value)
113
+ raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
114
+
115
+ attempts
116
+ end
117
+
118
+ def option_circuit_breaker_reset_attempts_in(value)
119
+ timeout = Float(value)
120
+ raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
121
+
122
+ timeout
123
+ end
124
+
125
+ def option_circuit_breaker_break_in(value)
126
+ timeout = Float(value)
127
+ raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
128
+
129
+ timeout
130
+ end
131
+
132
+ def option_circuit_breaker_half_open_drip_rate(value)
133
+ ratio = Float(value)
134
+ raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
135
+
136
+ ratio
137
+ end
138
+
139
+ def option_circuit_breaker_break_on(value)
140
+ raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
141
+
142
+ value
143
+ end
144
+ end
145
+ end
146
+ register_plugin :circuit_breaker, CircuitBreaker
147
+ end
148
+ end
@@ -6,8 +6,6 @@ require "time"
6
6
  module HTTPX
7
7
  module Plugins::Cookies
8
8
  module SetCookieParser
9
- using(RegexpExtensions) unless Regexp.method_defined?(:match?)
10
-
11
9
  # Whitespace.
12
10
  RE_WSP = /[ \t]+/.freeze
13
11
 
@@ -9,7 +9,7 @@ module HTTPX
9
9
  #
10
10
  # It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
11
11
  #
12
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Cookies
12
+ # https://gitlab.com/os85/httpx/wikis/Cookies
13
13
  #
14
14
  module Cookies
15
15
  def self.load_dependencies(*)
@@ -40,9 +40,15 @@ module HTTPX
40
40
  end
41
41
  end
42
42
 
43
+ def build_request(*)
44
+ request = super
45
+ request.headers.set_cookie(request.options.cookies[request.uri])
46
+ request
47
+ end
48
+
43
49
  private
44
50
 
45
- def on_response(reuest, response)
51
+ def on_response(_request, response)
46
52
  if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
47
53
 
48
54
  log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
@@ -52,12 +58,6 @@ module HTTPX
52
58
 
53
59
  super
54
60
  end
55
-
56
- def build_request(*, _)
57
- request = super
58
- request.headers.set_cookie(request.options.cookies[request.uri])
59
- request
60
- end
61
61
  end
62
62
 
63
63
  module HeadersMethods
@@ -70,23 +70,36 @@ module HTTPX
70
70
  end
71
71
  end
72
72
 
73
+ # adds support for the following options:
74
+ #
75
+ # :cookies :: cookie jar for the session (can be a Hash, an Array, an instance of HTTPX::Plugins::Cookies::CookieJar)
73
76
  module OptionsMethods
74
- def __initialize__(*)
75
- super
77
+ def option_headers(*)
78
+ value = super
76
79
 
77
- return unless @headers.key?("cookie")
80
+ merge_cookie_in_jar(value.delete("cookie"), @cookies) if defined?(@cookies) && value.key?("cookie")
78
81
 
79
- @headers.delete("cookie").each do |ck|
82
+ value
83
+ end
84
+
85
+ def option_cookies(value)
86
+ jar = value.is_a?(Jar) ? value : Jar.new(value)
87
+
88
+ merge_cookie_in_jar(@headers.delete("cookie"), jar) if defined?(@headers) && @headers.key?("cookie")
89
+
90
+ jar
91
+ end
92
+
93
+ private
94
+
95
+ def merge_cookie_in_jar(cookies, jar)
96
+ cookies.each do |ck|
80
97
  ck.split(/ *; */).each do |cookie|
81
98
  name, value = cookie.split("=", 2)
82
- @cookies.add(Cookie.new(name, value))
99
+ jar.add(Cookie.new(name, value))
83
100
  end
84
101
  end
85
102
  end
86
-
87
- def option_cookies(value)
88
- value.is_a?(Jar) ? value : Jar.new(value)
89
- end
90
103
  end
91
104
  end
92
105
  register_plugin :cookies, Cookies