httpx 0.20.5 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_13_0.md +1 -1
  3. data/doc/release_notes/0_21_0.md +94 -0
  4. data/lib/httpx/connection/http1.rb +2 -1
  5. data/lib/httpx/connection.rb +41 -2
  6. data/lib/httpx/errors.rb +18 -0
  7. data/lib/httpx/extensions.rb +8 -4
  8. data/lib/httpx/io/unix.rb +1 -1
  9. data/lib/httpx/options.rb +7 -3
  10. data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
  11. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
  12. data/lib/httpx/plugins/circuit_breaker.rb +115 -0
  13. data/lib/httpx/plugins/cookies.rb +1 -1
  14. data/lib/httpx/plugins/expect.rb +1 -1
  15. data/lib/httpx/plugins/multipart/decoder.rb +1 -1
  16. data/lib/httpx/plugins/proxy.rb +7 -1
  17. data/lib/httpx/plugins/retries.rb +1 -1
  18. data/lib/httpx/plugins/webdav.rb +78 -0
  19. data/lib/httpx/request.rb +15 -25
  20. data/lib/httpx/resolver/https.rb +2 -7
  21. data/lib/httpx/resolver/native.rb +2 -1
  22. data/lib/httpx/response.rb +27 -9
  23. data/lib/httpx/timers.rb +3 -0
  24. data/lib/httpx/transcoder/form.rb +1 -1
  25. data/lib/httpx/transcoder/json.rb +19 -3
  26. data/lib/httpx/transcoder/xml.rb +57 -0
  27. data/lib/httpx/transcoder.rb +1 -0
  28. data/lib/httpx/version.rb +1 -1
  29. data/sig/buffer.rbs +1 -1
  30. data/sig/chainable.rbs +1 -0
  31. data/sig/connection.rbs +12 -4
  32. data/sig/errors.rbs +13 -0
  33. data/sig/io.rbs +6 -0
  34. data/sig/options.rbs +4 -1
  35. data/sig/plugins/circuit_breaker.rbs +61 -0
  36. data/sig/plugins/compression/brotli.rbs +1 -1
  37. data/sig/plugins/compression/deflate.rbs +1 -1
  38. data/sig/plugins/compression/gzip.rbs +3 -3
  39. data/sig/plugins/compression.rbs +1 -1
  40. data/sig/plugins/multipart.rbs +1 -1
  41. data/sig/plugins/proxy/socks5.rbs +3 -2
  42. data/sig/plugins/proxy.rbs +1 -1
  43. data/sig/registry.rbs +5 -4
  44. data/sig/request.rbs +7 -1
  45. data/sig/resolver/native.rbs +5 -2
  46. data/sig/response.rbs +3 -1
  47. data/sig/timers.rbs +1 -1
  48. data/sig/transcoder/json.rbs +4 -1
  49. data/sig/transcoder/xml.rbs +21 -0
  50. data/sig/transcoder.rbs +2 -2
  51. data/sig/utils.rbs +2 -2
  52. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47766fc5b4fb9cb70e7b0feb8333656b987a25aea262c645c36de53e7cc3f464
4
- data.tar.gz: 2f49417d21841b803d9f44b76c199785a54b7983c7712cf8a39b4837900e892b
3
+ metadata.gz: c62cb1d027ee7e62770de459b00411614372578117ea56354df0d9114cf4397b
4
+ data.tar.gz: 18275725fc8adeac596f02f83f034dfcfe24a7e4c96ec34bc11400e8ca0e9567
5
5
  SHA512:
6
- metadata.gz: f74fd61a16e738fbe668312e14d7c658af062f4f3dbcf65360b33892e27f31a524e7b675136de5e7693e327ba9a388c74c995e43b4e49a4593ec63fa385d07df
7
- data.tar.gz: b04ae701b05663bd8c94d74416fd6a124d3a700e5860065d373f452383cc6f48a5569e8696dfac5a7f398824c163b3ddb62553a56c465022dc35ac34e627a479
6
+ metadata.gz: 840bf59a4cbbb45d26a836ccd4060007470d380314b4aedea58e49cb434b050a8252f7db3823daf9c2dfdb5d54075215e4ee1216c80852afbb7abfd57e1dfea2
7
+ data.tar.gz: 35a666b92236240e8ccd09b1dc29a2ccf83061222b0511d35eee6fb7f3794dc737ca86ac9ca7d87ba01e03d8e648b07e6e52290b02b25674bd783b7f9d2cfbce
@@ -34,7 +34,7 @@ HTTPX.get("http://example.com", addresses: %w[172.5.3.1 172.5.3.2]))
34
34
  You should also use it to connect to HTTP servers bound to a UNIX socket, in which case you'll have to provide a path:
