httpx 0.18.7 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_19_0.md +39 -0
  3. data/lib/httpx/adapters/faraday.rb +7 -3
  4. data/lib/httpx/connection.rb +14 -10
  5. data/lib/httpx/extensions.rb +16 -0
  6. data/lib/httpx/headers.rb +0 -2
  7. data/lib/httpx/io/tcp.rb +24 -5
  8. data/lib/httpx/options.rb +44 -11
  9. data/lib/httpx/plugins/cookies.rb +5 -7
  10. data/lib/httpx/plugins/proxy.rb +2 -5
  11. data/lib/httpx/plugins/retries.rb +1 -1
  12. data/lib/httpx/pool.rb +40 -20
  13. data/lib/httpx/resolver/https.rb +32 -42
  14. data/lib/httpx/resolver/multi.rb +79 -0
  15. data/lib/httpx/resolver/native.rb +24 -39
  16. data/lib/httpx/resolver/resolver.rb +92 -0
  17. data/lib/httpx/resolver/system.rb +175 -19
  18. data/lib/httpx/resolver.rb +37 -11
  19. data/lib/httpx/response.rb +4 -2
  20. data/lib/httpx/session_extensions.rb +9 -2
  21. data/lib/httpx/timers.rb +1 -1
  22. data/lib/httpx/transcoder/chunker.rb +0 -1
  23. data/lib/httpx/version.rb +1 -1
  24. data/lib/httpx.rb +2 -0
  25. data/sig/errors.rbs +8 -0
  26. data/sig/headers.rbs +0 -2
  27. data/sig/httpx.rbs +4 -0
  28. data/sig/options.rbs +10 -7
  29. data/sig/parser/http1.rbs +14 -5
  30. data/sig/pool.rbs +17 -9
  31. data/sig/registry.rbs +3 -0
  32. data/sig/request.rbs +11 -0
  33. data/sig/resolver/https.rbs +15 -27
  34. data/sig/resolver/multi.rbs +7 -0
  35. data/sig/resolver/native.rbs +3 -12
  36. data/sig/resolver/resolver.rbs +36 -0
  37. data/sig/resolver/system.rbs +3 -9
  38. data/sig/resolver.rbs +12 -10
  39. data/sig/response.rbs +15 -5
  40. data/sig/selector.rbs +3 -3
  41. data/sig/timers.rbs +5 -2
  42. data/sig/transcoder/chunker.rbs +16 -5
  43. data/sig/transcoder/json.rbs +5 -0
  44. data/sig/transcoder.rbs +3 -1
  45. metadata +9 -5
  46. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  47. data/sig/resolver/resolver_mixin.rbs +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab9b9577b0e266b140e21030307b3f2436af346a13f1e0931af05020ba42f7e6
4
- data.tar.gz: c5d043448ef41b0532c08d1622540d80c68511ef59cffcc810a6f8bd92f348d5
3
+ metadata.gz: 1d2af1a18388c76620d8a5b28e0f1d2dd90a4d2d0ee2c514be85ea7ba1b05c45
4
+ data.tar.gz: ca44331c601c46d51813cd5ccab8b258e94cf517913e26b78716885234e6cf86
5
5
  SHA512:
