httpx 1.1.4 → 1.2.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/README.md +5 -5
- data/doc/release_notes/1_1_4.md +1 -1
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/lib/httpx/adapters/webmock.rb +29 -8
- data/lib/httpx/altsvc.rb +57 -2
- data/lib/httpx/chainable.rb +40 -29
- data/lib/httpx/connection/http1.rb +27 -22
- data/lib/httpx/connection/http2.rb +7 -3
- data/lib/httpx/connection.rb +45 -60
- data/lib/httpx/extensions.rb +0 -15
- data/lib/httpx/options.rb +84 -27
- data/lib/httpx/plugins/aws_sigv4.rb +2 -2
- data/lib/httpx/plugins/basic_auth.rb +1 -1
- data/lib/httpx/plugins/callbacks.rb +91 -0
- data/lib/httpx/plugins/circuit_breaker.rb +2 -0
- data/lib/httpx/plugins/cookies.rb +19 -9
- data/lib/httpx/plugins/digest_auth.rb +1 -1
- data/lib/httpx/plugins/follow_redirects.rb +11 -0
- data/lib/httpx/plugins/grpc/call.rb +2 -3
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -0
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/h2c.rb +20 -8
- data/lib/httpx/plugins/proxy/socks4.rb +2 -2
- data/lib/httpx/plugins/proxy/socks5.rb +2 -2
- data/lib/httpx/plugins/proxy.rb +14 -32
- data/lib/httpx/plugins/rate_limiter.rb +1 -1
- data/lib/httpx/plugins/retries.rb +4 -0
- data/lib/httpx/plugins/ssrf_filter.rb +142 -0
- data/lib/httpx/plugins/stream.rb +9 -4
- data/lib/httpx/plugins/upgrade/h2.rb +1 -1
- data/lib/httpx/plugins/upgrade.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +1 -1
- data/lib/httpx/pool.rb +32 -28
- data/lib/httpx/request/body.rb +3 -3
- data/lib/httpx/request.rb +12 -3
- data/lib/httpx/resolver/https.rb +3 -2
- data/lib/httpx/resolver/native.rb +1 -0
- data/lib/httpx/resolver/resolver.rb +17 -6
- data/lib/httpx/response.rb +1 -1
- data/lib/httpx/session.rb +13 -82
- data/lib/httpx/timers.rb +3 -10
- data/lib/httpx/transcoder.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +33 -0
- data/sig/chainable.rbs +1 -0
- data/sig/connection/http1.rbs +2 -1
- data/sig/connection.rbs +16 -16
- data/sig/options.rbs +10 -2
- data/sig/plugins/callbacks.rbs +38 -0
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/follow_redirects.rbs +2 -0
- data/sig/plugins/proxy/socks4.rbs +2 -1
- data/sig/plugins/proxy/socks5.rbs +2 -1
- data/sig/plugins/proxy.rbs +11 -1
- data/sig/plugins/stream.rbs +24 -22
- data/sig/pool.rbs +1 -3
- data/sig/resolver/resolver.rbs +3 -1
- data/sig/session.rbs +4 -4
- metadata +12 -4
    
        data/lib/httpx/plugins/h2c.rb
    CHANGED
    
    | @@ -4,9 +4,9 @@ module HTTPX | |
| 4 4 | 
             
              module Plugins
         | 
| 5 5 | 
             
                #
         | 
| 6 6 | 
             
                # This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2
         | 
| 7 | 
            -
                # (https:// | 
| 7 | 
            +
                # (https://datatracker.ietf.org/doc/html/rfc7540#section-3.2)
         | 
| 8 8 | 
             
                #
         | 
| 9 | 
            -
                # https://gitlab.com/os85/httpx/wikis/Upgrade#h2c
         | 
| 9 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2c
         | 
| 10 10 | 
             
                #
         | 
| 11 11 | 
             
                module H2C
         | 
| 12 12 | 
             
                  VALID_H2C_VERBS = %w[GET OPTIONS HEAD].freeze
         | 
| @@ -73,22 +73,34 @@ module HTTPX | |
| 73 73 | 
             
                        @inflight -= prev_parser.requests.size
         | 
| 74 74 | 
             
                      end
         | 
| 75 75 |  | 
| 76 | 
            -
                       | 
| 77 | 
            -
                      @parser = H2CParser.new(@write_buffer, parser_options)
         | 
| 76 | 
            +
                      @parser = H2CParser.new(@write_buffer, @options)
         | 
| 78 77 | 
             
                      set_parser_callbacks(@parser)
         | 
| 79 78 | 
             
                      @inflight += 1
         | 
| 80 79 | 
             
                      @parser.upgrade(request, response)
         | 