35
35
 
36
36
  ```ruby
37
- HTTPX.get("http://example.com", addresses: %w[/path/to/usocket]))
37
+ HTTPX.get("http://example.com", transport: "unix", addresses: %w[/path/to/usocket]))
38
38
  ```
39
39
 
40
40
  The `:transport_options` are therefore deprecated, and will be moved in a major version.
@@ -0,0 +1,94 @@
1
+ # 0.21.0
2
+
3
+ ## Features
4
+
5
+ ### `:write_timeout`, `:read_timeout` and `:request_timeout`
6
+
7
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts
8
+
9
+ The following timeouts are now supported:
10
+
11
+ * `:write_timeout`: total time (in seconds) to write a request to the server;
12
+ * `:read_timeout`: total time (in seconds) to read aa response from the server;
13
+ * `:request_timeout`: tracks both of the above (time to write the request and read a response);
14
+
15
+ ```ruby
16
+ HTTPX.with(timeout: { request_timeout: 60}).get(...
17
+ ```
18
+
19
+ Just like `:connect_timeout`, the new timeouts are deadline-oriented, rather than op-oriented, meaning that they do not reset on each socket operation (as most ruby HTTP clients do).
20
+
21
+ None of them has a default value, in order not to break integrations, but that'll change in a future v1, where they'll become the default timeouts.
22
+
23
+ ### Circuit Breaker plugin
24
+
25
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Circuit-Breaker
26
+
27
+ The `:circuit_breaker` plugin wraps around errors happening when performing HTTP requests, and support options for setting maximum number of attempts before circuit opens (`:circuit_breaker_max_attempts`), period after which attempts should be reset (`:circuit_breaker_reset_attempts_in`), timespan until circuit half-opens (`circuit_breaker_break_in`), respective half-open drip rate (`:circuit_breaker_half_open_drip_rate`), and a callback to do your own check on whether a response has failed, in case you want HTTP level errors to be marked as failed attempts (`:circuit_breaker_break_on`).
28
+
29
+ ```ruby
30
+ http = HTTPX.plugin(:circuit_breaker)
31
+ # that's it!
32
+ http.get(...
33
+ ```
34
+
35
+ ### WebDAV plugin
36
+
37
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/WebDav
38
+
39
+ The `:webdav` introduces some "convenience" methods to perform common WebDAV operations.
40
+
41
+ ```ruby
42
+ webdav = HTTPX.plugin(:webdav, origin: "http://webdav-server")
43
+ .plugin(:digest_authentication).digest_auth("user", "pass")
44
+
45
+ res = webdav.put("/file.html", body: "this is the file body")
46
+ res = webdav.copy("/file.html", "/newdir/copy.html")
47
+ # ...
48
+ ```
49
+
50
+ ### XML transcoder, `:xml` option and `response.xml`
51
+
52
+ A new transcoder was added fot the XML mime type, which requires `"nokogiri"` to be installed; it can both serialize Nokogiri nodes in a request, and parse response content into nokogiri nodes:
53
+
54
+ ```ruby
55
+ response = HTTPX.post("https://xml-server.com", xml: Nokogiri::XML("<xml ..."))
56
+ response.xml #=> #(Document:0x16e4 { name = "document", children = ...
57
+ ```
58
+
59
+ ## Improvements
60
+
61
+ ### `:proxy` plugin: `:no_proxy` option
62
+
63
+ Support was added, in the `:proxy` plugin, to declare domains, either via regexp patterns, or strings, for which requests should bypass the proxy.
64
+
65
+ ```ruby
66
+ http = HTTPX.plugin(:proxy).with_proxy(
67
+ uri: "http://10.10.0.1:51432",
68
+ no_proxy: ["gitlab.local", /*.google.com/]
69
+ )
70
+ http.get("https://duckduckgo.com/?q=httpx") #=> proxied
71
+ http.get("https://google.com/?q=httpx") #=> not proxied
72
+ http.get("https://gitlab.com") #=> proxied
73
+ http.get("https://gitlab.local") #=> not proxied
74
+ ```
75
+
76
+ ### OOTB support for other JSON libraries
77
+
78
+ If one of `multi_json`, `oj` or `yajl` is available, all `httpx` operations doing JSON parsing or dumping will use it (the `json` standard library will be used otherwise).
79
+
80
+ ```ruby
81
+ require "oj"
82
+ require "httpx"
83
+
84
+ response = HTTPX.post("https://somedomain.json", json: { "foo" => "bar" }) # will use "oj"
85
+ puts response.json # will use "oj"
86
+ ```
87
+
88
+ ## Bugfixes
89
+
90
+ * `:expect` plugin: `:expect_timeout` can accept floats (not just integers).
91
+
92
+ ## Chore
93
+
94
+ * DoH `:https` resolver: support was removed for the "application/dns-json" mime-type (it was only supported in practice by the Google DoH resolver, which has since added support for the standardized "application/dns-message").
@@ -118,7 +118,7 @@ module HTTPX
118
118
  log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