6
- metadata.gz: 8b4ec58f2ab031d23f28a86bc47b40d0f0d79eea9aea64590b0cc3d07269738513de1364608e0c0282a6956d816b78b900520dbad4daab1f73522b8b7f73d138
7
- data.tar.gz: f2d2c297b530bfb3567ebcd1678bad1d729187dd75c9107839c925def1a708c4648fe80df614a01af02bce309594e256e274e042f3c6b8347182aebff1df9935
6
+ metadata.gz: eac8b6a85fdd44dda2bcaf000cc7dc5dfa19b075165b52fa709dd8b575e9b4a919293e724242d2449794b74e28cf34c00687c6f99e23baee563c45626429b3c7
7
+ data.tar.gz: 564f2e8f6d580070eaffa3cda012f0efb032948ce4851b96f947cd5a004d1e8fde9496f29031b341709ccc44040ea3473304697e5d5c7c659b7c2cd286702b78
@@ -0,0 +1,39 @@
1
+ # 0.19.0
2
+
3
+ ## Features
4
+
5
+ ### Happy Eyeballs v2
6
+
7
+ When the system supports dual-stack networking, `httpx` implements the Happy Eyeballs v2 algorithm (RFC 8305) to resolve hostnames to both IPv6 and IPv4 addresses while privileging IPv6 connectivity. This is implemented by `httpx` both for the `:native` as well as the `:https` (DoH) resolver (which do not perform address sorting, thereby being "DNS-based load-balancing" friendly), and "outsourced" to `getaddrinfo` when using the `:system` resolver.
8
+
9
+ IPv6 connectivity will also be privileged for `/etc/hosts` local DNS (i.e. `localhost` connections will connec to `::1`).
10
+
11
+ A new option, `:ip_families`, will also be available (`[Socket::AF_INET6, Socket::AF_INET]` in dual-stack systems). If you'd like to i.e. force IPv4 connectivity, you can do use it (`client = HTTPX.with(ip_families: [Socket::AF_INET])`).
12
+
13
+ ## Improvements
14
+
15
+ ### DNS: :system resolver uses getaddrinfo (instead of the resolver lib)
16
+
17
+ The `:system` resolver switched to using the `getaddinfo` system function to perform DNS requests. Not only is this call **not** blocking the session event loop anymore (unlike pre-0.19.0 `:system` resolver), it adds a lot of functionality that the stdlib `resolv` library just doesn't support at the moment (such as SRV records).
18
+
19
+ ### HTTP/2 proxy support
20
+
21
+ The `:proxy` plugin handles "prior-knowledge" HTTP/2 proxies.
22
+
23
+ ```ruby
24
+ HTTPX.plugin(:proxy, fallback_protocol: "h2").with_proxy(uri: "http://http2-proxy:3128").get(...
25
+ ```
26
+
27
+ Connection coalescing has also been enabled for proxied connections (also `CONNECT`-tunneled connections).
28
+
29
+ ### curl-to-httpx
30
+
31
+ widget in [project website](https://honeyryderchuck.gitlab.io/httpx/) to turn curl commands into the equivalent `httpx` code.
32
+
33
+ ## Bugfixes
34
+
35
+ * faraday adapter now supports passing session options.
36
+ * proxy: several fixes which enabled env-var (`HTTP(S)_PROXY`) defined proxy support.
37
+ * proxy: fixed graceful recovery from proxy tcp connect errors.
38
+ * several fixes around CNAMEs timeouts with the native resolver.
39
+ * https resolver is now closed when wrapping session closes (it was left open).
@@ -81,6 +81,9 @@ module Faraday
81
81
 
82
82
  def response=(response)
83
83
  super
84
+
85
+ return if response.is_a?(::HTTPX::ErrorResponse)
86
+
84
87
  response.body.on_data = @response_on_data
85
88
  end
86
89
  end
@@ -136,7 +139,7 @@ module Faraday
136
139
 
137
140
  def on_response(&blk)
138
141
  if blk
139
- @on_response = lambda do |response|
142
+ @on_response = ->(response) do
140
143
  blk.call(response)
141
144
  end
142
145
  self
@@ -200,9 +203,9 @@ module Faraday
200
203
  end
201
204
  end
202
205
 
203
- def initialize(app)
206
+ def initialize(app, options = {})
204
207
  super(app)
205
- @session = Session.new
208
+ @session = Session.new(options)
206
209
  end
207
210
 
208
211
  def call(env)
@@ -210,6 +213,7 @@ module Faraday
210
213
  if parallel?(env)
211
214
  handler = env[:parallel_manager].enqueue(env)
212
215
  handler.on_response do |response|
216
+ response.raise_for_status
213
217
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
214
218
  response_headers.merge!(response.headers)
215
219
  end
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  def_delegator :@write_buffer, :empty?
46
46
 
47
- attr_reader :origin, :origins, :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
@@ -490,14 +494,14 @@ module HTTPX
490
494
 
491
495
  def transition(nextstate)
492
496
  handle_transition(nextstate)
493
- rescue Errno::ECONNREFUSED,
494
- Errno::EADDRNOTAVAIL,
495
- Errno::EHOSTUNREACH,
496
- TLSError => e
497
- # connect errors, exit gracefully
498
- handle_error(e)
499
- @state = :closed
500
- emit(:close)
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)
501
505
  end
502
506
 
503
507
  def handle_transition(nextstate)
@@ -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/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,7 +67,7 @@ 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
@@ -62,12 +77,16 @@ module HTTPX
62
77
  Errno::EHOSTUNREACH => e
63
78
  raise e if @ip_index <= 0
64
79
 
80
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
65
81
  @ip_index -= 1
82
+ @io = build_socket
66
83
  retry
67
84
  rescue Errno::ETIMEDOUT => e
68
85
  raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
69
86
 
87
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
70
88
  @ip_index -= 1
89
+ @io = build_socket
71
90
  retry
72
91
  end
73
92
 
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
@@ -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
 
@@ -117,6 +113,7 @@ module HTTPX
117
113
 
118
114
  def fetch_response(request, connections, options)
119
115
  response = super
116
+
120
117
  if response.is_a?(ErrorResponse) &&
121
118
  __proxy_error?(response) && !@_proxy_uris.empty?
122
119
  @_proxy_uris.shift
