httpx 0.21.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (229) 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 +4 -4
  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_20_0.md +1 -1
  15. data/doc/release_notes/0_21_0.md +7 -5
  16. data/doc/release_notes/0_21_1.md +12 -0
  17. data/doc/release_notes/0_22_0.md +13 -0
  18. data/doc/release_notes/0_22_1.md +11 -0
  19. data/doc/release_notes/0_22_2.md +5 -0
  20. data/doc/release_notes/0_22_3.md +55 -0
  21. data/doc/release_notes/0_22_4.md +6 -0
  22. data/doc/release_notes/0_22_5.md +6 -0
  23. data/doc/release_notes/0_23_0.md +42 -0
  24. data/doc/release_notes/0_23_1.md +5 -0
  25. data/doc/release_notes/0_23_2.md +5 -0
  26. data/doc/release_notes/0_23_3.md +6 -0
  27. data/doc/release_notes/0_23_4.md +5 -0
  28. data/doc/release_notes/0_24_0.md +48 -0
  29. data/doc/release_notes/0_24_1.md +12 -0
  30. data/doc/release_notes/0_24_2.md +12 -0
  31. data/doc/release_notes/0_24_3.md +12 -0
  32. data/doc/release_notes/0_24_4.md +18 -0
  33. data/doc/release_notes/0_24_5.md +6 -0
  34. data/doc/release_notes/0_24_6.md +5 -0
  35. data/doc/release_notes/0_24_7.md +10 -0
  36. data/doc/release_notes/1_0_0.md +60 -0
  37. data/doc/release_notes/1_0_1.md +5 -0
  38. data/doc/release_notes/1_0_2.md +7 -0
  39. data/doc/release_notes/1_1_0.md +32 -0
  40. data/doc/release_notes/1_1_1.md +17 -0
  41. data/doc/release_notes/1_1_2.md +12 -0
  42. data/doc/release_notes/1_1_3.md +18 -0
  43. data/doc/release_notes/1_1_4.md +6 -0
  44. data/doc/release_notes/1_1_5.md +12 -0
  45. data/doc/release_notes/1_2_0.md +49 -0
  46. data/doc/release_notes/1_2_1.md +6 -0
  47. data/lib/httpx/adapters/datadog.rb +100 -106
  48. data/lib/httpx/adapters/faraday.rb +143 -107
  49. data/lib/httpx/adapters/sentry.rb +26 -7
  50. data/lib/httpx/adapters/webmock.rb +33 -17
  51. data/lib/httpx/altsvc.rb +61 -24
  52. data/lib/httpx/base64.rb +27 -0
  53. data/lib/httpx/buffer.rb +12 -0
  54. data/lib/httpx/callbacks.rb +5 -3
  55. data/lib/httpx/chainable.rb +54 -39
  56. data/lib/httpx/connection/http1.rb +62 -37
  57. data/lib/httpx/connection/http2.rb +16 -27
  58. data/lib/httpx/connection.rb +213 -120
  59. data/lib/httpx/domain_name.rb +10 -13
  60. data/lib/httpx/errors.rb +34 -2
  61. data/lib/httpx/extensions.rb +4 -134
  62. data/lib/httpx/io/ssl.rb +77 -71
  63. data/lib/httpx/io/tcp.rb +46 -70
  64. data/lib/httpx/io/udp.rb +18 -52
  65. data/lib/httpx/io/unix.rb +6 -13
  66. data/lib/httpx/io.rb +3 -9
  67. data/lib/httpx/loggable.rb +4 -19
  68. data/lib/httpx/options.rb +168 -110
  69. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  70. data/lib/httpx/plugins/{authentication → auth}/digest.rb +13 -14
  71. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  72. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  73. data/lib/httpx/plugins/auth.rb +25 -0
  74. data/lib/httpx/plugins/aws_sdk_authentication.rb +1 -3
  75. data/lib/httpx/plugins/aws_sigv4.rb +5 -6
  76. data/lib/httpx/plugins/basic_auth.rb +29 -0
  77. data/lib/httpx/plugins/brotli.rb +50 -0
  78. data/lib/httpx/plugins/callbacks.rb +91 -0
  79. data/lib/httpx/plugins/circuit_breaker/circuit.rb +40 -16
  80. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +14 -5
  81. data/lib/httpx/plugins/circuit_breaker.rb +30 -7
  82. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  83. data/lib/httpx/plugins/cookies.rb +20 -10
  84. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +11 -12
  85. data/lib/httpx/plugins/expect.rb +15 -13
  86. data/lib/httpx/plugins/follow_redirects.rb +71 -29
  87. data/lib/httpx/plugins/grpc/call.rb +2 -3
  88. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  89. data/lib/httpx/plugins/grpc/message.rb +7 -37
  90. data/lib/httpx/plugins/grpc.rb +35 -29
  91. data/lib/httpx/plugins/h2c.rb +25 -18
  92. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  93. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  94. data/lib/httpx/plugins/oauth.rb +170 -0
  95. data/lib/httpx/plugins/persistent.rb +1 -1
  96. data/lib/httpx/plugins/proxy/http.rb +15 -10
  97. data/lib/httpx/plugins/proxy/socks4.rb +8 -6
  98. data/lib/httpx/plugins/proxy/socks5.rb +10 -8
  99. data/lib/httpx/plugins/proxy.rb +69 -67
  100. data/lib/httpx/plugins/push_promise.rb +1 -1
  101. data/lib/httpx/plugins/rate_limiter.rb +3 -1
  102. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  103. data/lib/httpx/plugins/response_cache/store.rb +34 -17
  104. data/lib/httpx/plugins/response_cache.rb +6 -6
  105. data/lib/httpx/plugins/retries.rb +61 -12
  106. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  107. data/lib/httpx/plugins/stream.rb +27 -32
  108. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  109. data/lib/httpx/plugins/upgrade.rb +8 -10
  110. data/lib/httpx/plugins/webdav.rb +10 -8
  111. data/lib/httpx/pool.rb +85 -23
  112. data/lib/httpx/punycode.rb +9 -291
  113. data/lib/httpx/request/body.rb +158 -0
  114. data/lib/httpx/request.rb +86 -121
  115. data/lib/httpx/resolver/https.rb +54 -17
  116. data/lib/httpx/resolver/multi.rb +8 -12
  117. data/lib/httpx/resolver/native.rb +163 -70
  118. data/lib/httpx/resolver/resolver.rb +28 -13
  119. data/lib/httpx/resolver/system.rb +15 -10
  120. data/lib/httpx/resolver.rb +38 -16
  121. data/lib/httpx/response/body.rb +242 -0
  122. data/lib/httpx/response/buffer.rb +96 -0
  123. data/lib/httpx/response.rb +113 -211
  124. data/lib/httpx/selector.rb +2 -4
  125. data/lib/httpx/session.rb +91 -64
  126. data/lib/httpx/session_extensions.rb +4 -1
  127. data/lib/httpx/timers.rb +28 -8
  128. data/lib/httpx/transcoder/body.rb +0 -2
  129. data/lib/httpx/transcoder/chunker.rb +0 -1
  130. data/lib/httpx/transcoder/deflate.rb +37 -0
  131. data/lib/httpx/transcoder/form.rb +52 -33
  132. data/lib/httpx/transcoder/gzip.rb +74 -0
  133. data/lib/httpx/transcoder/json.rb +2 -5
  134. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  135. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +3 -3
  136. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  137. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  138. data/lib/httpx/transcoder/multipart.rb +17 -0
  139. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  140. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  141. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  142. data/lib/httpx/transcoder/xml.rb +0 -5
  143. data/lib/httpx/transcoder.rb +4 -6
  144. data/lib/httpx/utils.rb +36 -16
  145. data/lib/httpx/version.rb +1 -1
  146. data/lib/httpx.rb +12 -14
  147. data/sig/altsvc.rbs +33 -0
  148. data/sig/buffer.rbs +1 -0
  149. data/sig/callbacks.rbs +3 -3
  150. data/sig/chainable.rbs +10 -9
  151. data/sig/connection/http1.rbs +5 -4
  152. data/sig/connection/http2.rbs +1 -1
  153. data/sig/connection.rbs +46 -24
  154. data/sig/errors.rbs +9 -3
  155. data/sig/httpx.rbs +5 -4
  156. data/sig/io/ssl.rbs +26 -0
  157. data/sig/io/tcp.rbs +60 -0
  158. data/sig/io/udp.rbs +20 -0
  159. data/sig/io/unix.rbs +10 -0
  160. data/sig/options.rbs +28 -12
  161. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  162. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  163. data/sig/plugins/auth.rbs +13 -0
  164. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  165. data/sig/plugins/brotli.rbs +22 -0
  166. data/sig/plugins/callbacks.rbs +38 -0
  167. data/sig/plugins/circuit_breaker.rbs +13 -3
  168. data/sig/plugins/compression.rbs +6 -4
  169. data/sig/plugins/cookies/jar.rbs +2 -2
  170. data/sig/plugins/cookies.rbs +2 -0
  171. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  172. data/sig/plugins/follow_redirects.rbs +11 -2
  173. data/sig/plugins/grpc/call.rbs +19 -0
  174. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  175. data/sig/plugins/grpc/message.rbs +17 -0
  176. data/sig/plugins/grpc.rbs +2 -32
  177. data/sig/plugins/h2c.rbs +1 -1
  178. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  179. data/sig/plugins/oauth.rbs +54 -0
  180. data/sig/plugins/proxy/socks4.rbs +4 -4
  181. data/sig/plugins/proxy/socks5.rbs +2 -2
  182. data/sig/plugins/proxy/ssh.rbs +1 -1
  183. data/sig/plugins/proxy.rbs +10 -4
  184. data/sig/plugins/response_cache.rbs +12 -3
  185. data/sig/plugins/retries.rbs +28 -8
  186. data/sig/plugins/stream.rbs +24 -17
  187. data/sig/plugins/upgrade.rbs +5 -3
  188. data/sig/pool.rbs +5 -4
  189. data/sig/request/body.rbs +40 -0
  190. data/sig/request.rbs +12 -28
  191. data/sig/resolver/https.rbs +7 -2
  192. data/sig/resolver/native.rbs +10 -4
  193. data/sig/resolver/resolver.rbs +6 -4
  194. data/sig/resolver/system.rbs +2 -0
  195. data/sig/resolver.rbs +9 -5
  196. data/sig/response/body.rbs +53 -0
  197. data/sig/response/buffer.rbs +24 -0
  198. data/sig/response.rbs +17 -38
  199. data/sig/session.rbs +24 -18
  200. data/sig/timers.rbs +17 -7
  201. data/sig/transcoder/body.rbs +4 -3
  202. data/sig/transcoder/deflate.rbs +11 -0
  203. data/sig/transcoder/form.rbs +5 -3
  204. data/sig/transcoder/gzip.rbs +24 -0
  205. data/sig/transcoder/json.rbs +4 -2
  206. data/sig/{plugins → transcoder}/multipart.rbs +3 -12
  207. data/sig/transcoder/utils/body_reader.rbs +15 -0
  208. data/sig/transcoder/utils/deflater.rbs +29 -0
  209. data/sig/transcoder/utils/inflater.rbs +12 -0
  210. data/sig/transcoder/xml.rbs +1 -1
  211. data/sig/transcoder.rbs +22 -7
  212. data/sig/utils.rbs +2 -0
  213. metadata +127 -40
  214. data/lib/httpx/plugins/authentication.rb +0 -20
  215. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  216. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  217. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  218. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  219. data/lib/httpx/plugins/compression.rb +0 -164
  220. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  221. data/lib/httpx/plugins/multipart.rb +0 -84
  222. data/lib/httpx/registry.rb +0 -85
  223. data/sig/plugins/authentication.rbs +0 -11
  224. data/sig/plugins/compression/brotli.rbs +0 -21
  225. data/sig/plugins/compression/deflate.rbs +0 -17
  226. data/sig/plugins/compression/gzip.rbs +0 -29
  227. data/sig/registry.rbs +0 -13
  228. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  229. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -8,12 +8,11 @@ module HTTPX
