httpx 1.6.1 → 1.6.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_6_2.md +11 -0
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/lib/httpx/adapters/datadog.rb +15 -11
  5. data/lib/httpx/adapters/sentry.rb +1 -1
  6. data/lib/httpx/connection/http1.rb +9 -9
  7. data/lib/httpx/connection/http2.rb +14 -15
  8. data/lib/httpx/connection.rb +119 -102
  9. data/lib/httpx/extensions.rb +0 -14
  10. data/lib/httpx/io/ssl.rb +1 -1
  11. data/lib/httpx/loggable.rb +12 -2
  12. data/lib/httpx/options.rb +20 -0
  13. data/lib/httpx/plugins/callbacks.rb +15 -1
  14. data/lib/httpx/plugins/digest_auth.rb +1 -1
  15. data/lib/httpx/plugins/proxy/http.rb +37 -9
  16. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  17. data/lib/httpx/plugins/response_cache.rb +13 -2
  18. data/lib/httpx/plugins/stream_bidi.rb +15 -6
  19. data/lib/httpx/pool.rb +53 -19
  20. data/lib/httpx/request.rb +3 -13
  21. data/lib/httpx/resolver/https.rb +35 -19
  22. data/lib/httpx/resolver/multi.rb +9 -32
  23. data/lib/httpx/resolver/native.rb +46 -38
  24. data/lib/httpx/resolver/resolver.rb +45 -28
  25. data/lib/httpx/resolver/system.rb +63 -39
  26. data/lib/httpx/selector.rb +35 -20
  27. data/lib/httpx/session.rb +18 -28
  28. data/lib/httpx/transcoder/deflate.rb +13 -8
  29. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  30. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  31. data/lib/httpx/version.rb +1 -1
  32. data/sig/connection.rbs +12 -3
  33. data/sig/loggable.rbs +5 -1
  34. data/sig/options.rbs +5 -1
  35. data/sig/plugins/callbacks.rbs +3 -0
  36. data/sig/plugins/stream_bidi.rbs +3 -5
  37. data/sig/resolver/https.rbs +2 -0
  38. data/sig/resolver/multi.rbs +0 -9
  39. data/sig/resolver/native.rbs +0 -2
  40. data/sig/resolver/resolver.rbs +9 -8
  41. data/sig/resolver/system.rbs +4 -2
  42. data/sig/selector.rbs +2 -0
  43. data/sig/session.rbs +5 -3
  44. metadata +5 -1
@@ -16,7 +16,7 @@ module HTTPX
16
16
  # The streams keeps send DATA frames while there's data; when they're ain't,
17
17
  # the stream is kept open; it must be explicitly closed by the end user.
18
18
  #
19
- class HTTP2Bidi < Connection::HTTP2
19
+ module HTTP2Methods
20
20
  def initialize(*)
21
21
  super
22
22
  @lock = Thread::Mutex.new
@@ -117,8 +117,11 @@ module HTTPX
117
117
  # which allows it to be registered in the selector alongside actual HTTP-based
118
118
  # HTTPX::Connection objects.
119
119
  class Signal
120
+ attr_reader :error
121
+
120
122
  def initialize
121
123
  @closed = false
124
+ @error = nil
122
125
  @pipe_read, @pipe_write = IO.pipe
123
126
  end
124
127
 
@@ -159,6 +162,11 @@ module HTTPX
159
162
  @closed = true
160
163
  end
161
164
 
165
+ def on_error(error)
166
+ @error = error
167
+ terminate
168
+ end
169
+
162
170
  # noop (the owner connection will take of it)
163
171
  def handle_socket_timeout(interval); end
164
172
  end
@@ -193,6 +201,7 @@ module HTTPX
193
201
 
194
202
  def deselect_connection(connection, *)
195
203
  super
204
+
196
205
  connection.signal = nil
197
206
  end
198
207
  end
@@ -294,14 +303,14 @@ module HTTPX
294
303
  super
295
304
  end
296
305
 
297
- private
298
-
299
- def parser_type(protocol)
300
- return HTTP2Bidi if protocol == "h2"
306
+ def call
307
+ return super unless (error = @signal.error)
301
308
 
302
- super
309
+ on_error(error)
303
310
  end
304
311
 
312
+ private
313
+
305
314
  def set_parser_callbacks(parser)
306
315
  super
307
316
  parser.on(:flush_buffer) do
data/lib/httpx/pool.rb CHANGED
@@ -8,7 +8,6 @@ require "httpx/resolver"
8
8
 
9
9
  module HTTPX
10
10
  class Pool