| 81 80 | 
             
                      @upgrade_protocol = "h2c"
         | 
| 82 81 |  | 
| 83 | 
            -
                      if request.options.max_concurrent_requests != @options.max_concurrent_requests
         | 
| 84 | 
            -
                        @options = @options.merge(max_concurrent_requests: nil)
         | 
| 85 | 
            -
                      end
         | 
| 86 | 
            -
             | 
| 87 82 | 
             
                      prev_parser.requests.each do |req|
         | 
| 88 83 | 
             
                        req.transition(:idle)
         | 
| 89 84 | 
             
                        send(req)
         | 
| 90 85 | 
             
                      end
         | 
| 91 86 | 
             
                    end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    private
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    def send_request_to_parser(request)
         | 
| 91 | 
            +
                      super
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      return unless request.headers["upgrade"] == "h2c" && parser.is_a?(Connection::HTTP1)
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                      max_concurrent_requests = parser.max_concurrent_requests
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                      return if max_concurrent_requests == 1
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                      parser.max_concurrent_requests = 1
         | 
| 100 | 
            +
                      request.once(:response) do
         | 
| 101 | 
            +
                        parser.max_concurrent_requests = max_concurrent_requests
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
                    end
         | 
| 92 104 | 
             
                  end
         | 
| 93 105 | 
             
                end
         | 
| 94 106 | 
             
                register_plugin(:h2c, H2C)
         | 
| @@ -4,7 +4,7 @@ require "resolv" | |
| 4 4 | 
             
            require "ipaddr"
         | 
| 5 5 |  | 
| 6 6 | 
             
            module HTTPX
         | 
| 7 | 
            -
              class Socks4Error <  | 
| 7 | 
            +
              class Socks4Error < HTTPProxyError; end
         | 
| 8 8 |  | 
| 9 9 | 
             
              module Plugins
         | 
| 10 10 | 
             
                module Proxy
         | 
| @@ -85,7 +85,7 @@ module HTTPX | |
| 85 85 | 
             
                    end
         | 
| 86 86 |  | 
| 87 87 | 
             
                    class SocksParser
         | 
| 88 | 
            -
                      include Callbacks
         | 
| 88 | 
            +
                      include HTTPX::Callbacks
         | 
| 89 89 |  | 
| 90 90 | 
             
                      def initialize(buffer, options)
         | 
| 91 91 | 
             
                        @buffer = buffer
         | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module HTTPX
         | 
| 4 | 
            -
              class Socks5Error <  | 
| 4 | 
            +
              class Socks5Error < HTTPProxyError; end
         | 
| 5 5 |  | 
| 6 6 | 
             
              module Plugins
         | 
| 7 7 | 
             
                module Proxy
         | 
| @@ -137,7 +137,7 @@ module HTTPX | |
| 137 137 | 
             
                    end
         | 
| 138 138 |  | 
| 139 139 | 
             
                    class SocksParser
         | 
| 140 | 
            -
                      include Callbacks
         | 
| 140 | 
            +
                      include HTTPX::Callbacks
         | 
| 141 141 |  | 
| 142 142 | 
             
                      def initialize(buffer, options)
         | 
| 143 143 | 
             
                        @buffer = buffer
         | 
    
        data/lib/httpx/plugins/proxy.rb
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module HTTPX
         | 
| 4 | 
            -
              class HTTPProxyError <  | 
| 4 | 
            +
              class HTTPProxyError < ConnectionError; end
         | 
| 5 5 |  | 
| 6 6 | 
             
              module Plugins
         | 
| 7 7 | 
             
                #
         | 
| @@ -143,7 +143,7 @@ module HTTPX | |
| 143 143 | 
             
                      proxy = Parameters.new(**proxy_opts)
         | 
| 144 144 |  | 
| 145 145 | 
             
                      proxy_options = options.merge(proxy: proxy)
         | 
| 146 | 
            -
                      connection = pool.find_connection(uri, proxy_options) ||  | 
| 146 | 
            +
                      connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
         | 
| 147 147 | 
             
                      unless connections.nil? || connections.include?(connection)
         | 
| 148 148 | 
             
                        connections << connection
         | 
| 149 149 | 
             
                        set_connection_callbacks(connection, connections, options)
         | 
| @@ -151,19 +151,15 @@ module HTTPX | |
| 151 151 | 
             
                      connection
         | 
| 152 152 | 
             
                    end
         | 
| 153 153 |  | 
| 154 | 
            -
                    def build_connection(uri, options)
         | 
| 155 | 
            -
                      proxy = options.proxy
         | 
| 156 | 
            -
                      return super unless proxy
         | 
| 157 | 
            -
             | 