119
119
 
120
120
  @request.response = response
121
- on_complete if response.complete?
121
+ on_complete if response.finished?
122
122
  end
123
123
 
124
124
  def on_trailers(h)
@@ -158,6 +158,7 @@ module HTTPX
158
158
  @request = nil
159
159
  @requests.shift
160
160
  response = request.response
161
+ response.finish!
161
162
  emit(:response, request, response)
162
163
 
163
164
  if @parser.upgrade?
@@ -34,6 +34,7 @@ module HTTPX
34
34
  include Callbacks
35
35
 
36
36
  using URIExtensions
37
+ using NumericExtensions
37
38
 
38
39
  require "httpx/connection/http2"
39
40
  require "httpx/connection/http1"
@@ -233,6 +234,7 @@ module HTTPX
233
234
  # when pushing a request into an existing connection, we have to check whether there
234
235
  # is the possibility that the connection might have extended the keep alive timeout.
235
236
  # for such cases, we want to ping for availability before deciding to shovel requests.
237
+ log(level: 3) { "keep alive timeout expired, pinging connection..." }
236
238
  @pending << request
237
239
  parser.ping
238
240
  transition(:active) if @state == :inactive
@@ -430,6 +432,8 @@ module HTTPX
430
432
  @inflight += 1
431
433
  parser.send(request)
432
434
 
435
+ set_request_timeouts(request)
436
+
433
437
  return unless @state == :inactive
434
438
 
435
439
  transition(:active)
@@ -573,7 +577,7 @@ module HTTPX
573
577
  error = ex
574
578
  else
575
579
  # inactive connections do not contribute to the select loop, therefore
576
- # they should fail due to such errors.
580
+ # they should not fail due to such errors.
577
581
  return if @state == :inactive
578
582
 
579
583
  if @timeout
@@ -591,10 +595,45 @@ module HTTPX
591
595
  def handle_error(error)
592
596
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
593
597
  while (request = @pending.shift)
594
- response = ErrorResponse.new(request, error, @options)
598
+ response = ErrorResponse.new(request, error, request.options)
595
599
  request.response = response
596
600
  request.emit(:response, response)
597
601
  end
598
602
  end
