httpx 0.18.0 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -4
  3. data/doc/release_notes/0_18_1.md +12 -0
  4. data/doc/release_notes/0_18_2.md +10 -0
  5. data/doc/release_notes/0_18_3.md +7 -0
  6. data/doc/release_notes/0_18_4.md +14 -0
  7. data/doc/release_notes/0_18_5.md +10 -0
  8. data/doc/release_notes/0_18_6.md +5 -0
  9. data/doc/release_notes/0_18_7.md +5 -0
  10. data/doc/release_notes/0_19_0.md +39 -0
  11. data/doc/release_notes/0_19_1.md +5 -0
  12. data/doc/release_notes/0_19_2.md +7 -0
  13. data/doc/release_notes/0_19_3.md +6 -0
  14. data/lib/httpx/adapters/faraday.rb +58 -12
  15. data/lib/httpx/adapters/webmock.rb +71 -59
  16. data/lib/httpx/altsvc.rb +25 -9
  17. data/lib/httpx/connection/http1.rb +10 -7
  18. data/lib/httpx/connection/http2.rb +23 -8
  19. data/lib/httpx/connection.rb +26 -12
  20. data/lib/httpx/extensions.rb +16 -0
  21. data/lib/httpx/headers.rb +0 -2
  22. data/lib/httpx/io/ssl.rb +4 -0
  23. data/lib/httpx/io/tcp.rb +27 -6
  24. data/lib/httpx/io/udp.rb +0 -1
  25. data/lib/httpx/options.rb +44 -11
  26. data/lib/httpx/plugins/cookies.rb +5 -7
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/multipart/mime_type_detector.rb +18 -4
  29. data/lib/httpx/plugins/proxy/http.rb +10 -23
  30. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  31. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  32. data/lib/httpx/plugins/proxy.rb +35 -15
  33. data/lib/httpx/plugins/retries.rb +15 -12
  34. data/lib/httpx/pool.rb +40 -20
  35. data/lib/httpx/request.rb +1 -1
  36. data/lib/httpx/resolver/https.rb +32 -42
  37. data/lib/httpx/resolver/multi.rb +79 -0
  38. data/lib/httpx/resolver/native.rb +28 -36
  39. data/lib/httpx/resolver/resolver.rb +95 -0
  40. data/lib/httpx/resolver/system.rb +175 -19
  41. data/lib/httpx/resolver.rb +37 -11
  42. data/lib/httpx/response.rb +4 -2
  43. data/lib/httpx/selector.rb +7 -0
  44. data/lib/httpx/session.rb +2 -16
  45. data/lib/httpx/session_extensions.rb +26 -0
  46. data/lib/httpx/timers.rb +1 -1
  47. data/lib/httpx/transcoder/chunker.rb +0 -1
  48. data/lib/httpx/version.rb +1 -1
  49. data/lib/httpx.rb +3 -0
  50. data/sig/connection/http1.rbs +5 -2
  51. data/sig/connection/http2.rbs +5 -2
  52. data/sig/connection.rbs +1 -0
  53. data/sig/errors.rbs +8 -0
  54. data/sig/headers.rbs +0 -2
  55. data/sig/httpx.rbs +4 -0
  56. data/sig/options.rbs +10 -7
  57. data/sig/parser/http1.rbs +14 -5
  58. data/sig/pool.rbs +17 -9
  59. data/sig/registry.rbs +3 -0
  60. data/sig/request.rbs +11 -0
  61. data/sig/resolver/https.rbs +15 -27
  62. data/sig/resolver/multi.rbs +7 -0
  63. data/sig/resolver/native.rbs +3 -12
  64. data/sig/resolver/resolver.rbs +36 -0
  65. data/sig/resolver/system.rbs +3 -9
  66. data/sig/resolver.rbs +12 -10
  67. data/sig/response.rbs +15 -5
  68. data/sig/selector.rbs +3 -3
  69. data/sig/timers.rbs +5 -2
  70. data/sig/transcoder/chunker.rbs +16 -5
  71. data/sig/transcoder/json.rbs +5 -0
  72. data/sig/transcoder.rbs +3 -1
  73. metadata +31 -5
  74. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  75. data/sig/resolver/resolver_mixin.rbs +0 -26
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  def_delegator :@write_buffer, :empty?
46
46
 
