httpx 1.9.0 → 1.9.1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_1.md +1 -1
  3. data/doc/release_notes/1_8_2.md +4 -4
  4. data/doc/release_notes/1_8_3.md +3 -5
  5. data/doc/release_notes/1_8_4.md +1 -1
  6. data/doc/release_notes/1_9_0.md +7 -7
  7. data/doc/release_notes/1_9_1.md +32 -0
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +14 -1
  10. data/lib/httpx/connection/http1.rb +24 -17
  11. data/lib/httpx/connection/http2.rb +108 -29
  12. data/lib/httpx/connection.rb +158 -162
  13. data/lib/httpx/io/tcp.rb +24 -7
  14. data/lib/httpx/plugins/aws_sigv4.rb +0 -1
  15. data/lib/httpx/plugins/cache.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/content_digest.rb +3 -5
  18. data/lib/httpx/plugins/expect.rb +8 -1
  19. data/lib/httpx/plugins/fiber_concurrency.rb +56 -19
  20. data/lib/httpx/plugins/h2c.rb +12 -11
  21. data/lib/httpx/plugins/hedging.rb +2 -2
  22. data/lib/httpx/plugins/ntlm_auth.rb +4 -3
  23. data/lib/httpx/plugins/ntlm_v2_auth.rb +4 -3
  24. data/lib/httpx/plugins/oauth.rb +1 -1
  25. data/lib/httpx/plugins/proxy/http.rb +23 -16
  26. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  27. data/lib/httpx/plugins/proxy.rb +8 -15
  28. data/lib/httpx/plugins/query.rb +1 -0
  29. data/lib/httpx/plugins/stream.rb +12 -10
  30. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  31. data/lib/httpx/plugins/tracing.rb +62 -21
  32. data/lib/httpx/plugins/upgrade/h2.rb +2 -2
  33. data/lib/httpx/request.rb +1 -1
  34. data/lib/httpx/resolver/https.rb +3 -2
  35. data/lib/httpx/resolver/native.rb +19 -9
  36. data/lib/httpx/resolver/resolver.rb +2 -2
  37. data/lib/httpx/resolver/system.rb +4 -0
  38. data/lib/httpx/resolver.rb +5 -5
  39. data/lib/httpx/response.rb +5 -1
  40. data/lib/httpx/selector.rb +108 -7
  41. data/lib/httpx/transcoder/json.rb +1 -1
  42. data/lib/httpx/version.rb +1 -1
  43. data/lib/httpx.rb +1 -0
  44. data/sig/chainable.rbs +2 -1
  45. data/sig/connection/http1.rbs +4 -2
  46. data/sig/connection/http2.rbs +8 -2
  47. data/sig/connection.rbs +11 -6
  48. data/sig/io/tcp.rbs +5 -0
  49. data/sig/plugins/expect.rbs +6 -0
  50. data/sig/plugins/fiber_concurrency.rbs +10 -1
  51. data/sig/plugins/proxy/http.rbs +3 -1
  52. data/sig/plugins/stream_bidi.rbs +0 -2
  53. data/sig/plugins/tracing.rbs +10 -2
  54. data/sig/resolver/https.rbs +2 -0
  55. data/sig/resolver/native.rbs +3 -0
  56. data/sig/resolver/resolver.rbs +1 -1
  57. data/sig/resolver/system.rbs +2 -0
  58. data/sig/resolver.rbs +1 -1
  59. data/sig/selector.rbs +15 -1
  60. data/sig/session.rbs +1 -1
  61. metadata +3 -1
@@ -70,11 +70,14 @@ module HTTPX::Plugins
70
70
  # the connection to connect.
71
71
  attr_accessor :handshake_time
72
72
 
73
+ # time at which the request was first handed to a connection, or last moved back to waiting for one.
74
+ attr_accessor :enqueue_time
75
+
73
76
  # intercepts request initialization to inject the tracing logic.
74
77
  def initialize(*)
75
78
  super
76
79
 
77
- @init_time = @handshake_time = nil
80
+ @init_time = @handshake_time = @enqueue_time = nil
78
81
 