| 158 | 
            -
                      init_connection("tcp", uri, options)
         | 
| 159 | 
            -
                    end
         | 
| 160 | 
            -
             | 
| 161 154 | 
             
                    def fetch_response(request, connections, options)
         | 
| 162 155 | 
             
                      response = super
         | 
| 163 156 |  | 
| 164 | 
            -
                      if response.is_a?(ErrorResponse) &&
         | 
| 165 | 
            -
                         __proxy_error?(response) && !@_proxy_uris.empty?
         | 
| 157 | 
            +
                      if response.is_a?(ErrorResponse) && proxy_error?(request, response)
         | 
| 166 158 | 
             
                        @_proxy_uris.shift
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                        # return last error response if no more proxies to try
         | 
| 161 | 
            +
                        return response if @_proxy_uris.empty?
         | 
| 162 | 
            +
             | 
| 167 163 | 
             
                        log { "failed connecting to proxy, trying next..." }
         | 
| 168 164 | 
             
                        request.transition(:idle)
         | 
| 169 165 | 
             
                        send_request(request, connections, options)
         | 
| @@ -172,13 +168,7 @@ module HTTPX | |
| 172 168 | 
             
                      response
         | 
| 173 169 | 
             
                    end
         | 
| 174 170 |  | 
| 175 | 
            -
                    def  | 
| 176 | 
            -
                      return if options.proxy
         | 
| 177 | 
            -
             | 
| 178 | 
            -
                      super
         | 
| 179 | 
            -
                    end
         | 
| 180 | 
            -
             | 
| 181 | 
            -
                    def __proxy_error?(response)
         | 
| 171 | 
            +
                    def proxy_error?(_request, response)
         | 
| 182 172 | 
             
                      error = response.error
         | 
| 183 173 | 
             
                      case error
         | 
| 184 174 | 
             
                      when NativeResolveError
         | 
| @@ -235,14 +225,6 @@ module HTTPX | |
| 235 225 | 
             
                      end
         | 
| 236 226 | 
             
                    end
         | 
| 237 227 |  | 
| 238 | 
            -
                    def send(request)
         | 
| 239 | 
            -
                      return super unless (
         | 
| 240 | 
            -
                        @options.proxy && @state != :idle && connecting?
         | 
| 241 | 
            -
                      )
         | 
| 242 | 
            -
             | 
| 243 | 
            -
                      (@proxy_pending ||= []) << request
         | 
| 244 | 
            -
                    end
         | 
| 245 | 
            -
             | 
| 246 228 | 
             
                    def connecting?
         | 
| 247 229 | 
             
                      return super unless @options.proxy
         | 
| 248 230 |  | 
| @@ -271,6 +253,12 @@ module HTTPX | |
| 271 253 |  | 
| 272 254 | 
             
                    private
         | 
| 273 255 |  | 
| 256 | 
            +
                    def initialize_type(uri, options)
         | 
| 257 | 
            +
                      return super unless options.proxy
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                      "tcp"
         | 
| 260 | 
            +
                    end
         | 
| 261 | 
            +
             | 
| 274 262 | 
             
                    def connect
         | 
| 275 263 | 
             
                      return super unless @options.proxy
         | 
| 276 264 |  | 
| @@ -278,12 +266,6 @@ module HTTPX | |
| 278 266 | 
             
                      when :idle
         | 
| 279 267 | 
             
                        transition(:connecting)
         | 
| 280 268 | 
             
                      when :connected
         | 
| 281 | 
            -
                        if @proxy_pending
         | 
| 282 | 
            -
                          while (req = @proxy_pendind.shift)
         | 
| 283 | 
            -
                            send(req)
         | 
| 284 | 
            -
                          end
         | 
| 285 | 
            -
                        end
         | 
| 286 | 
            -
             | 
| 287 269 | 
             
                        transition(:open)
         | 
| 288 270 | 
             
                      end
         | 
| 289 271 | 
             
                    end
         | 
| @@ -9,7 +9,7 @@ module HTTPX | |
| 9 9 | 
             
                # * when the server is unavailable (503);
         | 
| 10 10 | 
             
                # * when a 3xx request comes with a "retry-after" value
         | 
| 11 11 | 
             
                #
         | 
| 12 | 
            -
                # https://gitlab.com/os85/httpx/wikis/ | 
| 12 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Rate-Limiter
         | 
| 13 13 | 
             
                #
         | 
| 14 14 | 
             
                module RateLimiter
         | 
| 15 15 | 
             
                  class << self
         | 
| @@ -132,6 +132,10 @@ module HTTPX | |
| 132 132 | 
             
                      RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
         | 