47
- attr_reader :origin, :state, :pending, :options
47
+ attr_reader :io, :origin, :origins, :state, :pending, :options
48
48
 
49
49
  attr_writer :timers
50
50
 
@@ -78,7 +78,11 @@ module HTTPX
78
78
  # this is a semi-private method, to be used by the resolver
79
79
  # to initiate the io object.
80
80
  def addresses=(addrs)
81
- @io ||= IO.registry(@type).new(@origin, addrs, @options) # rubocop:disable Naming/MemoizedInstanceVariableName
81
+ if @io
82
+ @io.add_addresses(addrs)
83
+ else
84
+ @io = IO.registry(@type).new(@origin, addrs, @options)
85
+ end
82
86
  end
83
87
 
84
88
  def addresses
@@ -117,7 +121,8 @@ module HTTPX
117
121
  def coalescable?(connection)
118
122
  if @io.protocol == "h2" &&
119
123
  @origin.scheme == "https" &&
120
- connection.origin.scheme == "https"
124
+ connection.origin.scheme == "https" &&
125
+ @io.can_verify_peer?
121
126
  @io.verify_hostname(connection.origin.host)
122
127
  else
123
128
  @origin == connection.origin
@@ -241,7 +246,7 @@ module HTTPX
241
246
  if elapsed_time.negative?
242
247
  ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
243
248
  ex.set_backtrace(caller)
244
- on_error(@total_timeout)
249
+ on_error(ex)
245
250
  return
246
251
  end
247
252
 
@@ -463,6 +468,7 @@ module HTTPX
463
468
  transition(:closing)
464
469
  transition(:closed)
465
470
  emit(:reset)
471
+
466
472
  @parser.reset if @parser
467
473
  transition(:idle)
468
474
  transition(:open)
@@ -487,6 +493,18 @@ module HTTPX
487
493
  end
488
494
 
489
495
  def transition(nextstate)
496
+ handle_transition(nextstate)
497
+ rescue Errno::ECONNREFUSED,
498
+ Errno::EADDRNOTAVAIL,
499
+ Errno::EHOSTUNREACH,
500
+ TLSError => e
501
+ # connect errors, exit gracefully
502
+ handle_error(e)
503
+ @state = :closed
504
+ emit(:close)
505
+ end
506
+
507
+ def handle_transition(nextstate)
490
508
  case nextstate
491
509
  when :idle
492
510
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
@@ -523,14 +541,6 @@ module HTTPX
523
541
  emit(:activate)
524
542
  end
525
543
  @state = nextstate
526
- rescue Errno::ECONNREFUSED,
527
- Errno::EADDRNOTAVAIL,
528
- Errno::EHOSTUNREACH,
529
- TLSError => e
530
- # connect errors, exit gracefully
531
- handle_error(e)
532
- @state = :closed
533
- emit(:close)
534
544
  end
535
545
 
536
546
  def purge_after_closed
@@ -548,6 +558,10 @@ module HTTPX
548
558
  ex.set_backtrace(error.backtrace)
549
559
  error = ex
550
560
  else
561
+ # inactive connections do not contribute to the select loop, therefore
562
+ # they should fail due to such errors.
563
+ return if @state == :inactive
564
+
551
565
  if @timeout
552
566
  @timeout -= error.timeout
553
567
  return unless @timeout <= 0
@@ -54,6 +54,22 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module StringExtensions
58
+ refine String do
59
+ def delete_suffix!(suffix)
60
+ suffix = Backports.coerce_to_str(suffix)
61
+ chomp! if frozen?
62
+ len = suffix.length
63
+ if len > 0 && index(suffix, -len)
64
+ self[-len..-1] = ''
65
+ self
66
+ else
67
+ nil
68
+ end
69
+ end unless String.method_defined?(:delete_suffix!)
70
+ end
71
+ end
72
+
57
73
  module HashExtensions
58
74
  refine Hash do
59
75
  def compact
data/lib/httpx/headers.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module HTTPX
4
4
  class Headers
5
- EMPTY = [].freeze
6
-
7
5
  class << self
8
6
  def new(headers = nil)
9
7
  return headers if headers.is_a?(self)