79
82
  tracer = @options.tracer
80
83
 
@@ -85,12 +88,14 @@ module HTTPX::Plugins
85
88
 
86
89
  # request is reset when it's retried.
87
90
  @init_time = @handshake_time = nil
91
+
92
+ # going back to idle means waiting to be sent again.
93
+ @enqueue_time = ::Time.now.utc
88
94
  end
89
95
  on(:headers) do
90
96
  # the usual request init time (when not including the connection handshake)
91
97
  # should be the time the request is buffered the first time.
92
- @init_time = ::Time.now.utc
93
- @init_time -= @handshake_time if @handshake_time
98
+ @init_time = calculate_init_time
94
99
 
95
100
  tracer.start(self)
96
101
  end
@@ -102,14 +107,27 @@ module HTTPX::Plugins
102
107
  # Example is the :ssrf_filter plugin, which raises an error on
103
108
  # initialize if the host is an IP which matches against the known set.
104
109
  # in such cases, we'll just set here right here.
105
- unless @init_time
106
- @init_time = ::Time.now.utc
107
-
108
- @init_time -= @handshake_time if @handshake_time
109
- end
110
+ @init_time ||= calculate_init_time
110
111
 
111
112
  super
112
113
  end
114
+
115
+ private
116
+
117
+ def calculate_init_time
118
+ init_time = ::Time.now.utc
119
+
120
+ return init_time unless (handshake_time = @handshake_time)
121
+
122
+ init_time -= handshake_time
123
+
124
+ enqueue_time = @enqueue_time
125
+
126
+ # handshake time is measured for the connection, and may happen before the request was sent to it.
127
+ return init_time unless enqueue_time && init_time < enqueue_time
128
+
129
+ enqueue_time
130
+ end
113
131
  end
114
132
 
115
133
  # Connection mixin
@@ -120,7 +138,15 @@ module HTTPX::Plugins
120
138
  @init_time = nil
121
139
  end
122
140
 
123
- def send_request_to_parser(request)
141
+ def send(request)
142
+ # requests may be rerouted to other connections (i.e. when coalescing), so only the first
143
+ # connection the request is handed to marks it.
144
+ request.enqueue_time ||= ::Time.now.utc
145
+
146
+ super
147
+ end
148
+
149
+ def send_request_to_parser(request, *)
124
150
  if connecting? && @init_time
125
151
  # at this point, the connection has connected, but hasn't yet transitioned to :open,
126
152
  # so it's the time the requests which caused this connection to connect, are finally being
@@ -144,8 +170,26 @@ module HTTPX::Plugins
144
170
  @init_time = nil
145
171
  end
146
172
 
173
+ def disconnect
174
+ return if @exhausted
175
+
176
+ return unless @current_session && @current_selector
177
+
178
+ # reset init time of connections abandoned while connecting.
179
+ @init_time = nil if connecting?
180
+
181
+ super
182
+ end
183
+
147
184
  private
148
185
 
186
+ def handle_transition(*)
187
+ super
188
+
189
+ # no longer relevant after connection established.
190
+ @init_time = nil unless connecting?
191
+ end
192
+
149
193
  def handle_error(er, request = nil)
150
194
  if connecting? && @init_time
151
195
  # bookkeep handshake time back into pending requests, which were never initialized with it.
@@ -176,24 +220,21 @@ module HTTPX::Plugins
176
220
  def initialize(*)
177
221
  super
178
222
 
179
- @handshake_time = nil
180
223
  @handshake_init_time = ::Time.now.utc
181
224
  end
182
225
 
183
- def send(request, head = false)
184
- if head
185
- # only true for pending requests waiting on the handshake, so handshake time accrues.
186
- request.handshake_time ||= 0.0
187
- request.handshake_time += @handshake_time
188
- end
189
-
190
- super
191
- end
192
-
193
226
  private
194
227
 
195
228
  def on_settings(*)
