httpx 0.18.0 → 0.19.3

Sign up to get free protection for your applications and to get access to all the features.
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