data/lib/httpx/io/ssl.rb CHANGED
@@ -27,6 +27,10 @@ module HTTPX
27
27
  super
28
28
  end
29
29
 
30
+ def can_verify_peer?
31
+ @ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
32
+ end
33
+
30
34
  def verify_hostname(host)
31
35
  return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
32
36
  return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
data/lib/httpx/io/tcp.rb CHANGED
@@ -15,6 +15,7 @@ module HTTPX
15
15
 
16
16
  def initialize(origin, addresses, options)
17
17
  @state = :idle
18
+ @addresses = []
18
19
  @hostname = origin.host
19
20
  @options = Options.new(options)
20
21
  @fallback_protocol = @options.fallback_protocol
@@ -30,15 +31,29 @@ module HTTPX
30
31
  raise Error, "Given IO objects do not match the request authority" unless @io
31
32
 
32
33
  _, _, _, @ip = @io.addr
33
- @addresses ||= [@ip]
34
- @ip_index = @addresses.size - 1
34
+ @addresses << @ip
35
35
  @keep_open = true
36
36
  @state = :connected
37
37
  else
38
- @addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
38
+ add_addresses(addresses)
39
39
  end
40
40
  @ip_index = @addresses.size - 1
41
- @io ||= build_socket
41
+ # @io ||= build_socket
42
+ end
43
+
44
+ def add_addresses(addrs)
45
+ return if addrs.empty?
46
+
47
+ addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
48
+
49
+ ip_index = @ip_index || (@addresses.size - 1)
50
+ if addrs.first.ipv6?
51
+ # should be the next in line
52
+ @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
53
+ else
54
+ @addresses.unshift(*addrs)
55
+ @ip_index += addrs.size if @ip_index
56
+ end
42
57
  end
43
58
 
44
59
  def to_io
@@ -52,20 +67,26 @@ module HTTPX
52
67
  def connect
53
68
  return unless closed?
54
69
 
55
- if @io.closed?
70
+ if !@io || @io.closed?
56
71
  transition(:idle)
57
72
  @io = build_socket
58
73
  end
59
74
  try_connect
60
- rescue Errno::EHOSTUNREACH => e
75
+ rescue Errno::ECONNREFUSED,
76
+ Errno::EADDRNOTAVAIL,
77
+ Errno::EHOSTUNREACH => e
61
78
  raise e if @ip_index <= 0
62
79
 
80
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
63
81
  @ip_index -= 1
82
+ @io = build_socket
64
83
  retry
65
84
  rescue Errno::ETIMEDOUT => e
66
85
  raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
67
86
 
87
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
68
88
  @ip_index -= 1
89
+ @io = build_socket
69
90
  retry
70
91
  end
71
92
 
data/lib/httpx/io/udp.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
3
  require "ipaddr"
5
4
 
6
5
  module HTTPX
data/lib/httpx/options.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+
3
5
  module HTTPX
4
6
  class Options
5
7
  WINDOW_SIZE = 1 << 14 # 16K
@@ -9,6 +11,18 @@ module HTTPX
9
11
  KEEP_ALIVE_TIMEOUT = 20
10
12
  SETTINGS_TIMEOUT = 10
11
13
 
14
+ # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
15
+ ip_address_families = begin
16
+ list = Socket.ip_address_list
17
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
18
+ [Socket::AF_INET6, Socket::AF_INET]
19
+ else
20
+ [Socket::AF_INET]
21
+ end
22
+ rescue NotImplementedError
23
+ [Socket::AF_INET]
24
+ end
25
+
12
26
  DEFAULT_OPTIONS = {
13
27
  :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
14
28
  :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
@@ -37,6 +51,7 @@ module HTTPX
37
51
  :persistent => false,
38
52
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
39
53
  :resolver_options => { cache: true },
54
+ :ip_families => ip_address_families,
40
55
  }.freeze
41
56
 
42
57
  begin
@@ -110,20 +125,18 @@ module HTTPX
110
125
  end
111
126
 
112
127
  def initialize(options = {})
113
- defaults = DEFAULT_OPTIONS.merge(options)
114
- defaults.each do |k, v|
115
- next if v.nil?
116
-
117
- begin
118
- value = __send__(:"option_#{k}", v)
119
- instance_variable_set(:"@#{k}", value)
120
- rescue NoMethodError
121
- raise Error, "unknown option: #{k}"
122
- end
123
- end
128
+ __initialize__(options)
124
129
  freeze