196
- @handshake_time = ::Time.now.utc - @handshake_init_time unless @handshake_completed
229
+ unless @handshake_completed
230
+ # pending requests must wait for the handshake completion
231
+ handshake_time = ::Time.now.utc - @handshake_init_time
232
+
233
+ @pending.each do |request|
234
+ request.handshake_time ||= 0.0
235
+ request.handshake_time += handshake_time
236
+ end
237
+ end
197
238
 
198
239
  super
199
240
  end
@@ -33,9 +33,9 @@ module HTTPX
33
33
  end
34
34
 
35
35
  def upgrade_to_h2
36
- enqueue_pending_requests_from_parser(@parser)
36
+ @parser.reset_requests
37
37
 
38
- @parser = @options.http2_class.new(@write_buffer, @options)
38
+ @parser = @options.http2_class.new(@write_buffer, @pending, @options)
39
39
  set_parser_callbacks(@parser)
40
40
  @upgrade_protocol = "h2"
41
41
 
data/lib/httpx/request.rb CHANGED
@@ -180,7 +180,7 @@ module HTTPX
180
180
  end
181
181
 
182
182
  def can_buffer?
183
- @state != :done
183
+ @state != :done && @state != :expect
184
184
  end
185
185
 
186
186
  def started?
@@ -32,7 +32,8 @@ module HTTPX
32
32
  }.freeze
33
33
 
34
34
  def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
35
- :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
35
+ :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout,
36
+ :closed_for_select?
36
37
 
37
38
  def initialize(_, options)
38
39
  super
@@ -49,7 +50,7 @@ module HTTPX
49
50
  end
50
51
 
51
52
  def state
52
- @resolver_connection ? @resolver_connection.state : :idle
53
+ @resolver_connection&.state || :idle
53
54
  end
54
55
 
55
56
  def <<(connection)
@@ -38,12 +38,11 @@ module HTTPX
38
38
  @search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
39
39
  @_timeouts = Array(@resolver_options[:timeouts])
40
40
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
41
- @name = nil
41
+ @name = @message_id = @timer = nil
42
42
  @queries = {}
43
43
  @read_buffer = "".b
44
44
  @write_buffer = Buffer.new(@resolver_options[:packet_size])
45
45
  @state = :idle
46
- @timer = nil
47
46
  end
48
47
 
49
48
  def close
@@ -52,7 +51,7 @@ module HTTPX
52
51
 
53
52
  def force_close(*)
54
53
  @timer.cancel if @timer
55
- @timer = @name = nil
54
+ @timer = @name = @message_id = nil
56
55
  @queries.clear
57
56
  @timeouts.clear
58
57
  close
@@ -87,6 +86,10 @@ module HTTPX
87
86
  end
88
87
  end
89
88
 
89
+ def closed_for_select?
90
+ @state == :closed
91
+ end
92
+
90
93
  def interests
91
94
  case @state
92
95
  when :idle
@@ -170,6 +173,7 @@ module HTTPX
170
173
  if nameserver && @ns_index < nameserver.size