603
+
604
+ def set_request_timeouts(request)
605
+ write_timeout = request.write_timeout
606
+ request.once(:headers) do
607
+ @timers.after(write_timeout) { write_timeout_callback(request, write_timeout) }
608
+ end unless write_timeout.nil? || write_timeout.infinite?
609
+
610
+ read_timeout = request.read_timeout
611
+ request.once(:done) do
612
+ @timers.after(read_timeout) { read_timeout_callback(request, read_timeout) }
613
+ end unless read_timeout.nil? || read_timeout.infinite?
614
+
615
+ request_timeout = request.request_timeout
616
+ request.once(:headers) do
617
+ @timers.after(request_timeout) { read_timeout_callback(request, request_timeout, RequestTimeoutError) }
618
+ end unless request_timeout.nil? || request_timeout.infinite?
619
+ end
620
+
621
+ def write_timeout_callback(request, write_timeout)
622
+ return if request.state == :done
623
+
624
+ @write_buffer.clear
625
+ error = WriteTimeoutError.new(request, nil, write_timeout)
626
+ on_error(error)
627
+ end
628
+
629
+ def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
630
+ response = request.response
631
+
632
+ return if response && response.finished?
633
+
634
+ @write_buffer.clear
635
+ error = error_type.new(request, request.response, read_timeout)
636
+ on_error(error)
637
+ end
599
638
  end
600
639
  end
data/lib/httpx/errors.rb CHANGED
@@ -24,6 +24,24 @@ module HTTPX
24
24
 
25
25
  class ConnectTimeoutError < TimeoutError; end
26
26
 
27
+ class RequestTimeoutError < TimeoutError
28
+ attr_reader :request
29
+
30
+ def initialize(request, response, timeout)
31
+ @request = request
32
+ @response = response
33
+ super(timeout, "Timed out after #{timeout} seconds")
34
+ end
35
+
36
+ def marshal_dump
37
+ [message]
38
+ end
39
+ end
40
+
41
+ class ReadTimeoutError < RequestTimeoutError; end
42
+
43
+ class WriteTimeoutError < RequestTimeoutError; end
44
+
27
45
  class SettingsTimeoutError < TimeoutError; end
28
46
 
29
47
  class ResolveTimeoutError < TimeoutError; end
@@ -54,6 +54,14 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module NumericExtensions
58
+ refine Numeric do
59
+ def infinite?
60
+ self == Float::INFINITY
61
+ end unless Numeric.method_defined?(:infinite?)
62
+ end
63
+ end
64
+
57
65
  module StringExtensions
58
66
  refine String do
59
67
  def delete_suffix!(suffix)
@@ -135,10 +143,6 @@ module HTTPX
135
143
  end
136
144
 
137
145
  module RegexpExtensions
138
- # If you wonder why this is there: the oauth feature uses a refinement to enhance the
139
- # Regexp class locally with #match? , but this is never tested, because ActiveSupport
140
- # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
141
- # :nocov:
142
146
  refine(Regexp) do
143
147
  def match?(*args)
144
148
  !match(*args).nil?
data/lib/httpx/io/unix.rb CHANGED
@@ -33,7 +33,7 @@ module HTTPX
33
33
  else
34
34
  if @options.transport_options
35
35
  # :nocov:
36
- warn ":#{__method__} is deprecated, use :addresses instead"
36
+ warn ":transport_options is deprecated, use :addresses instead"
37
37
  @path = @options.transport_options[:path]
38
38
  # :nocov:
39
39
  else
data/lib/httpx/options.rb CHANGED
@@ -10,6 +10,7 @@ module HTTPX
10
10
  OPERATION_TIMEOUT = 60
11
11
  KEEP_ALIVE_TIMEOUT = 20
12
12
  SETTINGS_TIMEOUT = 10
13
+ READ_TIMEOUT = WRITE_TIMEOUT = REQUEST_TIMEOUT = Float::INFINITY
13
14
 
14
15
  # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
15
16
  ip_address_families = begin
@@ -34,6 +35,9 @@ module HTTPX
34
35
  settings_timeout: SETTINGS_TIMEOUT,
35
36
  operation_timeout: OPERATION_TIMEOUT,
36
37
  keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
38
+ read_timeout: READ_TIMEOUT,
39
+ write_timeout: WRITE_TIMEOUT,
40
+ request_timeout: REQUEST_TIMEOUT,
37
41
  },
38
42
  :headers => {},
39
43
  :window_size => WINDOW_SIZE,
@@ -197,7 +201,7 @@ module HTTPX
197
201
  end
198
202
 