8
8
  module Plugins
9
9
  module Authentication
10
10
  class Digest
11
- using RegexpExtensions unless Regexp.method_defined?(:match?)
12
-
13
- def initialize(user, password, **)
11
+ def initialize(user, password, hashed: false, **)
14
12
  @user = user
15
13
  @password = password
16
14
  @nonce = 0
15
+ @hashed = hashed
17
16
  end
18
17
 
19
18
  def can_authenticate?(authenticate)
@@ -21,7 +20,7 @@ module HTTPX
21
20
  end
22
21
 
23
22
  def authenticate(request, authenticate)
24
- "Digest #{generate_header(request.verb.to_s.upcase, request.path, authenticate)}"
23
+ "Digest #{generate_header(request.verb, request.path, authenticate)}"
25
24
  end
26
25
 
27
26
  private
@@ -30,9 +29,8 @@ module HTTPX
30
29
  # discard first token, it's Digest
31
30
  auth_info = authenticate[/^(\w+) (.*)/, 2]
32
31
 
33
- params = Hash[auth_info.split(/ *, */)
34
- .map { |val| val.split("=") }
35
- .map { |k, v| [k, v.delete("\"")] }]
32
+ params = auth_info.split(/ *, */)
33
+ .to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
36
34
  nonce = params["nonce"]