11
- using ArrayExtensions::FilterMap
12
11
  using URIExtensions
13
12
 
14
13
  POOL_TIMEOUT = 5
@@ -51,32 +50,53 @@ module HTTPX
51
50
  acquire_connection(uri, options) || begin
52
51
  if @connections_counter == @max_connections
53
52
  # this takes precedence over per-origin
54
- @max_connections_cond.wait(@connection_mtx, @pool_timeout)
55
53
 
56
- if (conn = acquire_connection(uri, options))
57
- return conn
58
- end
54
+ expires_at = Utils.now + @pool_timeout
59
55
 
60
- if @connections_counter == @max_connections
61
- # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
62
- # this means that all of them are persistent or being used, so raise a timeout error.
63
- conn = @connections.find { |c| c.state == :closed }
56
+ loop do
57
+ @max_connections_cond.wait(@connection_mtx, @pool_timeout)
64
58
 
65
- raise PoolTimeoutError.new(@pool_timeout,
66
- "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
59
+ if (conn = acquire_connection(uri, options))
60
+ return conn
61
+ end
62
+
63
+ # if one can afford to create a new connection, do it
64
+ break unless @connections_counter == @max_connections
65
+
66
+ # if no matching usable connection was found, the pool will make room and drop a closed connection.
67
+ if (conn = @connections.find { |c| c.state == :closed })
68
+ drop_connection(conn)
69
+ break
70
+ end
71
+
72
+ # happens when a condition was signalled, but another thread snatched the available connection before
73
+ # context was passed back here.
74
+ next if Utils.now < expires_at
67
75
 
68
- drop_connection(conn)
76
+ raise PoolTimeoutError.new(@pool_timeout,
77
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection")
69
78
  end
70
79
 
71
80
  end
72
81
 
73
82
  if @origin_counters[uri.origin] == @max_connections_per_origin
74
83
 
75
- @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
84
+ expires_at = Utils.now + @pool_timeout
85
+
86
+ loop do
87
+ @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
88
+
89
+ if (conn = acquire_connection(uri, options))
90
+ return conn
91
+ end
92
+
93
+ # happens when a condition was signalled, but another thread snatched the available connection before
94
+ # context was passed back here.
95
+ next if Utils.now < expires_at
76
96
 
77
- return acquire_connection(uri, options) ||
78
- raise(PoolTimeoutError.new(@pool_timeout,
79
- "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
97
+ raise(PoolTimeoutError.new(@pool_timeout,
98
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
99
+ end
80
100
  end
81
101
 
82
102
  @connections_counter += 1
@@ -91,6 +111,13 @@ module HTTPX
91
111
  return if connection.options.io
92
112
 
93
113
  @connection_mtx.synchronize do
114
+ if connection.coalesced? || connection.state == :idle
115
+ # when connections coalesce
116
+ drop_connection(connection)
117
+
118
+ return
119
+ end
120
+
94
121
  @connections << connection
95
122
 
96
123
  @max_connections_cond.signal
@@ -128,10 +155,16 @@ module HTTPX
128
155
  end
129
156
 
130
157
  def checkin_resolver(resolver)
131
- @resolver_mtx.synchronize do
132
- resolvers = @resolvers[resolver.class]
158
+ resolver_class = resolver.class
159
+
160
+ resolver = resolver.multi
133
161
 
134
- resolver = resolver.multi
162
+ # a multi requires all sub-resolvers being closed in order to be
163
+ # correctly checked back in.
164
+ return unless resolver.closed?
165
+
166
+ @resolver_mtx.synchronize do
167
+ resolvers = @resolvers[resolver_class]
135
168
 
136
169
  resolvers << resolver unless resolvers.include?(resolver)
137
170
  end
@@ -140,6 +173,7 @@ module HTTPX
140
173
  # :nocov:
141
174
  def inspect
142
175
  "#<#{self.class}:#{object_id} " \
176
+ "@max_connections=#{@max_connections} " \
143
177
  "@max_connections_per_origin=#{@max_connections_per_origin} " \
144
178
  "@pool_timeout=#{@pool_timeout} " \
145
179
  "@connections=#{@connections.size}>"
data/lib/httpx/request.rb CHANGED
@@ -14,9 +14,6 @@ module HTTPX
14
14
 
15
15
  ALLOWED_URI_SCHEMES = %w[https http].freeze
16
16
 
17
- # default value used for "user-agent" header, when not overridden.
18
- USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
19
-
20
17
  # the upcased string HTTP verb for this request.
21
18
  attr_reader :verb
22
19
 
@@ -75,16 +72,6 @@ module HTTPX
75
72
  @headers = options.headers.dup
76
73
  merge_headers(params.delete(:headers)) if params.key?(:headers)
77
74
 
78
- @headers["user-agent"] ||= USER_AGENT
79
- @headers["accept"] ||= "*/*"
80
-
81
- # forego compression in the Range request case
82
- if @headers.key?("range")
83
- @headers.delete("accept-encoding")
84
- else
85
- @headers["accept-encoding"] ||= options.supported_compression_formats
86
- end
87
-
88
75
  @query_params = params.delete(:params) if params.key?(:params)
89
76
 
90
77
  @body = options.request_body_class.new(@headers, options, **params)
@@ -166,6 +153,9 @@ module HTTPX
166
153
  # merges +h+ into the instance of HTTPX::Headers of the request.
167
154
  def merge_headers(h)
168
155
  @headers = @headers.merge(h)
156
+ return unless @headers.key?("range")
157
+
158
+ @headers.delete("accept-encoding")
169
159
  end
170
160
 
171
161
  # the URI scheme of the request +uri+.
@@ -30,7 +30,8 @@ module HTTPX
30
30
  use_get: false,
31
31
  }.freeze
32
32
 
33
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?, :handle_socket_timeout
33
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close,
34
+ :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
34
35
 
35
36
  def initialize(_, options)
36
37
  super
@@ -52,29 +53,24 @@ module HTTPX
52
53
  if @uri_addresses.empty?
53
54
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
54
55
  ex.set_backtrace(caller)
55
- connection.force_reset
56
+ connection.force_close
56
57
  throw(:resolve_error, ex)
57
58
  end
58
59
 
59
60
  resolve(connection)
60
61
  end
61
62
 
62
- # This is already indirectly monitored bt the HTTP connection. In order to skip
63
- # monitoring, this method returns <tt>true</tt>.
64
- def closed?
65
- true
66
- end
67
-
68
- def empty?
69
- true
70
- end
71
-
72
63
  def resolver_connection
73
64
  # TODO: leaks connection object into the pool
74
- @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
75
- @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
76
- emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
77
- end
65
+ @resolver_connection ||=
66
+ @current_session.find_connection(
67
+ @uri,
68
+ @current_selector,
69
+ @options.merge(resolver_class: :system, ssl: { alpn_protocols: %w[h2] })
70
+ ).tap do |conn|
71
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
72
+ conn.on(:force_closed, &method(:force_close))
73
+ end
78
74
  end
79
75
 
80
76
  private
@@ -111,7 +107,9 @@ module HTTPX
111
107
  @connections << connection
112
108
  rescue ResolveError, Resolv::DNS::EncodeError => e
113
109
  reset_hostname(hostname)
110
+ throw(:resolve_error, e) if connection.pending.empty?
114
111
  emit_resolve_error(connection, connection.peer.host, e)
112
+ close_or_resolve
115
113
  end
116
114
  end
117
115
 
@@ -121,6 +119,7 @@ module HTTPX
121
119
  hostname = @requests.delete(request)
122
120
  connection = reset_hostname(hostname)
123
121
  emit_resolve_error(connection, connection.peer.host, e)
122
+ close_or_resolve
124
123
  else
125
124
  # @type var response: HTTPX::Response
126
125
  parse(request, response)
@@ -147,6 +146,7 @@ module HTTPX
147
146
 
148
147
  unless @queries.value?(connection)
149
148
  emit_resolve_error(connection)
149
+ close_or_resolve
150
150
  return
151
151
  end
152
152
 
@@ -156,10 +156,12 @@ module HTTPX
156
156
  connection = reset_hostname(host)
157
157
 
158
158
  emit_resolve_error(connection)
159
+ close_or_resolve
159
160
  when :decode_error
160
161
  host = @requests.delete(request)
161
162
  connection = reset_hostname(host)
162
163
  emit_resolve_error(connection, connection.peer.host, result)
164
+ close_or_resolve
163
165
  end
164
166
  end
165
167
 
@@ -169,6 +171,7 @@ module HTTPX
169
171
  host = @requests.delete(request)
170
172
  connection = reset_hostname(host)
171
173
  emit_resolve_error(connection)
174
+ close_or_resolve
172
175
  return
173
176
 
174
177
  else
@@ -207,9 +210,7 @@ module HTTPX
207
210
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
208
211
  end
209
212
  end
210
- return if @connections.empty?
211
-
212
- resolve
213
+ close_or_resolve(true)
213
214
  end
214
215
 
215
216
  def build_request(hostname)
@@ -252,5 +253,20 @@ module HTTPX
252
253
 
253
254
  connection
254
255
  end
256
+
257
+ def close_or_resolve(should_deactivate = false)
258
+ # drop already closed connections
259
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
260
+
261
+ if (@connections - @queries.values).empty?
262
+ if should_deactivate
263
+ deactivate
264
+ else
265
+ disconnect
266
+ end
267
+ else
268
+ resolve
269
+ end
270
+ end
255
271
  end
256
272
  end
@@ -5,9 +5,6 @@ require "resolv"
5
5
 
6
6
  module HTTPX
7
7
  class Resolver::Multi
8
- include Callbacks
9
- using ArrayExtensions::FilterMap
10
-
11
8
  attr_reader :resolvers, :options
12
9
 
13
10
  def initialize(resolver_type, options)
@@ -28,52 +25,32 @@ module HTTPX
28
25
 
29
26
  def current_selector=(s)
30
27
  @current_selector = s
31
- @resolvers.each { |r| r.__send__(__method__, s) }
28
+ @resolvers.each { |r| r.current_selector = s }
32
29
  end
33
30
 
34
31
  def current_session=(s)
35
32
  @current_session = s
36
- @resolvers.each { |r| r.__send__(__method__, s) }
33
+ @resolvers.each { |r| r.current_session = s }
37
34
  end
38
35
 
39
36
  def log(*args, **kwargs, &blk)
40
- @resolvers.each { |r| r.__send__(__method__, *args, **kwargs, &blk) }
37
+ @resolvers.each { |r| r.log(*args, **kwargs, &blk) }
41
38
  end
42
39
 
43
40
  def closed?
44
41
  @resolvers.all?(&:closed?)
45
42
  end
46
43
 
47
- def empty?
48
- @resolvers.all?(&:empty?)
49
- end
50
-
51
- def inflight?
52
- @resolvers.any(&:inflight?)
53
- end
54
-
55
- def timeout
56
- @resolvers.filter_map(&:timeout).min
57
- end
58
-
59
- def close
60
- @resolvers.each(&:close)
61
- end
62
-
63
- def connections
64
- @resolvers.filter_map { |r| r.resolver_connection if r.respond_to?(:resolver_connection) }
65
- end
66
-
67
44
  def early_resolve(connection)
68
45
  hostname = connection.peer.host
69
46
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
70
47
  return false unless addresses
71
48
 
72
- ip_families = connection.options.ip_families || Resolver.supported_ip_families
49
+ ip_families = connection.options.ip_families
73
50
 
74
51
  resolved = false
75
52
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
76
- next unless ip_families.include?(family)
53
+ next unless ip_families.nil? || ip_families.include?(family)
77
54
 
78
55
  # try to match the resolver by family. However, there are cases where that's not possible, as when
79
56
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
@@ -91,14 +68,14 @@ module HTTPX
91
68
  end
92
69
 
93
70
  def lazy_resolve(connection)
94
- ip_families = connection.options.ip_families || Resolver.supported_ip_families
95
-
96
71
  @resolvers.each do |resolver|
97
- next unless ip_families.include?(resolver.family)
72
+ conn_to_resolve = @current_session.try_clone_connection(connection, @current_selector, resolver.family)
73
+ resolver << conn_to_resolve
98
74
 
99
- resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
100
75
  next if resolver.empty?
101
76
 
77
+ # both the resolver and the connection it's resolving must be pineed to the session
78
+ @current_session.pin(conn_to_resolve, @current_selector)
102
79
  @current_session.select_resolver(resolver, @current_selector)
103
80
  end
104
81
  end
@@ -49,8 +49,19 @@ module HTTPX
49
49
  transition(:closed)
50
50
  end
51
51
 
52
+ def force_close(*)
53
+ @timer.cancel if @timer
54
+ @timer = @name = nil
55
+ @queries.clear
56
+ @timeouts.clear
57
+ close
58
+ super
59
+ ensure
60
+ terminate
61
+ end
62
+
52
63
  def terminate
53
- emit(:close, self)
64
+ disconnect
54
65
  end
55
66
 
56
67
  def closed?
@@ -84,7 +95,7 @@ module HTTPX
84
95
  if @nameserver.nil?
85
96
  ex = ResolveError.new("No available nameserver")
86
97
  ex.set_backtrace(caller)
87
- connection.force_reset
98
+ connection.force_close
88
99
  throw(:resolve_error, ex)
89
100
  else
90
101
  @connections << connection
@@ -93,15 +104,34 @@ module HTTPX
93
104
  end
94
105
 
95
106
  def timeout
96
- return if @connections.empty?
107
+ return unless @name
97
108
 
98
109
  @start_timeout = Utils.now
99
- hosts = @queries.keys
100
- @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
110
+
111
+ timeouts = @timeouts[@name]
112
+
113
+ return if timeouts.empty?
114
+
115
+ log(level: 2) { "resolver #{FAMILY_TYPES[@record_type]}: next timeout #{timeouts.first} secs... (#{timeouts.size - 1} left)" }
116
+
117
+ timeouts.first
101
118
  end
102
119
 
103
120
  def handle_socket_timeout(interval); end
104
121
 
122
+ def handle_error(error)
123
+ if error.respond_to?(:connection) &&
124
+ error.respond_to?(:host)
125
+ reset_hostname(error.host, connection: error.connection)
126
+ else
127
+ @queries.each do |host, connection|
128
+ reset_hostname(host, connection: connection)
129
+ end
130
+ end
131
+
132
+ super
133
+ end
134
+
105
135
  private
106
136
 
107
137
  def calculate_interests
@@ -118,7 +148,6 @@ module HTTPX
118
148
 
119
149
  break unless calculate_interests == :w
120
150
 
121
- # do_retry
122
151
  dwrite
123
152
 
124
153
  break unless calculate_interests == :r
@@ -133,7 +162,7 @@ module HTTPX
133
162
  retry
134
163
  else
135
164
  handle_error(e)
136
- emit(:close, self)
165
+ disconnect
137
166
  end
138
167
  rescue NativeResolveError => e
139
168
  handle_error(e)
@@ -154,7 +183,7 @@ module HTTPX
154
183
  @timer = @current_selector.after(timeout) do
155
184
  next unless @connections.include?(connection)
156
185
 
157
- @timer = nil
186
+ @timer = @name = nil
158
187
 
159
188
  do_retry(h, connection, timeout)
160
189
  end
@@ -178,8 +207,6 @@ module HTTPX
178
207
  @timeouts.clear
179
208
  resolve(connection, h)
180
209
  else
181
-
182
- @timeouts.delete(h)
183
210
  reset_hostname(h, reset_candidates: false)
184
211
 
185
212
  unless @queries.empty?
@@ -273,7 +300,7 @@ module HTTPX
273
300
  def parse(buffer)
274
301
  @timer.cancel
275
302
 
276
- @timer = nil
303
+ @timer = @name = nil
277
304
 
278
305
  code, result = Resolver.decode_dns_answer(buffer)
279
306
 
@@ -431,13 +458,14 @@ module HTTPX
431
458
  def generate_candidates(name)
432
459
  return [name] if name.end_with?(".")
433
460
 
434
- candidates = []
435
461
  name_parts = name.scan(/[^.]+/)
436
- candidates = [name] if @ndots <= name_parts.size - 1
437
- candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") })
462
+ candidates = @search.map { |domain| [*name_parts, *domain].join(".") }
438
463
  fname = "#{name}."
439
- candidates << fname unless candidates.include?(fname)
440
-
464
+ if @ndots <= name_parts.size - 1
465
+ candidates.unshift(fname)
466
+ else
467
+ candidates << fname
468
+ end
441
469
  candidates
442
470
  end
443
471
 
@@ -498,27 +526,7 @@ module HTTPX
498
526
  ConnectTimeoutError => e
499
527
  # these errors may happen during TCP handshake
500
528
  # treat them as resolve errors.
501
- handle_error(e)
502
- emit(:close, self)
503
- end
504
-
505
- def handle_error(error)
506
- if error.respond_to?(:connection) &&
507
- error.respond_to?(:host)
508
- reset_hostname(error.host, connection: error.connection)
509
- @connections.delete(error.connection)
510
- emit_resolve_error(error.connection, error.host, error)
511
- else
512
- @queries.each do |host, connection|
513
- reset_hostname(host, connection: connection)
514
- @connections.delete(connection)
515
- emit_resolve_error(connection, host, error)
516
- end
517
-
518
- while (connection = @connections.shift)
519
- emit_resolve_error(connection, connection.peer.host, error)
520
- end
521
- end
529
+ on_error(e)
522
530
  end
523
531
 
524
532
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -538,7 +546,7 @@ module HTTPX
538
546
  @connections.shift until @connections.empty? || @connections.first.state != :closed
539
547
 
540
548
  if (@connections - @queries.values).empty?
541
- emit(:close, self)
549
+ disconnect
542
550
  else
543
551
  resolve
544
552
  end