httpx 0.21.0 → 1.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 (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