37
35
  nc = next_nonce
38
36
 
@@ -45,7 +43,6 @@ module HTTPX
45
43
  raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
46
44
 
47
45
  sess = Regexp.last_match(2)
48
- params.delete("algorithm")
49
46
  else
50
47
  algorithm = ::Digest::MD5
51
48
  end
@@ -56,11 +53,13 @@ module HTTPX
56
53
  end
57
54
 
58
55
  a1 = if sess
59
- [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
60
- nonce,
61
- cnonce].join ":"
56
+ [
57
+ (@hashed ? @password : algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}")),
58
+ nonce,
59
+ cnonce,
60
+ ].join ":"
62
61
  else
63
- "#{@user}:#{params["realm"]}:#{@password}"
62
+ @hashed ? @password : "#{@user}:#{params["realm"]}:#{@password}"
64
63
  end
65
64
 
66
65
  ha1 = algorithm.hexdigest(a1)
@@ -77,11 +76,11 @@ module HTTPX
77
76
  %(response="#{algorithm.hexdigest(request_digest)}"),
78
77
  ]
79
78
  header << %(realm="#{params["realm"]}") if params.key?("realm")
80
- header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
81
- header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
79
+ header << %(algorithm=#{params["algorithm"]}) if params.key?("algorithm")
82
80
  header << %(cnonce="#{cnonce}") if cnonce
83
81
  header << %(nc=#{nc})
84
82
  header << %(qop=#{qop}) if qop
83
+ header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
85
84
  header.join ", "
86
85
  end
87
86
 
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
3
+ require "httpx/base64"
4
4
  require "ntlm"
5
5
 
6
6
  module HTTPX
7
7
  module Plugins
8
8
  module Authentication
9
9
  class Ntlm
10
- using RegexpExtensions unless Regexp.method_defined?(:match?)
11
-
12
10
  def initialize(user, password, domain: nil)
13
11
  @user = user
14
12
  @password = password
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
-
5
3
  module HTTPX
6
4
  module Plugins
7
5
  module Authentication
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds a shim +authorization+ method to the session, which will fill
7
+ # the HTTP Authorization header, and another, +bearer_auth+, which fill the "Bearer " prefix
8
+ # in its value.
9
+ #
10
+ # https://gitlab.com/os85/httpx/wikis/Auth#auth
11
+ #
12
+ module Auth
13
+ module InstanceMethods
14
+ def authorization(token)
15
+ with(headers: { "authorization" => token })
16
+ end
17
+
18
+ def bearer_auth(token)
19
+ authorization("Bearer #{token}")
20
+ end
21
+ end
22
+ end
23
+ register_plugin :auth, Auth
24
+ end
25
+ end
@@ -20,9 +20,7 @@ module HTTPX
20
20
  true
21
21
  end
22
22
 
23
- def method_missing(*)
24
- nil
25
- end
23
+ def method_missing(*); end
26
24
  end
27
25
 
28
26
  #
@@ -5,9 +5,9 @@ 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)
@@ -71,7 +71,7 @@ module HTTPX
71
71
  end.join
72
72
 
73
73
  # canonical request
74
- creq = "#{request.verb.to_s.upcase}" \
74
+ creq = "#{request.verb}" \
75
75
  "\n#{request.canonical_path}" \
76
76
  "\n#{request.canonical_query}" \
77
77
  "\n#{canonical_headers}" \
@@ -115,7 +115,7 @@ module HTTPX
115
115
  elsif value.respond_to?(:each)
116
116
  digest = OpenSSL::Digest.new(@algorithm)
117
117
 
118
- mb_buffer = value.each.each_with_object("".b) do |chunk, buffer|
118
+ mb_buffer = value.each.with_object("".b) do |chunk, buffer|
119
119
  buffer << chunk
120
120
  break if buffer.bytesize >= 1024 * 1024
121
121
  end
@@ -146,7 +146,6 @@ module HTTPX
146
146
 
147
147
  def configure(klass)
148
148
  klass.plugin(:expect)
149
- klass.plugin(:compression)
150
149
  end
151
150
  end
152
151
 
@@ -186,7 +185,7 @@ module HTTPX
186
185
  def canonical_query
187
186
  params = query.split("&")
188
187
  # params = params.map { |p| p.match(/=/) ? p : p + '=' }
189
- # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
188
+ # From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
190
189
  # Sort the parameter names by character code point in ascending order.
191
190
  # Parameters with duplicate names should be sorted by value.
192
191
  #
@@ -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
@@ -15,8 +15,11 @@ module HTTPX
15
15
  @max_attempts = max_attempts
16
16
  @reset_attempts_in = reset_attempts_in
17
17
  @break_in = break_in
18
- @circuit_breaker_half_open_drip_rate = 1 - circuit_breaker_half_open_drip_rate
18
+ @circuit_breaker_half_open_drip_rate = circuit_breaker_half_open_drip_rate
19
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
20
23
  @state = :closed
21
24
  end
22
25
 
@@ -27,8 +30,13 @@ module HTTPX
27
30
  when :closed
28
31
  nil
29
32
  when :half_open
30
- # return nothing or smth based on ratio
31
- return if Random::DEFAULT.rand >= @circuit_breaker_half_open_drip_rate
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
32
40
 
33
41
  @response
34
42
  when :open
@@ -38,23 +46,31 @@ module HTTPX
38
46
  end
39
47
 
40
48
  def try_open(response)
41
- return unless @state == :closed
49
+ case @state
50
+ when :closed
51
+ now = Utils.now
42
52
 
43
- now = Utils.now
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
44
59
 
45
- if @attempts.positive?
46
- @attempts = 0 if now - @attempted_at > @reset_attempts_in
47
- else
48
- @attempted_at = now
49
- end
60
+ @attempts += 1
50
61
 
51
- @attempts += 1
62
+ return unless @attempts >= @max_attempts
52
63
 
53
- return unless @attempts >= @max_attempts
64
+ @state = :open
65
+ @opened_at = now
66
+ @response = response
67
+ when :half_open
68
+ # open immediately
54
69
 
55
- @state = :open
56
- @opened_at = now
57
- @response = response
70
+ @state = :open
71
+ @attempted_at = @opened_at = Utils.now
72
+ @response = response
73
+ end
58
74
  end
59
75
 
60
76
  def try_close
@@ -62,13 +78,21 @@ module HTTPX
62
78
  when :closed
63
79
  nil
64
80
  when :half_open
81
+
82
+ # do not close circuit unless attempts exhausted
83
+ return unless @attempts >= @max_attempts
84
+
65
85
  # reset!
66
86
  @attempts = 0
67
87
  @opened_at = @attempted_at = @response = nil
68
88
  @state = :closed
69
89
 
70
90
  when :open
71
- @state = :half_open if Utils.elapsed_time(@opened_at) > @break_in
91
+ if Utils.elapsed_time(@opened_at) > @break_in
92
+ @state = :half_open
93
+ @attempts = 0
94
+ @real_attempts = 0
95
+ end
72
96
  end
73
97
  end
74
98
  end
@@ -13,18 +13,29 @@ module HTTPX::Plugins::CircuitBreaker
13
13
  options.circuit_breaker_half_open_drip_rate
14
14
  )
15
15
  end
16
+ @circuits_mutex = Thread::Mutex.new
16
17
  end
17
18
 
18
19
  def try_open(uri, response)
19
- circuit = get_circuit_for_uri(uri)
20
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(uri) }
20
21
 
