httpx 1.6.2 → 1.7.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/sentry.rb +1 -1
  8. data/lib/httpx/altsvc.rb +3 -1
  9. data/lib/httpx/connection/http1.rb +14 -15
  10. data/lib/httpx/connection/http2.rb +16 -15
  11. data/lib/httpx/connection.rb +118 -110
  12. data/lib/httpx/domain_name.rb +1 -1
  13. data/lib/httpx/extensions.rb +0 -14
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/ssl.rb +1 -1
  16. data/lib/httpx/loggable.rb +14 -2
  17. data/lib/httpx/options.rb +60 -17
  18. data/lib/httpx/plugins/auth/digest.rb +44 -4
  19. data/lib/httpx/plugins/auth.rb +87 -4
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  21. data/lib/httpx/plugins/callbacks.rb +15 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +162 -56
  30. data/lib/httpx/plugins/proxy/http.rb +37 -9
  31. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  32. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  33. data/lib/httpx/plugins/response_cache.rb +16 -9
  34. data/lib/httpx/plugins/retries.rb +55 -16
  35. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  36. data/lib/httpx/plugins/stream.rb +59 -8
  37. data/lib/httpx/plugins/stream_bidi.rb +87 -22
  38. data/lib/httpx/pool.rb +65 -21
  39. data/lib/httpx/request.rb +13 -14
  40. data/lib/httpx/resolver/https.rb +100 -34
  41. data/lib/httpx/resolver/multi.rb +12 -27
  42. data/lib/httpx/resolver/native.rb +68 -38
  43. data/lib/httpx/resolver/resolver.rb +46 -29
  44. data/lib/httpx/resolver/system.rb +63 -39
  45. data/lib/httpx/resolver.rb +97 -29
  46. data/lib/httpx/response/body.rb +2 -0
  47. data/lib/httpx/response.rb +22 -6
  48. data/lib/httpx/selector.rb +44 -20
  49. data/lib/httpx/session.rb +23 -33
  50. data/lib/httpx/transcoder/body.rb +1 -1
  51. data/lib/httpx/transcoder/deflate.rb +13 -8
  52. data/lib/httpx/transcoder/json.rb +1 -1
  53. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  54. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  55. data/lib/httpx/transcoder/multipart.rb +16 -8
  56. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  57. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  58. data/lib/httpx/transcoder.rb +4 -6
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/altsvc.rbs +3 -0
  61. data/sig/chainable.rbs +3 -3
  62. data/sig/connection.rbs +13 -6
  63. data/sig/loggable.rbs +5 -1
  64. data/sig/options.rbs +6 -2
  65. data/sig/plugins/auth/digest.rbs +6 -0
  66. data/sig/plugins/auth.rbs +28 -4
  67. data/sig/plugins/basic_auth.rbs +3 -3
  68. data/sig/plugins/callbacks.rbs +3 -0
  69. data/sig/plugins/digest_auth.rbs +2 -4
  70. data/sig/plugins/fiber_concurrency.rbs +6 -0
  71. data/sig/plugins/ntlm_auth.rbs +2 -2
  72. data/sig/plugins/oauth.rbs +46 -15
  73. data/sig/plugins/rate_limiter.rbs +1 -1
  74. data/sig/plugins/response_cache/file_store.rbs +2 -0
  75. data/sig/plugins/response_cache.rbs +4 -0
  76. data/sig/plugins/retries.rbs +8 -2
  77. data/sig/plugins/stream.rbs +13 -3
  78. data/sig/plugins/stream_bidi.rbs +5 -7
  79. data/sig/pool.rbs +1 -1
  80. data/sig/resolver/https.rbs +7 -0
  81. data/sig/resolver/multi.rbs +2 -9
  82. data/sig/resolver/native.rbs +1 -1
  83. data/sig/resolver/resolver.rbs +9 -8
  84. data/sig/resolver/system.rbs +4 -2
  85. data/sig/resolver.rbs +12 -3
  86. data/sig/response.rbs +3 -0
  87. data/sig/selector.rbs +2 -0
  88. data/sig/session.rbs +8 -8
  89. data/sig/transcoder/multipart.rbs +4 -2
  90. data/sig/transcoder.rbs +5 -1
  91. metadata +5 -1