| 133 133 | 
             
                    end
         | 
| 134 134 |  | 
| 135 | 
            +
                    def proxy_error?(request, response)
         | 
| 136 | 
            +
                      super && !request.retries.positive?
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
             | 
| 135 139 | 
             
                    #
         | 
| 136 140 | 
             
                    # Atttempt to set the request to perform a partial range request.
         | 
| 137 141 | 
             
                    # This happens if the peer server accepts byte-range requests, and
         | 
| @@ -0,0 +1,142 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module HTTPX
         | 
| 4 | 
            +
              class ServerSideRequestForgeryError < Error; end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              module Plugins
         | 
| 7 | 
            +
                #
         | 
| 8 | 
            +
                # This plugin adds support for preventing Server-Side Request Forgery attacks.
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                module SsrfFilter
         | 
| 13 | 
            +
                  module IPAddrExtensions
         | 
| 14 | 
            +
                    refine IPAddr do
         | 
| 15 | 
            +
                      def prefixlen
         | 
| 16 | 
            +
                        mask_addr = @mask_addr
         | 
| 17 | 
            +
                        raise "Invalid mask" if mask_addr.zero?
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                        mask_addr >>= 1 while (mask_addr & 0x1).zero?
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                        length = 0
         | 
| 22 | 
            +
                        while mask_addr & 0x1 == 0x1
         | 
| 23 | 
            +
                          length += 1
         | 
| 24 | 
            +
                          mask_addr >>= 1
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                        length
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  using IPAddrExtensions
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  # https://en.wikipedia.org/wiki/Reserved_IP_addresses
         | 
| 35 | 
            +
                  IPV4_BLACKLIST = [
         | 
| 36 | 
            +
                    IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
         | 
| 37 | 
            +
                    IPAddr.new("10.0.0.0/8"), # Private network
         | 
| 38 | 
            +
                    IPAddr.new("100.64.0.0/10"), # Shared Address Space
         | 
| 39 | 
            +
                    IPAddr.new("127.0.0.0/8"), # Loopback
         | 
| 40 | 
            +
                    IPAddr.new("169.254.0.0/16"), # Link-local
         | 
| 41 | 
            +
                    IPAddr.new("172.16.0.0/12"), # Private network
         | 
| 42 | 
            +
                    IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
         | 
| 43 | 
            +
                    IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
         | 
| 44 | 
            +
                    IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
         | 
| 45 | 
            +
                    IPAddr.new("192.168.0.0/16"), # Private network
         | 
| 46 | 
            +
                    IPAddr.new("198.18.0.0/15"), # Network benchmark tests
         | 
| 47 | 
            +
                    IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
         | 
| 48 | 
            +
                    IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
         | 
| 49 | 
            +
                    IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
         | 
| 50 | 
            +
                    IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
         | 
| 51 | 
            +
                    IPAddr.new("255.255.255.255"), # Broadcast
         | 
| 52 | 
            +
                  ].freeze
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  IPV6_BLACKLIST = ([
         | 
| 55 | 
            +
                    IPAddr.new("::1/128"), # Loopback
         | 
| 56 | 
            +
                    IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
         | 
| 57 | 
            +
                    IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
         | 
| 58 | 
            +
                    IPAddr.new("2001::/32"), # Teredo tunneling
         | 
| 59 | 
            +
                    IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
         | 
| 60 | 
            +
                    IPAddr.new("2001:20::/28"), # ORCHIDv2
         | 
| 61 | 
            +
                    IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
         | 
| 62 | 
            +
                    IPAddr.new("2002::/16"), # 6to4
         | 
| 63 | 
            +
                    IPAddr.new("fc00::/7"), # Unique local address
         | 
| 64 | 
            +
                    IPAddr.new("fe80::/10"), # Link-local address
         | 
| 65 | 
            +
                    IPAddr.new("ff00::/8"), # Multicast
         | 
| 66 | 
            +
                  ] + IPV4_BLACKLIST.flat_map do |ipaddr|
         | 
| 67 | 
            +
                    prefixlen = ipaddr.prefixlen
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
         | 
| 70 | 
            +
                    ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    [ipv4_compatible, ipv4_mapped]
         | 
| 73 | 
            +
                  end).freeze
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  class << self
         | 
| 76 | 
            +
                    def extra_options(options)
         | 
| 77 | 
            +
                      options.merge(allowed_schemes: %w[https http])
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    def unsafe_ip_address?(ipaddr)
         | 
| 81 | 
            +
                      range = ipaddr.to_range
         | 
| 82 | 
            +
                      return true if range.first != range.last
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  module OptionsMethods
         | 
| 91 | 
            +
                    def option_allowed_schemes(value)
         | 