21
22
  circuit.try_open(response)
22
23
  end
23
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
+
24
35
  # if circuit is open, it'll respond with the stored response.
25
36
  # if not, nil.
26
37
  def try_respond(request)
27
- circuit = get_circuit_for_uri(request.uri)
38
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(request.uri) }
28
39
 
29
40
  circuit.respond
30
41
  end
@@ -32,9 +43,7 @@ module HTTPX::Plugins::CircuitBreaker
32
43
  private
33
44
 
34
45
  def get_circuit_for_uri(uri)
35
- uri = URI(uri)
36
-
37
- if @circuits.key?(uri.origin)
46
+ if uri.respond_to?(:origin) && @circuits.key?(uri.origin)
38
47
  @circuits[uri.origin]
39
48
  else
40
49
  @circuits[uri.to_s]
@@ -5,7 +5,7 @@ module HTTPX
5
5
  #
6
6
  # This plugin implements a circuit breaker around connection errors.
7
7
  #
8
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Circuit-Breaker
8
+ # https://gitlab.com/os85/httpx/wikis/Circuit-Breaker
9
9
  #
10
10
  module CircuitBreaker
11
11
  using URIExtensions
@@ -16,11 +16,17 @@ module HTTPX
16
16
  end
17
17
 
18
18
  def self.extra_options(options)