@@ -251,7 +248,7 @@ module HTTPX
251
248
  case nextstate
252
249
  when :closing
253
250
  # this is a hack so that we can use the super method
254
- # and it'll thing that the current state is open
251
+ # and it'll think that the current state is open
255
252
  @state = :open if @state == :connecting
256
253
  end
257
254
  super
@@ -96,8 +96,8 @@ module HTTPX
96
96
  # rubocop:enable Style/MultilineTernaryOperator
97
97
  )
98
98
  response.close if response.respond_to?(:close)
99
- request.retries -= 1
100
99
  log { "failed to get response, #{request.retries} tries to go..." }
100
+ request.retries -= 1
101
101
  request.transition(:idle)
102
102
 
103
103
  retry_after = options.retry_after
data/lib/httpx/pool.rb CHANGED
@@ -14,7 +14,6 @@ module HTTPX
14
14
 
15
15
  def initialize
16
16
  @resolvers = {}
17
- @_resolver_ios = {}
18
17
  @timers = Timers.new
19
18
  @selector = Selector.new
20
19
  @connections = []
@@ -56,9 +55,20 @@ module HTTPX
56
55
  connections = connections.reject(&:inflight?)
57
56
  connections.each(&:close)
58
57
  next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
58
+
59
+ # close resolvers
60
+ outstanding_connections = @connections
61
+ resolver_connections = @resolvers.each_value.flat_map(&:connections).compact
62
+ outstanding_connections -= resolver_connections
63
+
64
+ return unless outstanding_connections.empty?
65
+
59
66
  @resolvers.each_value do |resolver|
60
67
  resolver.close unless resolver.closed?
61
- end if @connections.empty?
68
+ end
69
+ # for https resolver
70
+ resolver_connections.each(&:close)
71
+ next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
62
72
  end
63
73
 
64
74
  def init_connection(connection, _options)
@@ -107,11 +117,12 @@ module HTTPX
107
117
  return
108
118
  end
109
119
 
110
- resolver = find_resolver_for(connection)
111
- resolver << connection
112
- return if resolver.empty?
120
+ find_resolver_for(connection) do |resolver|
121
+ resolver << connection
122
+ next if resolver.empty?
113
123
 
114
- @_resolver_ios[resolver] ||= select_connection(resolver)
124
+ select_connection(resolver)
125
+ end
115
126
  end
116
127
 
117
128
  def on_resolver_connection(connection)
@@ -138,12 +149,11 @@ module HTTPX
138
149
 
139
150
  def on_resolver_close(resolver)
140
151
  resolver_type = resolver.class
141
- return unless @resolvers[resolver_type] == resolver
152
+ return if resolver.closed?
142
153
 
143
154
  @resolvers.delete(resolver_type)
144
155
 
145
156
  deselect_connection(resolver)
146
- @_resolver_ios.delete(resolver)
147
157
  resolver.close unless resolver.closed?
148
158
  end
149
159
 
@@ -174,12 +184,10 @@ module HTTPX
174
184
  end
175
185
 
176
186
  def coalesce_connections(conn1, conn2)
177
- if conn1.coalescable?(conn2)
178
- conn1.merge(conn2)
179
- @connections.delete(conn2)
180
- else
181
- register_connection(conn2)
182
- end
187
+ return register_connection(conn2) unless conn1.coalescable?(conn2)
188
+
189
+ conn1.merge(conn2)
190
+ @connections.delete(conn2)
183
191
  end
184
192
 
185
193
  def next_timeout
@@ -196,13 +204,25 @@ module HTTPX
196
204
  resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
197
205
 
198
206
  @resolvers[resolver_type] ||= begin
199
- resolver = resolver_type.new(connection_options)
200
- resolver.pool = self if resolver.respond_to?(:pool=)
201
- resolver.on(:resolve, &method(:on_resolver_connection))
202
- resolver.on(:error, &method(:on_resolver_error))
203
- resolver.on(:close) { on_resolver_close(resolver) }
204
- resolver
207
+ resolver_manager = if resolver_type.multi?
208
+ Resolver::Multi.new(resolver_type, connection_options)
209
+ else
210
+ resolver_type.new(connection_options)
211
+ end
212
+ resolver_manager.on(:resolve, &method(:on_resolver_connection))
213
+ resolver_manager.on(:error, &method(:on_resolver_error))
214
+ resolver_manager.on(:close, &method(:on_resolver_close))
215
+ resolver_manager
216
+ end
217
+
218
+ manager = @resolvers[resolver_type]
219
+
220
+ (manager.is_a?(Resolver::Multi) && manager.early_resolve(connection)) || manager.resolvers.each do |resolver|
221
+ resolver.pool = self
222
+ yield resolver
205
223
  end
224
+
225
+ manager
206
226
  end
207
227
  end
208
228
  end