| 92 | 
            +
                      Array(value)
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  module InstanceMethods
         | 
| 97 | 
            +
                    def send_requests(*requests)
         | 
| 98 | 
            +
                      responses = requests.map do |request|
         | 
| 99 | 
            +
                        next if @options.allowed_schemes.include?(request.uri.scheme)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                        error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
         | 
| 102 | 
            +
                        error.set_backtrace(caller)
         | 
| 103 | 
            +
                        response = ErrorResponse.new(request, error, request.options)
         | 
| 104 | 
            +
                        request.emit(:response, response)
         | 
| 105 | 
            +
                        response
         | 
| 106 | 
            +
                      end
         | 
| 107 | 
            +
                      allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
         | 
| 108 | 
            +
                      allowed_responses = super(*allowed_requests)
         | 
| 109 | 
            +
                      allowed_responses.each_with_index do |res, idx|
         | 
| 110 | 
            +
                        req = allowed_requests[idx]
         | 
| 111 | 
            +
                        responses[requests.index(req)] = res
         | 
| 112 | 
            +
                      end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                      responses
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  module ConnectionMethods
         | 
| 119 | 
            +
                    def initialize(*)
         | 
| 120 | 
            +
                      begin
         | 
| 121 | 
            +
                        super
         | 
| 122 | 
            +
                      rescue ServerSideRequestForgeryError => e
         | 
| 123 | 
            +
                        # may raise when IPs are passed as options via :addresses
         | 
| 124 | 
            +
                        throw(:resolve_error, e)
         | 
| 125 | 
            +
                      end
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    def addresses=(addrs)
         | 
| 129 | 
            +
                      addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                      addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                      raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                      super
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                register_plugin :ssrf_filter, SsrfFilter
         | 
| 141 | 
            +
              end
         | 
| 142 | 
            +
            end
         | 
    
        data/lib/httpx/plugins/stream.rb
    CHANGED
    
    | @@ -5,6 +5,7 @@ module HTTPX | |
| 5 5 | 
             
                def initialize(request, session)
         | 
| 6 6 | 
             
                  @request = request
         | 
| 7 7 | 
             
                  @session = session
         | 
| 8 | 
            +
                  @response = nil
         | 
| 8 9 | 
             
                end
         | 
| 9 10 |  | 
| 10 11 | 
             
                def each(&block)
         | 
| @@ -25,7 +26,7 @@ module HTTPX | |
| 25 26 | 
             
                def each_line
         | 
| 26 27 | 
             
                  return enum_for(__method__) unless block_given?
         | 
| 27 28 |  | 
| 28 | 
            -
                  line =  | 
| 29 | 
            +
                  line = "".b
         | 
| 29 30 |  | 
| 30 31 | 
             
                  each do |chunk|
         | 
| 31 32 | 
             
                    line << chunk
         | 
| @@ -36,6 +37,8 @@ module HTTPX | |
| 36 37 | 
             
                      line = line.byteslice(idx + 1..-1)
         | 
| 37 38 | 
             
                    end
         | 
| 38 39 | 
             
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  yield line unless line.empty?
         | 
| 39 42 | 
             
                end
         | 
| 40 43 |  | 
| 41 44 | 
             
                # This is a ghost method. It's to be used ONLY internally, when processing streams
         | 
| @@ -58,8 +61,10 @@ module HTTPX | |
| 58 61 | 
             
                private
         | 
| 59 62 |  | 
| 60 63 | 
             
                def response
         | 
| 61 | 
            -
                  @response  | 
| 62 | 
            -
             | 
| 64 | 
            +
                  return @response if @response
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  @request.response || begin
         | 
| 67 | 
            +
                    @response = @session.request(@request)
         | 
| 63 68 | 
             
                  end
         | 
| 64 69 | 
             
                end
         | 
| 65 70 |  | 
| @@ -78,7 +83,7 @@ module HTTPX | |
| 78 83 | 
             
                #
         | 
| 79 84 | 
             
                # This plugin adds support for stream response (text/event-stream).
         | 
| 80 85 | 
             
                #
         | 
| 81 | 
            -
                # https://gitlab.com/ | 
| 86 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Stream
         | 
| 82 87 | 
             
                #
         | 
| 83 88 | 
             
                module Stream
         | 
| 84 89 | 
             
                  def self.extra_options(options)
         | 
| @@ -6,7 +6,7 @@ module HTTPX | |
| 6 6 | 
             
                # This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
         | 
| 7 7 | 
             
                # via an Upgrade: h2 response declaration
         | 
| 8 8 | 
             
                #
         | 
| 9 | 
            -
                # https://gitlab.com/ | 