19
- options.merge(circuit_breaker_max_attempts: 3, circuit_breaker_reset_attempts_in: 60, circuit_breaker_break_in: 60,
20
- circuit_breaker_half_open_drip_rate: 1)
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
+ )
21
25
  end
22
26
 
23
27
  module InstanceMethods
28
+ include HTTPX::Callbacks
29
+
24
30
  def initialize(*)
25
31
  super
26
32
  @circuit_store = CircuitStore.new(@options)
@@ -31,19 +37,29 @@ module HTTPX
31
37
  @circuit_store = orig.instance_variable_get(:@circuit_store).dup
32
38
  end
33
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
+
34
50
  def send_requests(*requests)
35
51
  # @type var short_circuit_responses: Array[response]
36
52
  short_circuit_responses = []
37
53
 
38
54
  # run all requests through the circuit breaker, see if the circuit is
39
55
  # open for any of them.
40
- real_requests = requests.each_with_object([]) do |req, real_reqs|
56
+ real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
41
57
  short_circuit_response = @circuit_store.try_respond(req)
42
58
  if short_circuit_response.nil?
43
59
  real_reqs << req
44
60
  next
45
61
  end
46
- short_circuit_responses[requests.index(req)] = short_circuit_response
62
+ short_circuit_responses[idx] = short_circuit_response
47
63
  end