125
130
  end
126
131
 
132
+ def freeze
133
+ super
134
+ @origin.freeze
135
+ @timeout.freeze
136
+ @headers.freeze
137
+ @addresses.freeze
138
+ end
139
+
127
140
  def option_origin(value)
128
141
  URI(value)
129
142
  end
@@ -174,6 +187,10 @@ module HTTPX
174
187
  Array(value)
175
188
  end
176
189
 
190
+ def option_ip_families(value)
191
+ Array(value)
192
+ end
193
+
177
194
  %i[
178
195
  params form json body ssl http2_settings
179
196
  request_class response_class headers_class request_body_class
@@ -249,5 +266,21 @@ module HTTPX
249
266
  end
250
267
  end
251
268
  end
269
+
270
+ private
271
+
272
+ def __initialize__(options = {})
273
+ defaults = DEFAULT_OPTIONS.merge(options)
274
+ defaults.each do |k, v|
275
+ next if v.nil?
276
+
277
+ begin
278
+ value = __send__(:"option_#{k}", v)
279
+ instance_variable_set(:"@#{k}", value)
280
+ rescue NoMethodError
281
+ raise Error, "unknown option: #{k}"
282
+ end
283
+ end
284
+ end
252
285
  end
253
286
  end
@@ -18,12 +18,6 @@ module HTTPX
18
18
  require "httpx/plugins/cookies/set_cookie_parser"
19
19
  end
20
20
 
21
- module OptionsMethods
22
- def option_cookies(value)
23
- value.is_a?(Jar) ? value : Jar.new(value)
24
- end
25
- end
26
-
27
21
  module InstanceMethods
28
22
  extend Forwardable
29
23
 
@@ -77,7 +71,7 @@ module HTTPX
77
71
  end
78
72
 
79
73
  module OptionsMethods
80
- def initialize(*)
74
+ def __initialize__(*)
81
75
  super
82
76
 
83
77
  return unless @headers.key?("cookie")
@@ -89,6 +83,10 @@ module HTTPX
89
83
  end
90
84
  end
91
85
  end
86
+
87
+ def option_cookies(value)
88
+ value.is_a?(Jar) ? value : Jar.new(value)
89
+ end
92
90
  end
93
91
  end
94
92
  register_plugin :cookies, Cookies
@@ -81,7 +81,7 @@ module HTTPX
81
81
  super
82
82
  end
83
83
 
84
- def transition(nextstate)
84
+ def handle_transition(nextstate)
85
85
  state = @state
86
86
  super
87
87
  meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
@@ -8,11 +8,25 @@ module HTTPX
8
8
  DEFAULT_MIMETYPE = "application/octet-stream"
9
9
 
10
10
  # inspired by https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/determine_mime_type.rb
11
- if defined?(MIME::Types)
11
+ if defined?(FileMagic)
12
+ MAGIC_NUMBER = 256 * 1024
12
13
 
13
- def call(_file, filename)
14
- mime = MIME::Types.of(filename).first
15
- mime.content_type if mime
14
+ def call(file, _)
15
+ return nil if file.eof? # FileMagic returns "application/x-empty" for empty files
16
+
17
+ mime = FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
18
+ filemagic.buffer(file.read(MAGIC_NUMBER))
19
+ end
20
+
21
+ file.rewind
22
+
23
+ mime
24
+ end
25
+ elsif defined?(Marcel)
26
+ def call(file, filename)
27
+ return nil if file.eof? # marcel returns "application/octet-stream" for empty files
28
+
29
+ Marcel::MimeType.for(file, name: filename)
16
30
  end
17
31
 
18
32
  elsif defined?(MimeMagic)
@@ -13,7 +13,7 @@ module HTTPX
13
13
 
14
14
  private
15
15
 
16
- def transition(nextstate)
16
+ def handle_transition(nextstate)
17
17
  return super unless @options.proxy && @options.proxy.uri.scheme == "http"
18
18
 
19
19
  case nextstate
@@ -23,7 +23,8 @@ module HTTPX
23
23
  @io.connect
24
24
  return unless @io.connected?
25
25
 
26
- @parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
26
+ @parser = registry(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
27
+ @parser.extend(ProxyParser)
27
28
  @parser.once(:response, &method(:__http_on_connect))
28
29
  @parser.on(:close) { transition(:closing) }
29
30
  __http_proxy_connect
@@ -36,7 +37,7 @@ module HTTPX
36
37
  @parser.close
37
38
  @parser = nil
38
39
  when :idle
39
- @parser = ProxyParser.new(@write_buffer, @options)
40
+ @parser.callbacks.clear
40
41
  set_parser_callbacks(@parser)
41
42
  end
42
43
  end
@@ -54,7 +55,7 @@ module HTTPX
54
55
  @inflight += 1
55
56
  parser.send(connect_request)
56
57
  else
57
- transition(:connected)
58
+ handle_transition(:connected)
58
59
  end
59
60
  end
60
61
 
@@ -76,9 +77,11 @@ module HTTPX
76
77
  end
77
78
  end
78
79
 
79
- class ProxyParser < Connection::HTTP1
80
- def headline_uri(request)
81
- request.uri.to_s
80
+ module ProxyParser
81
+ def join_headline(request)
82
+ return super if request.verb == :connect
83
+
84
+ "#{request.verb.to_s.upcase} #{request.uri} HTTP/#{@version.join(".")}"
82
85
  end
83
86
 
84
87
  def set_protocol_headers(request)
@@ -91,22 +94,6 @@ module HTTPX
91
94
  end
92
95
  end
93
96
 
94
- class ConnectProxyParser < ProxyParser
95
- attr_reader :pending
96
-
97
- def headline_uri(request)
98
- return super unless request.verb == :connect
99
-
100
- tunnel = request.path
101
- log { "establishing HTTP proxy tunnel to #{tunnel}" }
102
- tunnel
103
- end
104
-
105
- def empty?
106
- @requests.reject { |r| r.verb == :connect }.empty? || @requests.all? { |request| !request.response.nil? }
107
- end
108
- end
109
-
110
97
  class ConnectRequest < Request
111
98
  def initialize(uri, _options)
112
99
  super(:connect, uri, {})
@@ -27,7 +27,7 @@ module HTTPX
27
27
 
28
28
  private
29
29
 
30
- def transition(nextstate)
30
+ def handle_transition(nextstate)
31
31
  return super unless @options.proxy && PROTOCOLS.include?(@options.proxy.uri.scheme)
32
32
 
33
33
  case nextstate
@@ -46,7 +46,7 @@ module HTTPX
46
46
 
47
47
  private
48
48
 
49
- def transition(nextstate)
49
+ def handle_transition(nextstate)
50
50
  return super unless @options.proxy && @options.proxy.uri.scheme == "socks5"
51
51
 
52
52
  case nextstate
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "resolv"
4
- require "ipaddr"
5
- require "forwardable"
6
-
7
3
  module HTTPX
8
4
  class HTTPProxyError < Error; end
9
5
 
@@ -85,7 +81,7 @@ module HTTPX
85
81
  end
86
82
  uris
87
83
  end
88
- options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
84
+ { uri: @_proxy_uris.first } unless @_proxy_uris.empty?
89
85
  end
90
86
 
91
87
  def find_connection(request, connections, options)
@@ -109,12 +105,15 @@ module HTTPX
109
105
  return super unless proxy
110
106
 
111
107
  connection = options.connection_class.new("tcp", uri, options)
112
- pool.init_connection(connection, options)
113
- connection
108
+ catch(:coalesced) do
109
+ pool.init_connection(connection, options)
110
+ connection
111
+ end
114
112
  end
115
113
 
116
114
  def fetch_response(request, connections, options)
117
115
  response = super
116
+
118
117
  if response.is_a?(ErrorResponse) &&
119
118
  __proxy_error?(response) && !@_proxy_uris.empty?
120
119
  @_proxy_uris.shift
@@ -138,10 +137,20 @@ module HTTPX
138
137
  error = response.error
139
138
  case error
140
139
  when NativeResolveError
140
+ return false unless @_proxy_uris && !@_proxy_uris.empty?
141
+
142
+ proxy_uri = URI(@_proxy_uris.first)
143
+
144
+ origin = error.connection.origin
145
+
141
146
  # failed resolving proxy domain
142
- error.connection.origin.to_s == @_proxy_uris.first
147
+ origin.host == proxy_uri.host && origin.port == proxy_uri.port
143
148
  when ResolveError
144
- error.message.end_with?(@_proxy_uris.first)
149
+ return false unless @_proxy_uris && !@_proxy_uris.empty?
150
+
151
+ proxy_uri = URI(@_proxy_uris.first)
152
+
153
+ error.message.end_with?(proxy_uri.to_s)
145
154
  when *PROXY_ERRORS
146
155
  # timeout errors connecting to proxy
147
156
  true
@@ -160,7 +169,9 @@ module HTTPX
160
169
 
161
170
  # redefining the connection origin as the proxy's URI,
162
171
  # as this will be used as the tcp peer ip.
163
- @origin = URI(@options.proxy.uri.origin)
172
+ proxy_uri = URI(@options.proxy.uri)
173
+ @origin.host = proxy_uri.host
174
+ @origin.port = proxy_uri.port
164
175
  end
165
176
 
166
177
  def match?(uri, options)
@@ -169,11 +180,20 @@ module HTTPX
169
180
  super && @options.proxy == options.proxy
170
181
  end
171
182
 
172
- # should not coalesce connections here, as the IP is the IP of the proxy
173
- def coalescable?(*)
183
+ def coalescable?(connection)
174
184
  return super unless @options.proxy
175
185
 
176
- false
186
+ if @io.protocol == "h2" &&
187
+ @origin.scheme == "https" &&
188
+ connection.origin.scheme == "https" &&
189
+ @io.can_verify_peer?
190
+ # in proxied connections, .origin is the proxy ; Given names
191
+ # are stored in .origins, this is what is used.
192
+ origin = URI(connection.origins.first)
193
+ @io.verify_hostname(origin.host)
194
+ else
195
+ @origin == connection.origin
196
+ end
177
197
  end
178
198
 
179
199
  def send(request)
@@ -222,13 +242,13 @@ module HTTPX
222
242
  end
223
243
  end
224
244
 
225
- def transition(nextstate)
245
+ def handle_transition(nextstate)
226
246
  return super unless @options.proxy
227
247
 
228
248
  case nextstate
229
249
  when :closing
230
250
  # this is a hack so that we can use the super method
231
- # and it'll thing that the current state is open
251
+ # and it'll think that the current state is open
232
252
  @state = :open if @state == :connecting
233
253
  end
234
254
  super
@@ -12,16 +12,19 @@ module HTTPX
12
12
  # TODO: pass max_retries in a configure/load block
13
13
 
14
14
  IDEMPOTENT_METHODS = %i[get options head put delete].freeze
15
- RETRYABLE_ERRORS = [IOError,
16
- EOFError,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- TLSError,
21
- TimeoutError,
22
- Parser::Error,
23
- Errno::EINVAL,
24
- Errno::ETIMEDOUT].freeze
15
+ RETRYABLE_ERRORS = [
16
+ IOError,
17
+ EOFError,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::EINVAL,
22
+ Errno::ETIMEDOUT,
23
+ Parser::Error,
24
+ TLSError,
25
+ TimeoutError,
26
+ Connection::HTTP2::GoawayError,
27
+ ].freeze
25
28
  DEFAULT_JITTER = ->(interval) { interval * (0.5 * (1 + rand)) }
26
29
 
27
30
  if ENV.key?("HTTPX_NO_JITTER")
@@ -38,7 +41,7 @@ module HTTPX
38
41
  def option_retry_after(value)
39
42
  # return early if callable
40
43
  unless value.respond_to?(:call)
41
- value = Integer(value)
44
+ value = Float(value)
42
45
  raise TypeError, ":retry_after must be positive" unless value.positive?
43
46
  end
44
47
 
@@ -93,8 +96,8 @@ module HTTPX
93
96
  # rubocop:enable Style/MultilineTernaryOperator
94
97
  )
95
98
  response.close if response.respond_to?(:close)
96
- request.retries -= 1
97
99
  log { "failed to get response, #{request.retries} tries to go..." }
100
+ request.retries -= 1
98
101
  request.transition(:idle)
99
102
 
100
103
  retry_after = options.retry_after