| 9 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
         | 
| 10 10 | 
             
                #
         | 
| 11 11 | 
             
                module H2
         | 
| 12 12 | 
             
                  class << self
         | 
| @@ -6,7 +6,7 @@ module HTTPX | |
| 6 6 | 
             
                # This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
         | 
| 7 7 | 
             
                # Upgrade header.
         | 
| 8 8 | 
             
                #
         | 
| 9 | 
            -
                # https://gitlab.com/ | 
| 9 | 
            +
                # https://gitlab.com/os85/httpx/wikis/Upgrade
         | 
| 10 10 | 
             
                #
         | 
| 11 11 | 
             
                module Upgrade
         | 
| 12 12 | 
             
                  class << self
         | 
    
        data/lib/httpx/plugins/webdav.rb
    CHANGED
    
    
    
        data/lib/httpx/pool.rb
    CHANGED
    
    | @@ -17,8 +17,6 @@ module HTTPX | |
| 17 17 | 
             
                  @timers = Timers.new
         | 
| 18 18 | 
             
                  @selector = Selector.new
         | 
| 19 19 | 
             
                  @connections = []
         | 
| 20 | 
            -
                  @eden_connections = []
         | 
| 21 | 
            -
                  @connected_connections = 0
         | 
| 22 20 | 
             
                end
         | 
| 23 21 |  | 
| 24 22 | 
             
                def empty?
         | 
| @@ -45,16 +43,15 @@ module HTTPX | |
| 45 43 | 
             
                    connection.emit(:error, e)
         | 
| 46 44 | 
             
                  end
         | 
| 47 45 | 
             
                rescue Exception # rubocop:disable Lint/RescueException
         | 