48
64
 
49
65
  # run requests for the remainder
@@ -59,6 +75,12 @@ module HTTPX
59
75
  end
60
76
 
61
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)
62
84
  if response.is_a?(ErrorResponse)
63
85
  case response.error
64
86
  when RequestTimeoutError
@@ -68,9 +90,10 @@ module HTTPX
68
90
  end
69
91
  elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
70
92
  @circuit_store.try_open(request.uri, response)
93
+ else
94
+ @circuit_store.try_close(request.uri)
95
+ nil
71
96
  end
72
-
73
- super
74
97
  end
75
98
  end
76
99
 
@@ -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(*)
@@ -71,22 +71,32 @@ module HTTPX
71
71
  end
72
72
 
73
73
  module OptionsMethods
74
- def __initialize__(*)
75
- super
74
+ def option_headers(*)
75
+ value = super
76
+
77
+ merge_cookie_in_jar(value.delete("cookie"), @cookies) if defined?(@cookies) && value.key?("cookie")
76
78
 
77
- return unless @headers.key?("cookie")
79
+ value
80
+ end
78
81
 
79
- @headers.delete("cookie").each do |ck|
82
+ def option_cookies(value)
83
+ jar = value.is_a?(Jar) ? value : Jar.new(value)
84
+
85
+ merge_cookie_in_jar(@headers.delete("cookie"), jar) if defined?(@headers) && @headers.key?("cookie")
86
+
87
+ jar
88
+ end
89
+
90
+ private
91
+
92
+ def merge_cookie_in_jar(cookies, jar)
93
+ cookies.each do |ck|
80
94
  ck.split(/ *; */).each do |cookie|
81
95
  name, value = cookie.split("=", 2)
82
- @cookies.add(Cookie.new(name, value))
96
+ jar.add(Cookie.new(name, value))
83
97
  end
84
98
  end
85
99
  end
86
-
87
- def option_cookies(value)
88
- value.is_a?(Jar) ? value : Jar.new(value)
89
- end
90
100
  end
91
101
  end
92
102
  register_plugin :cookies, Cookies