@@ -9,6 +9,7 @@ module HTTPX
9
9
  #
10
10
  class Resolver::Native < Resolver::Resolver
11
11
  extend Forwardable
12
+
12
13
  using URIExtensions
13
14
 
14
15
  DEFAULTS = {
@@ -49,8 +50,19 @@ module HTTPX
49
50
  transition(:closed)
50
51
  end
51
52
 
53
+ def force_close(*)
54
+ @timer.cancel if @timer
55
+ @timer = @name = nil
56
+ @queries.clear
57
+ @timeouts.clear
58
+ close
59
+ super
60
+ ensure
61
+ terminate
62
+ end
63
+
52
64
  def terminate
53
- emit(:close, self)
65
+ disconnect
54
66
  end
55
67
 
56
68
  def closed?
@@ -84,7 +96,7 @@ module HTTPX
84
96
  if @nameserver.nil?
85
97
  ex = ResolveError.new("No available nameserver")
86
98
  ex.set_backtrace(caller)
87
- connection.force_reset
99
+ connection.force_close
88
100
  throw(:resolve_error, ex)
89
101
  else
90
102
  @connections << connection
@@ -93,15 +105,34 @@ module HTTPX
93
105
  end
94
106
 
95
107
  def timeout
96
- return if @connections.empty?
108
+ return unless @name
97
109
 
98
110
  @start_timeout = Utils.now
99
- hosts = @queries.keys
100
- @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
111
+
112
+ timeouts = @timeouts[@name]
113
+
114
+ return if timeouts.empty?
115
+
116
+ log(level: 2) { "resolver #{FAMILY_TYPES[@record_type]}: next timeout #{timeouts.first} secs... (#{timeouts.size - 1} left)" }
117
+
118
+ timeouts.first
101
119
  end
102
120
 
103
121
  def handle_socket_timeout(interval); end
104
122
 
123
+ def handle_error(error)
124
+ if error.respond_to?(:connection) &&
125
+ error.respond_to?(:host)
126
+ reset_hostname(error.host, connection: error.connection)
127
+ else
128
+ @queries.each do |host, connection|
129
+ reset_hostname(host, connection: connection)
130
+ end
131
+ end
132
+
133
+ super
134
+ end
135
+
105
136
  private
106
137
 
107
138
  def calculate_interests
@@ -118,7 +149,6 @@ module HTTPX
118
149
 
119
150
  break unless calculate_interests == :w
120
151
 
121
- # do_retry
122
152
  dwrite
123
153
 
124
154
  break unless calculate_interests == :r
@@ -133,7 +163,7 @@ module HTTPX
133
163
  retry
134
164
  else
135
165
  handle_error(e)
136
- emit(:close, self)
166
+ disconnect
137
167
  end
138
168
  rescue NativeResolveError => e
139
169
  handle_error(e)
@@ -154,7 +184,7 @@ module HTTPX
154
184
  @timer = @current_selector.after(timeout) do
155
185
  next unless @connections.include?(connection)
156
186
 
157
- @timer = nil
187
+ @timer = @name = nil
158
188
 
159
189
  do_retry(h, connection, timeout)
160
190
  end
@@ -178,8 +208,6 @@ module HTTPX
178
208
  @timeouts.clear
179
209
  resolve(connection, h)
180
210
  else
181
-
182
- @timeouts.delete(h)
183
211
  reset_hostname(h, reset_candidates: false)
184
212
 
185
213
  unless @queries.empty?
@@ -271,16 +299,14 @@ module HTTPX
271
299
  end
272
300
 
273
301
  def parse(buffer)
274
- @timer.cancel
275
-
276
- @timer = nil
277
-
278
302
  code, result = Resolver.decode_dns_answer(buffer)
279
303
 
280
304
  case code
281
305
  when :ok
306
+ reset_query
282
307
  parse_addresses(result)
283
308
  when :no_domain_found
309
+ reset_query
284
310
  # Indicates no such domain was found.
285
311
  hostname, connection = @queries.first
286
312
  reset_hostname(hostname, reset_candidates: false)
@@ -297,6 +323,7 @@ module HTTPX
297
323
  close_or_resolve
298
324
  end
299
325
  when :message_truncated
326
+ reset_query
300
327
  # TODO: what to do if it's already tcp??
301
328
  return if @socket_type == :tcp
302
329
 
@@ -305,13 +332,29 @@ module HTTPX
305
332
  hostname, _ = @queries.first
306
333
  reset_hostname(hostname)
307
334
  transition(:closed)
335
+ when :retriable_error
336
+ if @name && @timer
337
+ log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
338
+ return
339
+ end
340
+ # retry now!
341
+ # connection = @queries[@name].shift
342
+ # @timer.fire
343
+ reset_query
344
+ hostname, connection = @queries.first
345
+ reset_hostname(hostname)
346
+ @connections.delete(connection)
347
+ ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
348
+ raise ex
308
349
  when :dns_error
350
+ reset_query
309
351
  hostname, connection = @queries.first
310
352
  reset_hostname(hostname)
311
353
  @connections.delete(connection)
312
354
  ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
313
355
  raise ex
314
356
  when :decode_error
357
+ reset_query
315
358
  hostname, connection = @queries.first
316
359
  reset_hostname(hostname)
317
360
  @connections.delete(connection)
@@ -431,13 +474,14 @@ module HTTPX
431
474
  def generate_candidates(name)
432
475
  return [name] if name.end_with?(".")
433
476
 
434
- candidates = []
435
477
  name_parts = name.scan(/[^.]+/)
436
- candidates = [name] if @ndots <= name_parts.size - 1
437
- candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") })
478
+ candidates = @search.map { |domain| [*name_parts, *domain].join(".") }
438
479
  fname = "#{name}."
439
- candidates << fname unless candidates.include?(fname)
440
-
480
+ if @ndots <= name_parts.size - 1
481
+ candidates.unshift(fname)
482
+ else
483
+ candidates << fname
484
+ end
441
485
  candidates
442
486
  end
443
487
 
@@ -498,27 +542,13 @@ module HTTPX
498
542
  ConnectTimeoutError => e
499
543
  # these errors may happen during TCP handshake
500
544
  # treat them as resolve errors.
501
- handle_error(e)
502
- emit(:close, self)
545
+ on_error(e)
503
546
  end
504
547
 
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
548
+ def reset_query
549
+ @timer.cancel
517
550
 
518
- while (connection = @connections.shift)
519
- emit_resolve_error(connection, connection.peer.host, error)
520
- end
521
- end
551
+ @timer = @name = nil
522
552
  end
523
553
 
524
554
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -538,7 +568,7 @@ module HTTPX
538
568
  @connections.shift until @connections.empty? || @connections.first.state != :closed
539
569
 
540
570
  if (@connections - @queries.values).empty?
541
- emit(:close, self)
571
+ disconnect
542
572
  else
543
573
  resolve
544
574
  end
@@ -7,7 +7,6 @@ module HTTPX
7
7
  # from the Selectable API.
8
8
  #
9
9
  class Resolver::Resolver
10
- include Callbacks
11
10
  include Loggable
12
11
 
13
12
  using ArrayExtensions::Intersect
@@ -39,8 +38,6 @@ module HTTPX
39
38
  @record_type = RECORD_TYPES[family]
40
39
  @options = options
41
40
  @connections = []
42
-
43
- set_resolver_callbacks
44
41
  end
45
42
 
46
43
  def each_connection(&block)
@@ -55,6 +52,12 @@ module HTTPX
55
52
 
56
53
  alias_method :terminate, :close
57
54
 
55
+ def force_close(*args)
56
+ while (connection = @connections.shift)
57
+ connection.force_close(*args)
58
+ end
59
+ end
60
+
58
61
  def closed?
59
62
  true
60
63
  end
@@ -72,7 +75,7 @@ module HTTPX
72
75
 
73
76
  # double emission check, but allow early resolution to work
74
77
  conn_addrs = connection.addresses
75
- return if !early_resolve && conn_addrs && (!conn_addrs.empty? && !addresses.intersect?(!conn_addrs))
78
+ return if !early_resolve && conn_addrs && !conn_addrs.empty? && !addresses.intersect?(conn_addrs)
76
79
 
77
80
  log do
78
81
  "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
@@ -104,26 +107,24 @@ module HTTPX
104
107
  end
105
108
  end
106
109
 
107
- private
108
-
109
- def emit_resolved_connection(connection, addresses, early_resolve)
110
- begin
111
- connection.addresses = addresses
112
-
113
- return if connection.state == :closed
114
-
115
- emit(:resolve, connection)
116
- rescue StandardError => e
117
- if early_resolve
118
- connection.force_reset
119
- throw(:resolve_error, e)
120
- else
121
- emit(:error, connection, e)
110
+ def handle_error(error)
111
+ if error.respond_to?(:connection) &&
112
+ error.respond_to?(:host)
113
+ @connections.delete(error.connection)
114
+ emit_resolve_error(error.connection, error.host, error)
115
+ else
116
+ while (connection = @connections.shift)
117
+ emit_resolve_error(connection, connection.peer.host, error)
122
118
  end
123
119
  end
124
120
  end
125
121
 
126
- def early_resolve(connection, hostname: connection.peer.host)
122
+ def on_error(error)
123
+ handle_error(error)
124
+ disconnect
125
+ end
126
+
127
+ def early_resolve(connection, hostname: connection.peer.host) # rubocop:disable Naming/PredicateMethod
127
128
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
128
129
 
129
130
  return false unless addresses
@@ -137,6 +138,25 @@ module HTTPX
137
138
  true
138
139
  end
139
140
 
141
+ private
142
+
143
+ def emit_resolved_connection(connection, addresses, early_resolve)
144
+ begin
145
+ connection.addresses = addresses
146
+
147
+ return if connection.state == :closed
148
+
149
+ resolve_connection(connection)
150
+ rescue StandardError => e
151
+ if early_resolve
152
+ connection.force_close
153
+ throw(:resolve_error, e)
154
+ else
155
+ emit_connection_error(connection, e)
156
+ end
157
+ end
158
+ end
159
+
140
160
  def emit_resolve_error(connection, hostname = connection.peer.host, ex = nil)
141
161
  emit_connection_error(connection, resolve_error(hostname, ex))
142
162
  end
@@ -150,12 +170,6 @@ module HTTPX
150
170
  error
151
171
  end
152
172
 
153
- def set_resolver_callbacks
154
- on(:resolve, &method(:resolve_connection))
155
- on(:error, &method(:emit_connection_error))
156
- on(:close, &method(:close_resolver))
157
- end
158
-
159
173
  def resolve_connection(connection)
160
174
  @current_session.__send__(:on_resolver_connection, connection, @current_selector)
161
175
  end
@@ -163,11 +177,14 @@ module HTTPX
163
177
  def emit_connection_error(connection, error)
164
178
  return connection.handle_connect_error(error) if connection.connecting?
165
179
 
166
- connection.emit(:error, error)
180
+ connection.on_error(error)
167
181
  end
168
182
 
169
- def close_resolver(resolver)
170
- @current_session.__send__(:on_resolver_close, resolver, @current_selector)
183
+ def disconnect
184
+ return if closed?
185
+
186
+ close
187
+ @current_session.deselect_resolver(self, @current_selector)
171
188
  end
172
189
  end
173
190
  end
@@ -56,13 +56,21 @@ module HTTPX
56
56
  end
57
57
 
58
58
  def empty?
59
- true
59
+ @connections.empty?
60
60
  end
61
61
 
62
62
  def close
63
63
  transition(:closed)
64
64
  end
65
65
 
66
+ def force_close(*)
67
+ close
68
+ @queries.clear
69
+ @timeouts.clear
70
+ @ips.clear
71
+ super
72
+ end
73
+
66
74
  def closed?
67
75
  @state == :closed
68
76
  end
@@ -86,36 +94,42 @@ module HTTPX
86
94
  end
87
95
 
88
96
  def timeout
89
- return unless @queries.empty?
90
-
91
97
  _, connection = @queries.first
92
98
 
93
99
  return unless connection
94
100
 
95
- @timeouts[connection.peer.host].first
101
+ timeouts = @timeouts[connection.peer.host]
102
+
103
+ return if timeouts.empty?
104
+
105
+ log(level: 2) { "resolver #{FAMILY_TYPES[@record_type]}: next timeout #{timeouts.first} secs... (#{timeouts.size - 1} left)" }
106
+
107
+ timeouts.first
96
108
  end
97
109
 
98
- def <<(connection)
110
+ def lazy_resolve(connection)
99
111
  @connections << connection
100
112
  resolve
101
- end
102
113
 
103
- def early_resolve(connection, **)
104
- self << connection
105
- true
114
+ return if empty?
115
+
116
+ @current_session.select_resolver(self, @current_selector)
106
117
  end
107
118
 
119
+ def early_resolve(connection, **); end
120
+
108
121
  def handle_socket_timeout(interval)
109
122
  error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
110
123
  error.set_backtrace(caller)
111
- @queries.each do |host, connection|
112
- @connections.delete(connection)
113
- emit_resolve_error(connection, host, error)
124
+ @queries.each do |_, connection| # rubocop:disable Style/HashEachMethods
125
+ emit_resolve_error(connection, connection.peer.host, error) if @connections.delete(connection)
114
126
  end
115
127
 
116
128
  while (connection = @connections.shift)
117
129
  emit_resolve_error(connection, connection.peer.host, error)
118
130
  end
131
+
132
+ close_or_resolve
119
133
  end
120
134
 
121
135
  private
@@ -140,34 +154,38 @@ module HTTPX
140
154
  def consume
141
155
  return if @connections.empty?
142
156
 
143
- if @pipe_read.wait_readable
144
- event = @pipe_read.getbyte
157
+ event = @pipe_read.read_nonblock(1, exception: false)
145
158
 
146
- case event
147
- when DONE
148
- *pair, addrs = @pipe_mutex.synchronize { @ips.pop }
149
- if pair
150
- @queries.delete(pair)
151
- family, connection = pair
152
- @connections.delete(connection)
159
+ return if event == :wait_readable
153
160
 
154
- catch(:coalesced) { emit_addresses(connection, family, addrs) }
155
- end
156
- when ERROR
157
- *pair, error = @pipe_mutex.synchronize { @ips.pop }
158
- if pair && error
159
- @queries.delete(pair)
160
- @connections.delete(connection)
161
-
162
- _, connection = pair
163
- emit_resolve_error(connection, connection.peer.host, error)
164
- end
161
+ raise ResolveError, "socket pipe closed unexpectedly" if event.nil?
162
+
163
+ case event.unpack1("C")
164
+ when DONE
165
+ *pair, addrs = @pipe_mutex.synchronize { @ips.pop }
166
+ if pair
167
+ @queries.delete(pair)
168
+ family, connection = pair
169
+ @connections.delete(connection)
170
+
171
+ catch(:coalesced) { emit_addresses(connection, family, addrs) }
172
+ end
173
+ when ERROR
174
+ *pair, error = @pipe_mutex.synchronize { @ips.pop }
175
+ if pair && error
176
+ @queries.delete(pair)
177
+ _, connection = pair
178
+ @connections.delete(connection)
179
+
180
+ emit_resolve_error(connection, connection.peer.host, error)
165
181
  end
166
182
  end
167
183
 
168
- return emit(:close, self) if @connections.empty?
184
+ return disconnect if @connections.empty?
169
185
 
170
186
  resolve
187
+ rescue StandardError => e
188
+ on_error(e)
171
189
  end
172
190
 
173
191
  def resolve(connection = nil, hostname = nil)
@@ -240,16 +258,22 @@ module HTTPX
240
258
  end
241
259
  end
242
260
  end
261
+ Thread.pass
243
262
  end
244
263
 
245
- def __addrinfo_resolve(host, scheme)
246
- Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
247
- end
264
+ def close_or_resolve
265
+ # drop already closed connections
266
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
248
267
 
249
- def emit_connection_error(_, error)
250
- throw(:resolve_error, error)
268
+ if (@connections - @queries.map(&:last)).empty?
269
+ disconnect
270
+ else
271
+ resolve
272
+ end
251
273
  end
252
274
 
253
- def close_resolver(resolver); end
275
+ def __addrinfo_resolve(host, scheme)
276
+ Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
277
+ end
254
278
  end
255
279
  end