httpx 0.20.5 → 0.21.0
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.
- checksums.yaml +4 -4
- data/doc/release_notes/0_13_0.md +1 -1
- data/doc/release_notes/0_21_0.md +94 -0
- data/lib/httpx/connection/http1.rb +2 -1
- data/lib/httpx/connection.rb +41 -2
- data/lib/httpx/errors.rb +18 -0
- data/lib/httpx/extensions.rb +8 -4
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/options.rb +7 -3
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
- data/lib/httpx/plugins/circuit_breaker.rb +115 -0
- data/lib/httpx/plugins/cookies.rb +1 -1
- data/lib/httpx/plugins/expect.rb +1 -1
- data/lib/httpx/plugins/multipart/decoder.rb +1 -1
- data/lib/httpx/plugins/proxy.rb +7 -1
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +78 -0
- data/lib/httpx/request.rb +15 -25
- data/lib/httpx/resolver/https.rb +2 -7
- data/lib/httpx/resolver/native.rb +2 -1
- data/lib/httpx/response.rb +27 -9
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/transcoder/form.rb +1 -1
- data/lib/httpx/transcoder/json.rb +19 -3
- data/lib/httpx/transcoder/xml.rb +57 -0
- data/lib/httpx/transcoder.rb +1 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +1 -1
- data/sig/chainable.rbs +1 -0
- data/sig/connection.rbs +12 -4
- data/sig/errors.rbs +13 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +4 -1
- data/sig/plugins/circuit_breaker.rbs +61 -0
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +3 -3
- data/sig/plugins/compression.rbs +1 -1
- data/sig/plugins/multipart.rbs +1 -1
- data/sig/plugins/proxy/socks5.rbs +3 -2
- data/sig/plugins/proxy.rbs +1 -1
- data/sig/registry.rbs +5 -4
- data/sig/request.rbs +7 -1
- data/sig/resolver/native.rbs +5 -2
- data/sig/response.rbs +3 -1
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/json.rbs +4 -1
- data/sig/transcoder/xml.rbs +21 -0
- data/sig/transcoder.rbs +2 -2
- data/sig/utils.rbs +2 -2
- metadata +12 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: c62cb1d027ee7e62770de459b00411614372578117ea56354df0d9114cf4397b
         | 
| 4 | 
            +
              data.tar.gz: 18275725fc8adeac596f02f83f034dfcfe24a7e4c96ec34bc11400e8ca0e9567
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 840bf59a4cbbb45d26a836ccd4060007470d380314b4aedea58e49cb434b050a8252f7db3823daf9c2dfdb5d54075215e4ee1216c80852afbb7abfd57e1dfea2
         | 
| 7 | 
            +
              data.tar.gz: 35a666b92236240e8ccd09b1dc29a2ccf83061222b0511d35eee6fb7f3794dc737ca86ac9ca7d87ba01e03d8e648b07e6e52290b02b25674bd783b7f9d2cfbce
         | 
    
        data/doc/release_notes/0_13_0.md
    CHANGED
    
    | @@ -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. | 
| 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?
         | 
    
        data/lib/httpx/connection.rb
    CHANGED
    
    | @@ -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,  | 
| 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
         | 
    
        data/lib/httpx/extensions.rb
    CHANGED
    
    | @@ -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 " | 
| 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,  | 
| 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( | 
| 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
         | 
    
        data/lib/httpx/plugins/expect.rb
    CHANGED
    
    
    
        data/lib/httpx/plugins/proxy.rb
    CHANGED
    
    | @@ -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 | 
            -
                       | 
| 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
         |