199
203
  %i[
200
- params form json body ssl http2_settings
204
+ params form json xml body ssl http2_settings
201
205
  request_class response_class headers_class request_body_class
202
206
  response_body_class connection_class options_class
203
207
  io fallback_protocol debug debug_level transport_options resolver_class resolver_options
@@ -206,7 +210,7 @@ module HTTPX
206
210
  def_option(method_name)
207
211
  end
208
212
 
209
- REQUEST_IVARS = %i[@params @form @json @body].freeze
213
+ REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
210
214
  private_constant :REQUEST_IVARS
211
215
 
212
216
  def ==(other)
@@ -263,7 +267,7 @@ module HTTPX
263
267
  instance_variables.each do |ivar|
264
268
  value = other.instance_variable_get(ivar)
265
269
  value = case value
266
- when Symbol, Fixnum, TrueClass, FalseClass # rubocop:disable Lint/UnifiedInteger
270
+ when Symbol, Numeric, TrueClass, FalseClass
267
271
  value
268
272
  else
269
273
  value.dup
@@ -0,0 +1,76 @@
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 = 1 - circuit_breaker_half_open_drip_rate
19
+ @attempts = 0
20
+ @state = :closed
21
+ end
22
+
23
+ def respond
24
+ try_close
25
+
26
+ case @state
27
+ when :closed
28
+ nil
29
+ when :half_open
30
+ # return nothing or smth based on ratio
31
+ return if Random::DEFAULT.rand >= @circuit_breaker_half_open_drip_rate
32
+
33
+ @response
34
+ when :open
35
+
36
+ @response
37
+ end
38
+ end
39
+
40
+ def try_open(response)
41
+ return unless @state == :closed
42
+
43
+ now = Utils.now
44
+
45
+ if @attempts.positive?
46
+ @attempts = 0 if now - @attempted_at > @reset_attempts_in
47
+ else
48
+ @attempted_at = now
49
+ end
50
+
51
+ @attempts += 1
52
+
53
+ return unless @attempts >= @max_attempts
54
+
55
+ @state = :open
56
+ @opened_at = now
57
+ @response = response
58
+ end
59
+
60
+ def try_close
61
+ case @state
62
+ when :closed
63
+ nil
64
+ when :half_open
65
+ # reset!
66
+ @attempts = 0
67
+ @opened_at = @attempted_at = @response = nil
68
+ @state = :closed
69
+
70
+ when :open
71
+ @state = :half_open if Utils.elapsed_time(@opened_at) > @break_in
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
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
+ end
17
+
18
+ def try_open(uri, response)
19
+ circuit = get_circuit_for_uri(uri)
20
+
21
+ circuit.try_open(response)
22
+ end
23
+
24
+ # if circuit is open, it'll respond with the stored response.
25
+ # if not, nil.
26
+ def try_respond(request)
27
+ circuit = get_circuit_for_uri(request.uri)
28
+
29
+ circuit.respond
30
+ end
31
+
32
+ private
33
+
34
+ def get_circuit_for_uri(uri)
35
+ uri = URI(uri)
36
+
37
+ if @circuits.key?(uri.origin)
38
+ @circuits[uri.origin]
39
+ else
40
+ @circuits[uri.to_s]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,115 @@
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/honeyryderchuck/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(circuit_breaker_max_attempts: 3, circuit_breaker_reset_attempts_in: 60, circuit_breaker_break_in: 60,
20
+ circuit_breaker_half_open_drip_rate: 1)
21
+ end
22
+
23
+ module InstanceMethods
24
+ def initialize(*)
25
+ super
26
+ @circuit_store = CircuitStore.new(@options)
27
+ end
28
+
29
+ def initialize_dup(orig)
30
+ super
31
+ @circuit_store = orig.instance_variable_get(:@circuit_store).dup
32
+ end
33
+
34
+ def send_requests(*requests)
35
+ # @type var short_circuit_responses: Array[response]
36
+ short_circuit_responses = []
37
+
38
+ # run all requests through the circuit breaker, see if the circuit is
39
+ # open for any of them.
40
+ real_requests = requests.each_with_object([]) do |req, real_reqs|
41
+ short_circuit_response = @circuit_store.try_respond(req)
42
+ if short_circuit_response.nil?
43
+ real_reqs << req
44
+ next
45
+ end
46
+ short_circuit_responses[requests.index(req)] = short_circuit_response
47
+ end
48
+
49
+ # run requests for the remainder
50
+ unless real_requests.empty?
51
+ responses = super(*real_requests)
52
+
53
+ real_requests.each_with_index do |request, idx|
54
+ short_circuit_responses[requests.index(request)] = responses[idx]
55
+ end
56
+ end
57
+
58
+ short_circuit_responses
59
+ end
60
+
61
+ def on_response(request, response)
62
+ if response.is_a?(ErrorResponse)
63
+ case response.error
64
+ when RequestTimeoutError
65
+ @circuit_store.try_open(request.uri, response)
66
+ else
67
+ @circuit_store.try_open(request.origin, response)
68
+ end
69
+ elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
70
+ @circuit_store.try_open(request.uri, response)
71
+ end
72
+
73
+ super
74
+ end
75
+ end
76
+
77
+ module OptionsMethods
78
+ def option_circuit_breaker_max_attempts(value)
79
+ attempts = Integer(value)
80
+ raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
81
+
82
+ attempts
83
+ end
84
+
85
+ def option_circuit_breaker_reset_attempts_in(value)
86
+ timeout = Float(value)
87
+ raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
88
+
89
+ timeout
90
+ end
91
+
92
+ def option_circuit_breaker_break_in(value)
93
+ timeout = Float(value)
94
+ raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
95
+
96
+ timeout
97
+ end
98
+
99
+ def option_circuit_breaker_half_open_drip_rate(value)
100
+ ratio = Float(value)
101
+ raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
102
+
103
+ ratio
104
+ end
105
+
106
+ def option_circuit_breaker_break_on(value)
107
+ raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
108
+
109
+ value
110
+ end
111
+ end
112
+ end
113
+ register_plugin :circuit_breaker, CircuitBreaker
114
+ end
115
+ end
@@ -42,7 +42,7 @@ module HTTPX
42
42
 
43
43
  private
44
44
 
45
- def on_response(reuest, response)
45
+ def on_response(_request, response)
46
46
  if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
47
47
 
48
48
  log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
@@ -22,7 +22,7 @@ module HTTPX
22
22
 
23
23
  module OptionsMethods
24
24
  def option_expect_timeout(value)
25
- seconds = Integer(value)
25
+ seconds = Float(value)
26
26
  raise TypeError, ":expect_timeout must be positive" unless seconds.positive?
27
27
 
28
28
  seconds
@@ -61,7 +61,7 @@ module HTTPX::Plugins
61
61
  @state = :idle
62
62
  end
63
63
 
64
- def call(response, _)
64
+ def call(response, *)
65
65
  response.body.each do |chunk|
66
66
  @buffer << chunk
67
67
 
@@ -105,6 +105,10 @@ module HTTPX
105
105
  end
106
106
  return if @_proxy_uris.empty?
107
107
 
108
+ proxy = options.proxy
109
+
110
+ return { uri: uri.host } if proxy && proxy.key?(:no_proxy) && !Array(proxy[:no_proxy]).grep(uri.host).empty?
111
+
108
112
  proxy_opts = { uri: @_proxy_uris.first }
109
113
  proxy_opts = options.proxy.merge(proxy_opts) if options.proxy
110
114
  proxy_opts
@@ -117,7 +121,9 @@ module HTTPX
117
121
  next_proxy = proxy_uris(uri, options)
118
122
  raise Error, "Failed to connect to proxy" unless next_proxy
119
123
 
120
- proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
124
+ proxy = Parameters.new(**next_proxy) unless next_proxy[:uri] == uri.host
125
+
126
+ proxy_options = options.merge(proxy: proxy)
121
127
  connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
122
128
  unless connections.nil? || connections.include?(connection)
123
129
  connections << connection
@@ -67,7 +67,7 @@ module HTTPX
67
67
  end
68
68
 
69
69
  def option_retry_on(value)
70
- raise ":retry_on must be called with the response" unless value.respond_to?(:call)
70
+ raise TypeError, ":retry_on must be called with the response" unless value.respond_to?(:call)
71
71
 
72
72
  value
73
73
  end