| 48 | 
            -
                  @connections.each(&: | 
| 46 | 
            +
                  @connections.each(&:force_reset)
         | 
| 49 47 | 
             
                  raise
         | 
| 50 48 | 
             
                end
         | 
| 51 49 |  | 
| 52 50 | 
             
                def close(connections = @connections)
         | 
| 53 51 | 
             
                  return if connections.empty?
         | 
| 54 52 |  | 
| 55 | 
            -
                  @eden_connections.clear
         | 
| 56 53 | 
             
                  connections = connections.reject(&:inflight?)
         | 
| 57 | 
            -
                  connections.each(&: | 
| 54 | 
            +
                  connections.each(&:terminate)
         | 
| 58 55 | 
             
                  next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
         | 
| 59 56 |  | 
| 60 57 | 
             
                  # close resolvers
         | 
| @@ -68,22 +65,36 @@ module HTTPX | |
| 68 65 | 
             
                    resolver.close unless resolver.closed?
         | 
| 69 66 | 
             
                  end
         | 
| 70 67 | 
             
                  # for https resolver
         | 
| 71 | 
            -
                  resolver_connections.each(&: | 
| 68 | 
            +
                  resolver_connections.each(&:terminate)
         | 
| 72 69 | 
             
                  next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
         | 
| 73 70 | 
             
                end
         | 
| 74 71 |  | 
| 75 72 | 
             
                def init_connection(connection, _options)
         | 
| 76 | 
            -
                  resolve_connection(connection) unless connection.family
         | 
| 77 73 | 
             
                  connection.timers = @timers
         | 
| 78 | 
            -
                  connection.on(:open) do
         | 
| 79 | 
            -
                    @connected_connections += 1
         | 
| 80 | 
            -
                  end
         | 
| 81 74 | 
             
                  connection.on(:activate) do
         | 
| 82 75 | 
             
                    select_connection(connection)
         | 
| 83 76 | 
             
                  end
         | 
| 77 | 
            +
                  connection.on(:exhausted) do
         | 
| 78 | 
            +
                    case connection.state
         | 
| 79 | 
            +
                    when :closed
         | 
| 80 | 
            +
                      connection.idling
         | 
| 81 | 
            +
                      @connections << connection
         | 
| 82 | 
            +
                      select_connection(connection)
         | 
| 83 | 
            +
                    when :closing
         | 
| 84 | 
            +
                      connection.once(:close) do
         | 
| 85 | 
            +
                        connection.idling
         | 
| 86 | 
            +
                        @connections << connection
         | 
| 87 | 
            +
                        select_connection(connection)
         | 
| 88 | 
            +
                      end
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
                  end
         | 
| 84 91 | 
             
                  connection.on(:close) do
         | 
| 85 92 | 
             
                    unregister_connection(connection)
         | 
| 86 93 | 
             
                  end
         | 
| 94 | 
            +
                  connection.on(:terminate) do
         | 
| 95 | 
            +
                    unregister_connection(connection, true)
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                  resolve_connection(connection) unless connection.family
         | 
| 87 98 | 
             
                end
         | 
| 88 99 |  | 
| 89 100 | 
             
                def deactivate(connections)
         | 
| @@ -102,16 +113,15 @@ module HTTPX | |
| 102 113 | 
             
                    connection.match?(uri, options)
         | 
| 103 114 | 
             
                  end
         | 
| 104 115 |  | 
| 105 | 
            -
                  unless conn
         | 
| 106 | 
            -
                    @eden_connections.delete_if do |connection|
         | 
| 107 | 
            -
                      is_expired = connection.expired?
         | 
| 108 | 
            -
                      conn = connection if conn.nil? && !is_expired && connection.match?(uri, options)
         | 
| 109 | 
            -
                      is_expired
         | 
| 110 | 
            -
                    end
         | 
| 116 | 
            +
                  return unless conn
         | 
| 111 117 |  | 
| 112 | 
            -
             | 
| 118 | 
            +
                  case conn.state
         | 
| 119 | 
            +
                  when :closed
         | 
| 120 | 
            +
                    conn.idling
         | 
| 121 | 
            +
                    select_connection(conn)
         | 
| 122 | 
            +
                  when :closing
         | 
| 123 | 
            +
                    conn.once(:close) do
         | 
| 113 124 | 
             
                      conn.idling
         | 
| 114 | 
            -
                      @connections << conn
         | 
| 115 125 | 
             
                      select_connection(conn)
         | 
| 116 126 | 
             
                    end
         | 
| 117 127 | 
             
                  end
         | 
| @@ -151,7 +161,7 @@ module HTTPX | |
| 151 161 |  | 
| 152 162 | 
             
                  return connection if connection.family == family
         | 
| 153 163 |  | 
| 154 | 
            -
                  new_connection = connection.class.new(connection. | 
| 164 | 
            +
                  new_connection = connection.class.new(connection.origin, connection.options)
         | 
| 155 165 | 
             
                  new_connection.family = family
         | 
| 156 166 |  | 
| 157 167 | 
             
                  connection.once(:tcp_open) { new_connection.force_reset }
         | 
| @@ -218,18 +228,12 @@ module HTTPX | |
| 218 228 | 
             
                end
         | 
| 219 229 |  | 
| 220 230 | 
             
                def register_connection(connection)
         | 
| 221 | 
            -
                  if connection.state == :open
         | 
| 222 | 
            -
                    # if open, an IO was passed upstream, therefore
         | 
| 223 | 
            -
                    # consider it connected already.
         | 
| 224 | 
            -
                    @connected_connections += 1
         | 
| 225 | 
            -
                  end
         | 
| 226 231 | 
             
                  select_connection(connection)
         | 
| 227 232 | 
             
                end
         | 
| 228 233 |  | 
| 229 | 
            -
                def unregister_connection(connection)
         | 
| 230 | 
            -
                  @connections.delete(connection)
         | 
| 231 | 
            -
                   | 
| 232 | 
            -
                  @connected_connections -= 1 if deselect_connection(connection)
         | 
| 234 | 
            +
                def unregister_connection(connection, cleanup = !connection.used?)
         | 
| 235 | 
            +
                  @connections.delete(connection) if cleanup
         | 
| 236 | 
            +
                  deselect_connection(connection)
         | 
| 233 237 | 
             
                end
         | 
| 234 238 |  | 
| 235 239 | 
             
                def select_connection(connection)
         | 
    
        data/lib/httpx/request/body.rb
    CHANGED
    
    | @@ -70,9 +70,9 @@ module HTTPX | |
| 70 70 |  | 
| 71 71 | 
             
                # sets the body to yield using chunked trannsfer encoding format.
         | 
| 72 72 | 
             
                def stream(body)
         | 
| 73 | 
            -
                   | 
| 74 | 
            -
             | 
| 75 | 
            -
                   | 
| 73 | 
            +
                  return body unless chunked?
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  Transcoder::Chunker.encode(body.enum_for(:each))
         | 
| 76 76 | 
             
                end
         | 
| 77 77 |  | 
| 78 78 | 
             
                # returns whether the body yields infinitely.
         | 
    
        data/lib/httpx/request.rb
    CHANGED
    
    | @@ -119,10 +119,19 @@ module HTTPX | |
| 119 119 | 
             
                def response=(response)
         | 
| 120 120 | 
             
                  return unless response
         | 
| 121 121 |  | 
| 122 | 
            -
                  if response.is_a?(Response) && response.status  | 
| 123 | 
            -
                     | 
| 124 | 
            -
             | 
| 122 | 
            +
                  if response.is_a?(Response) && response.status < 200
         | 
| 123 | 
            +
                    # deal with informational responses
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                    if response.status == 100 && @headers.key?("expect")
         | 
| 126 | 
            +
                      @informational_status = response.status
         | 
| 127 | 
            +
                      return
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    # 103 Early Hints advertises resources in document to browsers.
         | 
| 131 | 
            +
                    # not very relevant for an HTTP client, discard.
         | 
| 132 | 
            +
                    return if response.status >= 103
         | 
| 125 133 | 
             
                  end
         | 
| 134 | 
            +
             | 
| 126 135 | 
             
                  @response = response
         | 
| 127 136 |  | 
| 128 137 | 
             
                  emit(:response_started, response)
         | 
    
        data/lib/httpx/resolver/https.rb
    CHANGED
    
    | @@ -27,7 +27,7 @@ module HTTPX | |
| 27 27 | 
             
                  use_get: false,
         | 
| 28 28 | 
             
                }.freeze
         | 
| 29 29 |  | 
| 30 | 
            -
                def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
         | 
| 30 | 
            +
                def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
         | 
| 31 31 |  | 
| 32 32 | 
             
                def initialize(_, options)
         | 
| 33 33 | 
             
                  super
         | 
| @@ -50,6 +50,7 @@ module HTTPX | |
| 50 50 | 
             
                  if @uri_addresses.empty?
         | 
| 51 51 | 
             
                    ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
         | 
| 52 52 | 
             
                    ex.set_backtrace(caller)
         | 
| 53 | 
            +
                    connection.force_reset
         | 
| 53 54 | 
             
                    throw(:resolve_error, ex)
         | 
| 54 55 | 
             
                  end
         | 
| 55 56 |  | 
| @@ -67,7 +68,7 @@ module HTTPX | |
| 67 68 | 
             
                def resolver_connection
         | 
| 68 69 | 
             
                  @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
         | 
| 69 70 | 
             
                    @building_connection = true
         | 
| 70 | 
            -
                    connection = @options.connection_class.new( | 
| 71 | 
            +
                    connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
         | 
| 71 72 | 
             
                    @pool.init_connection(connection, @options)
         | 
| 72 73 | 
             
                    # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
         | 
| 73 74 | 
             
                    emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
         | 
| @@ -38,6 +38,8 @@ module HTTPX | |
| 38 38 |  | 
| 39 39 | 
             
                def close; end
         | 
| 40 40 |  | 
| 41 | 
            +
                alias_method :terminate, :close
         | 
| 42 | 
            +
             | 
| 41 43 | 
             
                def closed?
         | 
| 42 44 | 
             
                  true
         | 
| 43 45 | 
             
                end
         | 
| @@ -65,20 +67,29 @@ module HTTPX | |
| 65 67 | 
             
                      unless connection.state == :closed ||
         | 
| 66 68 | 
             
                             # double emission check
         | 
| 67 69 | 
             
                             (connection.addresses && addresses.intersect?(connection.addresses))
         | 
| 68 | 
            -
                        emit_resolved_connection(connection, addresses)
         | 
| 70 | 
            +
                        emit_resolved_connection(connection, addresses, early_resolve)
         | 
| 69 71 | 
             
                      end
         | 
| 70 72 | 
             
                    end
         | 
| 71 73 | 
             
                  else
         | 
| 72 | 
            -
                    emit_resolved_connection(connection, addresses)
         | 
| 74 | 
            +
                    emit_resolved_connection(connection, addresses, early_resolve)
         | 
| 73 75 | 
             
                  end
         | 
| 74 76 | 
             
                end
         | 
| 75 77 |  | 
| 76 78 | 
             
                private
         | 
| 77 79 |  | 
| 78 | 
            -
                def emit_resolved_connection(connection, addresses)
         | 
| 79 | 
            -
                   | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 80 | 
            +
                def emit_resolved_connection(connection, addresses, early_resolve)
         | 
| 81 | 
            +
                  begin
         | 
| 82 | 
            +
                    connection.addresses = addresses
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    emit(:resolve, connection)
         | 
| 85 | 
            +
                  rescue StandardError => e
         | 
| 86 | 
            +
                    if early_resolve
         | 
| 87 | 
            +
                      connection.force_reset
         | 
| 88 | 
            +
                      throw(:resolve_error, e)
         | 
| 89 | 
            +
                    else
         | 
| 90 | 
            +
                      emit(:error, connection, e)
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                  end
         | 
| 82 93 | 
             
                end
         | 
| 83 94 |  | 
| 84 95 | 
             
                def early_resolve(connection, hostname: connection.origin.host)
         | 
    
        data/lib/httpx/response.rb
    CHANGED