171
174
  log { "resolver #{FAMILY_TYPES[@record_type]}: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
172
175
  transition(:idle)
176
+ transition(:open)
173
177
  @timeouts.clear
174
178
  retry
175
179
  else
@@ -195,7 +199,7 @@ module HTTPX
195
199
  @timer = @current_selector.after(timeout) do
196
200
  next unless @connections.include?(connection)
197
201
 
198
- @timer = @name = nil
202
+ @timer = @name = @message_id = nil
199
203
 
200
204
  do_retry(h, connection, timeout)
201
205
  end
@@ -310,7 +314,13 @@ module HTTPX
310
314
  end
311
315
 
312
316
  def parse(buffer)
313
- code, result = Resolver.decode_dns_answer(buffer)
317
+ code, result, message_id = Resolver.decode_dns_answer(buffer)
318
+
319
+ # discard late message answers for no longer inflight queries.
320
+ if message_id && message_id != @message_id
321
+ log { "resolver #{FAMILY_TYPES[@record_type]}: discarding answer for a query no longer in flight" }
322
+ return
323
+ end
314
324
 
315
325
  case code
316
326
  when :ok
@@ -392,7 +402,7 @@ module HTTPX
392
402
  orig_name = name
393
403
  # absolute name
394
404
  name_labels = Resolv::DNS::Name.create(name).to_a
395
- name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
405
+ name = @queries.each_key.find { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
396
406
 
397
407
  # probably a retried query for which there's an answer
398
408
  unless name
@@ -462,11 +472,10 @@ module HTTPX
462
472
  @queries[hostname] = connection
463
473
  end
464
474
 
465
- @name = hostname
466
-
467
475
  log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
468
476
  begin
469
477
  @write_buffer << encode_dns_query(hostname)
478
+ @name = hostname
470
479
  rescue Resolv::DNS::EncodeError => e
471
480
  reset_hostname(hostname, connection: connection)
472
481
  @connections.delete(connection)
@@ -479,6 +488,7 @@ module HTTPX
479
488
  message_id = Resolver.generate_id
480
489
  msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
481
490
  msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
491
+ @message_id = message_id
482
492
  msg
483
493
  end
484
494
 
@@ -574,7 +584,7 @@ module HTTPX
574
584
  def reset_query
575
585
  @timer.cancel
576
586
 
577
- @timer = @name = nil
587
+ @timer = @name = @message_id = nil
578
588
  end
579
589
 
580
590
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -29,9 +29,9 @@ module HTTPX
29
29
 
30
30
  attr_reader :family, :options
31
31
 
32
- attr_writer :current_selector, :current_session
32
+ attr_writer :current_session
33
33
 
34
- attr_accessor :multi
34
+ attr_accessor :current_selector, :multi
35
35
 
36
36
  def initialize(family, options)
37
37
  @family = family
@@ -90,6 +90,10 @@ module HTTPX
90
90
  nil
91
91
  end
92
92
 
93
+ def closed_for_select?
94
+ @state == :closed
95
+ end
96
+
93
97
  def interests
94
98
  return if @queries.empty?
95
99
 
@@ -51,9 +51,9 @@ module HTTPX
51
51
  end
52
52
 
53
53
  # no domain was found
54
- return :no_domain_found if message.rcode == Resolv::DNS::RCode::NXDomain
54
+ return :no_domain_found, nil, message.id if message.rcode == Resolv::DNS::RCode::NXDomain
55
55
 
56
- return :message_truncated if message.tc == 1
56
+ return :message_truncated, nil, message.id if message.tc == 1
57
57
 
58
58
  if message.rcode != Resolv::DNS::RCode::NoError
59
59
  case message.rcode
@@ -62,9 +62,9 @@ module HTTPX
62
62
  # but the DNS resolver state.
63
63
  Resolv::DNS::RCode::NotImp,
64
64
  Resolv::DNS::RCode::Refused
65
- return :retriable_error, message.rcode
65
+ return :retriable_error, message.rcode, message.id
66
66
  else
67
- return :dns_error, message.rcode
67
+ return :dns_error, message.rcode, message.id
68
68
  end
69
69
  end
70
70
 
@@ -89,7 +89,7 @@ module HTTPX
89
89
  end
90
90
  end
91
91
 
92
- [:ok, addresses]
92
+ [:ok, addresses, message.id]
93
93
  end
94
94
 
95
95
  private
@@ -259,7 +259,11 @@ module HTTPX
259
259
  if c
260
260
  c.strip!
261
261
  c.delete_prefix!("charset=")
262
- c = c[1..-2] if c.start_with?('"') && c.end_with?('"')
262
+ c.gsub!(/\A"(.*?)(?<!\\)"\z/) do
263
+ c2 = ::Regexp.last_match(1)
264
+ c2.gsub!(/\\(.)/, '\1')
265
+ c2
266
+ end
263
267
  @charset = c
264
268
  end
265
269
  end
@@ -21,11 +21,17 @@ module HTTPX
21
21
  class Selector
22
22
  extend Forwardable
23
23
 
24
+ HAS_FIBER_SCHEDULER = Fiber.respond_to?(:scheduler)
25
+
24
26
  READABLE = %i[rw r].freeze
25
27
  WRITABLE = %i[rw w].freeze
26
28
 
29
+ # max number of consecutive ticks allowed to neither wait on IO nor call a ready selectable.
30
+ MAX_BUSY_ITERATIONS = 1000
31
+
27
32
  private_constant :READABLE
28
33
  private_constant :WRITABLE
34
+ private_constant :MAX_BUSY_ITERATIONS
29
35
 
30
36
  def_delegator :@timers, :after
31
37
 
@@ -35,6 +41,8 @@ module HTTPX
35
41
  @timers = Timers.new
36
42
  @selectables = []
37
43
  @is_timer_interval = false
44
+ @busy_iterations = 0
45
+ @last_select_registered = nil
38
46
  end
39
47
 
40
48
  def empty?
@@ -51,11 +59,16 @@ module HTTPX
51
59
  timeout = next_timeout
52
60
  if timeout && timeout.negative?
53
61
  @timers.fire
62
+ busy_loop_check(timeout)
54
63
  throw(:jump_tick)
55
64
  end
56
65
 
57
66
  begin
67
+ selected = false
68
+
58
69
  select(timeout) do |c|
70
+ selected = true
71
+
59
72
  c.log(level: 2) { "[#{c.state}] selected from selector##{object_id}#{" after #{timeout} secs" unless timeout.nil?}..." }
60
73
 
61
74
  c.call
@@ -64,6 +77,19 @@ module HTTPX
64
77
  @timers.fire
65
78
  rescue TimeoutError => e
66
79
  @timers.fire(e)
80
+
81
+ @busy_iterations = 0
82
+ else
83
+ # a tick is healthy if it either called a ready selectable, or actually blocked waiting for one.
84
+ # without a timeout, #select only blocks when something registered interests; with one,
85
+ # it always sleeps or selects, but a zero timeout just polls.
86
+ waited = timeout.nil? ? @last_select_registered.to_i.positive? : timeout.positive?
87
+
88
+ if selected || waited
89
+ @busy_iterations = 0
90
+ else
91
+ busy_loop_check(timeout)
92
+ end
67
93
  end
68
94
  end
69
95
  end
@@ -74,7 +100,9 @@ module HTTPX
74
100
 
75
101
  selectables.delete_if do |sel|
76
102
  sel.terminate
77
- sel.state == :closed
103
+ # TODO: this check is racy: connections check in to the pool on termination, and may check out and
104
+ # change state immediately after. find a more idempotent way of doing this.
105
+ sel.current_selector != self
78
106
  end
79
107
 
80
108
  until selectables.empty?
@@ -133,6 +161,8 @@ module HTTPX
133
161
  private
134
162
 
135
163
  def select(interval, &block)
164
+ @last_select_registered = nil
165
+
136
166
  # do not cause an infinite loop here.
137
167
  #
138
168
  # this may happen if timeout calculation actually triggered an error which causes
@@ -153,10 +183,12 @@ module HTTPX
153
183
  # @type var w: (selectable | Array[selectable])?
154
184
  r, w = nil
155
185
 
186
+ registered = 0
187
+
156
188
  @selectables.delete_if do |io|
157
189
  interests = io.interests
158
190
 
159
- is_closed = io.state == :closed
191
+ is_closed = io.closed_for_select?
160
192
 
161
193
  if is_closed
162
194
  # the process by which io was closed may have already triggered the on_close callback,
@@ -166,6 +198,8 @@ module HTTPX
166
198
  end
167
199
 
168
200
  if interests
201
+ registered += 1
202
+
169
203
  io.log(level: 2) do
170
204
  "[#{io.state}] registering in selector##{object_id} for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}"
171
205
  end
@@ -182,6 +216,8 @@ module HTTPX
182
216
  is_closed
183
217
  end
184
218
 
219
+ @last_select_registered = registered
220
+
185
221
  case r
186
222
  when Array
187
223
  w = Array(w) unless w.nil?
@@ -192,7 +228,14 @@ module HTTPX
192
228
  when Array
193
229
  select_many(r, w, interval, &block)
194
230
  when nil
195
- return unless interval && @selectables.any?
231
+ unless interval && @selectables.any?
232
+ # @fiber-switch-guard
233
+ # may mean that another fiber is moving a selectable forward (i.e. closing its
234
+ # socket), so let the scheduler resume it.
235
+ sleep(0) if HAS_FIBER_SCHEDULER && Fiber.scheduler
236
+
237
+ return
238
+ end
196
239
 
197
240
  # no selectables
198
241
  # TODO: replace with sleep?
@@ -221,18 +264,19 @@ module HTTPX
221
264
  begin
222
265
  readers, writers = ::IO.select(r, w, nil, interval)
223
266
  rescue IOError => e
224
- (Array(r) + Array(w)).each do |sel|
225
- # TODO: is there a way to cheaply find the IO associated with the error?
267
+ selectables_with_closed_ios(r, w).each do |sel|
226
268
  sel.on_io_error(e)
227
269
  end
270
+
271
+ return
228
272
  rescue StandardError => e
229
- (Array(r) + Array(w)).each do |sel|
273
+ selectables_with_closed_ios(r, w).each do |sel|
230
274
  sel.on_error(e)
231
275
  end
232
276
 
233
277
  return
234
278
  rescue Exception => e # rubocop:disable Lint/RescueException
235
- (Array(r) + Array(w)).each do |sel|
279
+ Set[*Array(r), *Array(w)].each do |sel|
236
280
  sel.force_close(true)
237
281
  end
238
282
 
@@ -258,6 +302,16 @@ module HTTPX
258
302
  end
259
303
  end
260
304
 
305
+ # returns the selectables whose IOs were closed.
306
+ def selectables_with_closed_ios(r, w)
307
+ selectables = Set[*Array(r), *Array(w)]
308
+
309
+ closed = selectables.select { |sel| sel.to_io.closed? }
310
+
311
+ # When none of them was closed, the error can't be attributed to any of them, so all are returned.
312
+ closed.empty? ? selectables : closed
313
+ end
314
+
261
315
  def select_one(io, interests, interval)
262
316
  begin
263
317
  result =
@@ -306,6 +360,53 @@ module HTTPX
306
360
  connection_interval
307
361
  end
308
362
 
363
+ def busy_loop_check(timeout)
364
+ @busy_iterations += 1
365
+
366
+ return if @busy_iterations < MAX_BUSY_ITERATIONS
367
+
368
+ # this selector is reused by subsequent requests in the same thread/fiber.
369
+ @busy_iterations = 0
370
+
371
+ raise Error, busy_loop_error_message(timeout)
372
+ end
373
+
374
+ def busy_loop_error_message(timeout)
375
+ selectables_state = @selectables.map do |sel|
376
+ state = +"#{sel.class}:#{sel.object_id}[state:#{sel.state},inflight?:#{sel.inflight?}"
377
+
378
+ case sel
379
+ when Connection
380
+ state << ",origin:#{sel.origin},pending:#{sel.pending.size}," \
381
+ "io-state:#{sel.io&.state.inspect},io-interests:#{sel.io&.interests.inspect}," \
382
+ "in-session:#{!sel.current_session.nil?}"
383
+ when Resolver::Resolver
384
+ num_connections = 0
385
+ sel.each_connection { num_connections += 1 }
386
+ state << ",connections:#{num_connections}"
387
+ end
388
+
389
+ state << "]"
390
+ end.join(",")
391
+
392
+ timers_state = @timers.instance_variable_get(:@intervals).map do |interval|
393
+ "#{interval.interval}s(callbacks:#{interval.instance_variable_get(:@callbacks).size})"
394
+ end.join(",")
395
+
396
+ "selector corrupted, aborted after looping #{MAX_BUSY_ITERATIONS} times " \
397
+ "without waiting on IO nor calling a ready selectable, " \
398
+ "please report this https://gitlab.com/os85/httpx/-/work_items " \
399
+ "along with debug logs\n(" \
400
+ "next-timeout:#{timeout.inspect}," \
401
+ "is-timer-interval:#{@is_timer_interval}," \
402
+ "timers:[#{timers_state}]," \
403
+ "next-interval-at:#{@timers.instance_variable_get(:@next_interval_at).inspect}," \
404
+ "last-select-registered:#{@last_select_registered.inspect}," \
405
+ "num-selectables:#{@selectables.size}," \
406
+ "selectables:[#{selectables_state}]" \
407
+ ")"
408
+ end
409
+
309
410
  if RUBY_ENGINE == "jruby"
310
411
  def rw_wait(io, interval)
311
412
  io.to_io.wait(interval, :read_write)
@@ -56,7 +56,7 @@ module HTTPX::Transcoder
56
56
  def json_load(...); MultiJson.load(...); end
57
57
  def json_dump(...); MultiJson.dump(...); end
58
58
  elsif defined?(Oj)
59
- def json_load(response, ...); Oj.load(response.to_s, ...); end
59
+ def json_load(response, options = {}); Oj.load(response.to_s, { mode: :strict }.merge(options)); end
60
60
  def json_dump(obj, options = {}); Oj.dump(obj, { mode: :compat }.merge(options)); end
61
61
  elsif defined?(Yajl)
62
62
  def json_load(response, ...); Yajl::Parser.new(...).parse(response.to_s); end
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.9.0"
4
+ VERSION = "1.9.1"
5
5
  end
data/lib/httpx.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require "httpx/version"
4
5
 
5
6
  # Top-Level Namespace
data/sig/chainable.rbs CHANGED
@@ -2,7 +2,8 @@ module HTTPX
2
2
  module Chainable
3
3
  def request: (Request, **untyped) -> response
4
4
  | (*Request, **untyped) -> Array[response]
5
- | (verb, uri | [uri], **untyped) -> response
5
+ | (verb, uri, **untyped) -> response
6
+ | (verb, [uri], **untyped) -> response
6
7
  | (Array[[verb, uri] | [verb, uri, request_params]], **untyped) -> Array[response]
7
8
  | (verb, _Each[uri | [uri, request_params]], **untyped) -> Array[response]
8
9
 
@@ -36,7 +36,7 @@ module HTTPX
36
36
 
37
37
  def <<: (string) -> void
38
38
 
39
- def send: (Request request, ?bool head) -> void
39
+ def send: (Request request, ?bool head) -> bool
40
40
 
41
41
  def consume: () -> void
42
42
 
@@ -60,9 +60,11 @@ module HTTPX
60
60
 
61
61
  def timeout: () -> interval?
62
62
 
63
+ def no_more_requests?: () -> bool
64
+
63
65
  private
64
66
 
65
- def initialize: (Buffer buffer, Options options) -> untyped
67
+ def initialize: (Buffer buffer, Array[Request] pending, Options options) -> untyped
66
68
 
67
69
  def manage_connection: (Request request, Response response) -> void
68
70
 
@@ -20,6 +20,7 @@ module HTTPX
20
20
  @buffer: Buffer
21
21
  @handshake_completed: bool
22
22
  @wait_for_handshake: bool
23
+ @last_error: GoawayError?
23
24
 
24
25
  def interests: () -> io_interests?
25
26
 
@@ -47,11 +48,15 @@ module HTTPX
47
48
 
48
49
  def reset_requests: () -> void
49
50
 
51
+ def no_more_requests?: () -> bool
52
+
50
53
  private
51
54
 
52
- def initialize: (Buffer buffer, Options options) -> untyped
55
+ def initialize: (Buffer buffer, Array[Request] pending, Options options) -> untyped
56
+
57
+ def waiting_for_peer?: () -> bool
53
58
 
54
- def can_buffer_more_requests?: () -> bool
59
+ def can_buffer_pending_requests?: () -> bool
55
60
 
56
61
  def send_pending: () -> void
57
62
 
@@ -119,6 +124,7 @@ module HTTPX
119
124
  UNRECOVERABLE_ERRORS: Array[Symbol]
120
125
 
121
126
  @code: Symbol
127
+ @last_stream_id: Integer
122
128
 
123
129
  def initialize: (Symbol code, Integer last_stream_id) -> void
124
130