httpx 0.15.4 → 0.18.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_16_0.md +93 -0
  3. data/doc/release_notes/0_16_1.md +5 -0
  4. data/doc/release_notes/0_17_0.md +49 -0
  5. data/doc/release_notes/0_18_0.md +69 -0
  6. data/lib/httpx/adapters/datadog.rb +1 -1
  7. data/lib/httpx/adapters/faraday.rb +8 -14
  8. data/lib/httpx/adapters/webmock.rb +9 -3
  9. data/lib/httpx/altsvc.rb +2 -2
  10. data/lib/httpx/buffer.rb +1 -1
  11. data/lib/httpx/callbacks.rb +1 -1
  12. data/lib/httpx/chainable.rb +18 -11
  13. data/lib/httpx/connection/http1.rb +21 -13
  14. data/lib/httpx/connection/http2.rb +20 -25
  15. data/lib/httpx/connection.rb +73 -77
  16. data/lib/httpx/domain_name.rb +1 -1
  17. data/lib/httpx/errors.rb +11 -11
  18. data/lib/httpx/extensions.rb +50 -4
  19. data/lib/httpx/headers.rb +1 -1
  20. data/lib/httpx/io/ssl.rb +3 -3
  21. data/lib/httpx/io/tls.rb +8 -8
  22. data/lib/httpx/loggable.rb +5 -5
  23. data/lib/httpx/options.rb +108 -81
  24. data/lib/httpx/parser/http1.rb +11 -7
  25. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  26. data/lib/httpx/plugins/aws_sigv4.rb +19 -20
  27. data/lib/httpx/plugins/compression.rb +17 -14
  28. data/lib/httpx/plugins/cookies/cookie.rb +4 -2
  29. data/lib/httpx/plugins/cookies/jar.rb +21 -2
  30. data/lib/httpx/plugins/cookies.rb +20 -7
  31. data/lib/httpx/plugins/digest_authentication.rb +19 -15
  32. data/lib/httpx/plugins/expect.rb +26 -18
  33. data/lib/httpx/plugins/follow_redirects.rb +9 -9
  34. data/lib/httpx/plugins/grpc/call.rb +4 -1
  35. data/lib/httpx/plugins/grpc/message.rb +2 -2
  36. data/lib/httpx/plugins/grpc.rb +72 -46
  37. data/lib/httpx/plugins/h2c.rb +7 -3
  38. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  39. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  40. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  41. data/lib/httpx/plugins/multipart/part.rb +2 -2
  42. data/lib/httpx/plugins/multipart.rb +16 -2
  43. data/lib/httpx/plugins/ntlm_authentication.rb +12 -10
  44. data/lib/httpx/plugins/proxy/socks4.rb +2 -1
  45. data/lib/httpx/plugins/proxy/socks5.rb +2 -1
  46. data/lib/httpx/plugins/proxy/ssh.rb +20 -13
  47. data/lib/httpx/plugins/proxy.rb +10 -10
  48. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  49. data/lib/httpx/plugins/response_cache.rb +88 -0
  50. data/lib/httpx/plugins/retries.rb +46 -23
  51. data/lib/httpx/plugins/stream.rb +3 -4
  52. data/lib/httpx/plugins/upgrade.rb +7 -6
  53. data/lib/httpx/pool.rb +39 -13
  54. data/lib/httpx/registry.rb +2 -2
  55. data/lib/httpx/request.rb +16 -25
  56. data/lib/httpx/resolver/https.rb +4 -8
  57. data/lib/httpx/resolver/native.rb +19 -5
  58. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  59. data/lib/httpx/resolver/system.rb +2 -0
  60. data/lib/httpx/resolver.rb +2 -2
  61. data/lib/httpx/response.rb +91 -48
  62. data/lib/httpx/selector.rb +11 -24
  63. data/lib/httpx/session.rb +41 -23
  64. data/lib/httpx/session2.rb +23 -0
  65. data/lib/httpx/timers.rb +84 -0
  66. data/lib/httpx/transcoder/body.rb +3 -2
  67. data/lib/httpx/transcoder/chunker.rb +2 -1
  68. data/lib/httpx/transcoder/form.rb +20 -0
  69. data/lib/httpx/transcoder/json.rb +12 -0
  70. data/lib/httpx/transcoder.rb +62 -1
  71. data/lib/httpx/utils.rb +10 -2
  72. data/lib/httpx/version.rb +1 -1
  73. data/lib/httpx.rb +7 -3
  74. data/sig/buffer.rbs +3 -1
  75. data/sig/chainable.rbs +31 -29
  76. data/sig/connection/http1.rbs +11 -5
  77. data/sig/connection/http2.rbs +16 -5
  78. data/sig/connection.rbs +31 -13
  79. data/sig/errors.rbs +35 -1
  80. data/sig/headers.rbs +20 -19
  81. data/sig/httpx.rbs +4 -1
  82. data/sig/loggable.rbs +3 -1
  83. data/sig/options.rbs +45 -34
  84. data/sig/parser/http1.rbs +3 -3
  85. data/sig/plugins/authentication.rbs +1 -1
  86. data/sig/plugins/aws_sdk_authentication.rbs +25 -3
  87. data/sig/plugins/aws_sigv4.rbs +13 -5
  88. data/sig/plugins/basic_authentication.rbs +1 -1
  89. data/sig/plugins/compression.rbs +4 -6
  90. data/sig/plugins/cookies/cookie.rbs +5 -7
  91. data/sig/plugins/cookies/jar.rbs +9 -10
  92. data/sig/plugins/cookies.rbs +4 -5
  93. data/sig/plugins/digest_authentication.rbs +2 -3
  94. data/sig/plugins/expect.rbs +2 -4
  95. data/sig/plugins/follow_redirects.rbs +3 -5
  96. data/sig/plugins/grpc.rbs +4 -7
  97. data/sig/plugins/h2c.rbs +0 -2
  98. data/sig/plugins/multipart.rbs +64 -10
  99. data/sig/plugins/ntlm_authentication.rbs +2 -3
  100. data/sig/plugins/persistent.rbs +3 -8
  101. data/sig/plugins/proxy/ssh.rbs +4 -4
  102. data/sig/plugins/proxy.rbs +13 -13
  103. data/sig/plugins/push_promise.rbs +0 -2
  104. data/sig/plugins/response_cache.rbs +35 -0
  105. data/sig/plugins/retries.rbs +7 -8
  106. data/sig/plugins/stream.rbs +1 -1
  107. data/sig/plugins/upgrade.rbs +2 -3
  108. data/sig/pool.rbs +7 -2
  109. data/sig/registry.rbs +1 -1
  110. data/sig/request.rbs +11 -8
  111. data/sig/resolver/native.rbs +10 -5
  112. data/sig/resolver/resolver_mixin.rbs +4 -5
  113. data/sig/resolver/system.rbs +4 -0
  114. data/sig/resolver.rbs +7 -0
  115. data/sig/response.rbs +26 -13
  116. data/sig/selector.rbs +11 -9
  117. data/sig/session.rbs +22 -23
  118. data/sig/timers.rbs +32 -0
  119. data/sig/transcoder/body.rbs +6 -1
  120. data/sig/transcoder/chunker.rbs +8 -2
  121. data/sig/transcoder/form.rbs +3 -1
  122. data/sig/transcoder/json.rbs +2 -0
  123. data/sig/transcoder.rbs +13 -5
  124. data/sig/utils.rbs +6 -0
  125. metadata +18 -18
  126. data/lib/httpx/request2.rb +0 -14
data/lib/httpx/request.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
3
4
  require "forwardable"
4
5
 
5
6
  module HTTPX
@@ -40,16 +41,15 @@ module HTTPX
40
41
 
41
42
  def_delegator :@body, :empty?
42
43
 
43
- def_delegator :@body, :chunk!
44
-
45
44
  def initialize(verb, uri, options = {})
46
45
  @verb = verb.to_s.downcase.to_sym
47
46
  @options = Options.new(options)
48
47
  @uri = Utils.to_uri(uri)
49
48
  if @uri.relative?
50
- raise(Error, "invalid URI: #{@uri}") unless @options.origin
49
+ origin = @options.origin
50
+ raise(Error, "invalid URI: #{@uri}") unless origin
51
51
 
52
- @uri = @options.origin.merge(@uri)
52
+ @uri = origin.merge(@uri)
53
53
  end
54
54
 
55
55
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
@@ -97,7 +97,7 @@ module HTTPX
97
97
  def response=(response)
98
98
  return unless response
99
99
 
100
- if response.status == 100
100
+ if response.is_a?(Response) && response.status == 100
101
101
  @informational_status = response.status
102
102
  return
103
103
  end
@@ -148,16 +148,16 @@ module HTTPX
148
148
  # :nocov:
149
149
  def inspect
150
150
  "#<HTTPX::Request:#{object_id} " \
151
- "#{@verb.to_s.upcase} " \
152
- "#{uri} " \
153
- "@headers=#{@headers} " \
154
- "@body=#{@body}>"
151
+ "#{@verb.to_s.upcase} " \
152
+ "#{uri} " \
153
+ "@headers=#{@headers} " \
154
+ "@body=#{@body}>"
155
155
  end
156
156
  # :nocov:
157
157
 
158
- class Body
158
+ class Body < SimpleDelegator
159
159
  class << self
160
- def new(*, options)
160
+ def new(_, options)
161
161
  return options.body if options.body.is_a?(self)
162
162
 
163
163
  super
@@ -177,10 +177,11 @@ module HTTPX
177
177
 
178
178
  @headers["content-type"] ||= @body.content_type
179
179
  @headers["content-length"] = @body.bytesize unless unbounded_body?
180
+ super(@body)
180
181
  end
181
182
 
182
183
  def each(&block)
183
- return enum_for(__method__) unless block_given?
184
+ return enum_for(__method__) unless block
184
185
  return if @body.nil?
185
186
 
186
187
  body = stream(@body)
@@ -214,14 +215,14 @@ module HTTPX
214
215
 
215
216
  def stream(body)
216
217
  encoded = body
217
- encoded = Transcoder.registry("chunker").encode(body) if chunked?
218
+ encoded = Transcoder.registry("chunker").encode(body.enum_for(:each)) if chunked?
218
219
  encoded
219
220
  end
220
221
 
221
222
  def unbounded_body?
222
223
  return @unbounded_body if defined?(@unbounded_body)
223
224
 
224
- @unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
225
+ @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
225
226
  end
226
227
 
227
228
  def chunked?
@@ -235,19 +236,9 @@ module HTTPX
235
236
  # :nocov:
236
237
  def inspect
237
238
  "#<HTTPX::Request::Body:#{object_id} " \
238
- "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
+ "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
240
  end
240
241
  # :nocov:
241
-
242
- def respond_to_missing?(meth, *args)
243
- @body.respond_to?(meth, *args) || super
244
- end
245
-
246
- def method_missing(meth, *args, &block)
247
- return super unless @body.respond_to?(meth)
248
-
249
- @body.__send__(meth, *args, &block)
250
- end
251
242
  end
252
243
 
253
244
  def transition(nextstate)
@@ -24,9 +24,9 @@ module HTTPX
24
24
  record_types: RECORD_TYPES.keys,
25
25
  }.freeze
26
26
 
27
- def_delegator :@connections, :empty?
27
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
28
28
 
29
- def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
29
+ attr_writer :pool
30
30
 
31
31
  def initialize(options)
32
32
  @options = Options.new(options)
@@ -65,15 +65,11 @@ module HTTPX
65
65
 
66
66
  private
67
67
 
68
- def pool
69
- Thread.current[:httpx_connection_pool] ||= Pool.new
70
- end
71
-
72
68
  def resolver_connection
73
- @resolver_connection ||= pool.find_connection(@uri, @options) || begin
69
+ @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
74
70
  @building_connection = true
75
71
  connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
76
- pool.init_connection(connection, @options)
72
+ @pool.init_connection(connection, @options)
77
73
  emit_addresses(connection, @uri_addresses)
78
74
  @building_connection = false
79
75
  connection
@@ -47,6 +47,8 @@ module HTTPX
47
47
 
48
48
  def_delegator :@connections, :empty?
49
49
 
50
+ attr_reader :state
51
+
50
52
  def initialize(options)
51
53
  @options = Options.new(options)
52
54
  @ns_index = 0
@@ -120,7 +122,7 @@ module HTTPX
120
122
  def timeout
121
123
  return if @connections.empty?
122
124
 
123
- @start_timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC)
125
+ @start_timeout = Utils.now
124
126
  hosts = @queries.keys
125
127
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
126
128
  end
@@ -140,7 +142,7 @@ module HTTPX
140
142
  def do_retry
141
143
  return if @queries.empty?
142
144
 
143
- loop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_timeout
145
+ loop_time = Utils.elapsed_time(@start_timeout)
144
146
  connections = []
145
147
  queries = {}
146
148
  while (query = @queries.shift)
@@ -217,15 +219,27 @@ module HTTPX
217
219
  end
218
220
  else
219
221
  address = addresses.first
220
- connection = @queries.delete(address["name"])
221
- return unless connection # probably a retried query for which there's an answer
222
+ name = address["name"]
223
+
224
+ connection = @queries.delete(name)
225
+
226
+ unless connection
227
+ # absolute name
228
+ name_labels = Resolv::DNS::Name.create(name).to_a
229
+ name = @queries.keys.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
230
+
231
+ # probably a retried query for which there's an answer
232
+ return unless name
233
+
234
+ address["name"] = name
235
+ connection = @queries.delete(name)
236
+ end
222
237
 
223
238
  if address.key?("alias") # CNAME
224
239
  if early_resolve(connection, hostname: address["alias"])
225
240
  @connections.delete(connection)
226
241
  else
227
242
  resolve(connection, address["alias"])
228
- @queries.delete(address["name"])
229
243
  return
230
244
  end
231
245
  else
@@ -9,7 +9,7 @@ module HTTPX
9
9
  include Callbacks
10
10
  include Loggable
11
11
 
12
- CHECK_IF_IP = proc do |name|
12
+ CHECK_IF_IP = lambda do |name|
13
13
  begin
14
14
  IPAddr.new(name)
15
15
  true
@@ -55,6 +55,7 @@ module HTTPX
55
55
  return if ips.empty?
56
56
 
57
57
  ips.map { |ip| IPAddr.new(ip) }
58
+ rescue IOError
58
59
  end
59
60
 
60
61
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -12,6 +12,8 @@ module HTTPX
12
12
  Resolv::DNS::EncodeError,
13
13
  Resolv::DNS::DecodeError].freeze
14
14
 
15
+ attr_reader :state
16
+
15
17
  def initialize(options)
16
18
  @options = Options.new(options)
17
19
  @resolver_options = @options.resolver_options
@@ -26,14 +26,14 @@ module HTTPX
26
26
  module_function
27
27
 
28
28
  def cached_lookup(hostname)
29
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ now = Utils.now
30
30
  @lookup_mutex.synchronize do
31
31
  lookup(hostname, now)
32
32
  end
33
33
  end
34
34
 
35
35
  def cached_lookup_set(hostname, entries)
36
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ now = Utils.now
37
37
  entries.each do |entry|
38
38
  entry["TTL"] += now
39
39
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "objspace"
3
4
  require "stringio"
4
5
  require "tempfile"
5
6
  require "fileutils"
@@ -13,6 +14,8 @@ module HTTPX
13
14
 
14
15
  def_delegator :@body, :to_s
15
16
 
17
+ def_delegator :@body, :to_str
18
+
16
19
  def_delegator :@body, :read
17
20
 
18
21
  def_delegator :@body, :copy_to
@@ -44,7 +47,7 @@ module HTTPX
44
47
  end
45
48
 
46
49
  def content_type
47
- ContentType.parse(@headers["content-type"])
50
+ @content_type ||= ContentType.new(@headers["content-type"])
48
51
  end
49
52
 
50
53
  def complete?
@@ -54,21 +57,50 @@ module HTTPX
54
57
  # :nocov:
55
58
  def inspect
56
59
  "#<Response:#{object_id} "\
57
- "HTTP/#{version} " \
58
- "@status=#{@status} " \
59
- "@headers=#{@headers} " \
60
- "@body=#{@body.bytesize}>"
60
+ "HTTP/#{version} " \
61
+ "@status=#{@status} " \
62
+ "@headers=#{@headers} " \
63
+ "@body=#{@body.bytesize}>"
61
64
  end
62
65
  # :nocov:
63
66
 
64
- def raise_for_status
67
+ def error
65
68
  return if @status < 400
66
69
 
67
- raise HTTPError, self
70
+ HTTPError.new(self)
71
+ end
72
+
73
+ def raise_for_status
74
+ return self unless (err = error)
75
+
76
+ raise err
77
+ end
78
+
79
+ def json(options = nil)
80
+ decode("json", options)
81
+ end
82
+
83
+ def form
84
+ decode("form")
68
85
  end
69
86
 
70
87
  private
71
88
 
89
+ def decode(format, options = nil)
90
+ # TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
91
+ transcoder = Transcoder.registry(format)
92
+
93
+ raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decode)
94
+
95
+ decoder = transcoder.decode(self)
96
+
97
+ raise Error, "no decoder available for \"#{format}\"" unless decoder
98
+
99
+ decoder.call(self, options)
100
+ rescue Registry::Error
101
+ raise Error, "no decoder available for \"#{format}\""
102
+ end
103
+
72
104
  def no_data?
73
105
  @status < 200 ||
74
106
  @status == 204 ||
@@ -134,18 +166,26 @@ module HTTPX
134
166
  end
135
167
 
136
168
  def to_s
137
- rewind
138
- if @buffer
139
- content = @buffer.read
169
+ case @buffer
170
+ when StringIO
171
+ begin
172
+ @buffer.string.force_encoding(@encoding)
173
+ rescue ArgumentError
174
+ @buffer.string
175
+ end
176
+ when Tempfile, File
177
+ rewind
178
+ content = _with_same_buffer_pos { @buffer.read }
140
179
  begin
141
- return content.force_encoding(@encoding)
180
+ content.force_encoding(@encoding)
142
181
  rescue ArgumentError # ex: unknown encoding name - utf
143
- return content
182
+ content
144
183
  end
184
+ when nil
185
+ "".b
186
+ else
187
+ @buffer
145
188
  end
146
- "".b
147
- ensure
148
- close
149
189
  end
150
190
  alias_method :to_str, :to_s
151
191
 
@@ -177,14 +217,20 @@ module HTTPX
177
217
  end
178
218
 
179
219
  def ==(other)
180
- to_s == other.to_s
220
+ object_id == other.object_id || begin
221
+ if other.respond_to?(:read)
222
+ _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
223
+ else
224
+ to_s == other.to_s
225
+ end
226
+ end
181
227
  end
182
228
 
183
229
  # :nocov:
184
230
  def inspect
185
231
  "#<HTTPX::Response::Body:#{object_id} " \
186
- "@state=#{@state} " \
187
- "@length=#{@length}>"
232
+ "@state=#{@state} " \
233
+ "@length=#{@length}>"
188
234
  end
189
235
  # :nocov:
190
236
 
@@ -204,7 +250,7 @@ module HTTPX
204
250
  @buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
205
251
  else
206
252
  @state = :memory
207
- @buffer = StringIO.new("".b, File::RDWR)
253
+ @buffer = StringIO.new("".b)
208
254
  end
209
255
  when :memory
210
256
  if @length > @threshold_size
@@ -222,6 +268,18 @@ module HTTPX
222
268
 
223
269
  return unless %i[memory buffer].include?(@state)
224
270
  end
271
+
272
+ def _with_same_buffer_pos
273
+ return yield unless @buffer && @buffer.respond_to?(:pos)
274
+
275
+ current_pos = @buffer.pos
276
+ @buffer.rewind
277
+ begin
278
+ yield
279
+ rescue StandardError
280
+ @buffer.pos = current_pos
281
+ end
282
+ end
225
283
  end
226
284
  end
227
285
 
@@ -229,30 +287,22 @@ module HTTPX
229
287
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
230
288
  CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
231
289
 
232
- attr_reader :mime_type, :charset
233
-
234
- def initialize(mime_type, charset)
235
- @mime_type = mime_type
236
- @charset = charset
290
+ def initialize(header_value)
291
+ @header_value = header_value
237
292
  end
238
293
 
239
- class << self
240
- # Parse string and return ContentType struct
241
- def parse(str)
242
- new(mime_type(str), charset(str))
243
- end
294
+ def mime_type
295
+ return @mime_type if defined?(@mime_type)
244
296
 
245
- private
297
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
298
+ m && @mime_type = m.strip.downcase
299
+ end
246
300
 
247
- def mime_type(str)
248
- m = str.to_s[MIME_TYPE_RE, 1]
249
- m && m.strip.downcase
250
- end
301
+ def charset
302
+ return @charset if defined?(@charset)
251
303
 
252
- def charset(str)
253
- m = str.to_s[CHARSET_RE, 1]
254
- m && m.strip.delete('"')
255
- end
304
+ m = @header_value.to_s[CHARSET_RE, 1]
305
+ m && @charset = m.strip.delete('"')
256
306
  end
257
307
  end
258
308
 
@@ -269,31 +319,24 @@ module HTTPX
269
319
  end
270
320
 
271
321
  def status
322
+ warn ":#{__method__} is deprecated, use :error.message instead"
272
323
  @error.message
273
324
  end
274
325
 
275
326
  if Exception.method_defined?(:full_message)
276
327
  def to_s
277
- @error.full_message
328
+ @error.full_message(highlight: false)
278
329
  end
279
330
  else
280
331
  def to_s
281
332
  "#{@error.message} (#{@error.class})\n" \
282
- "#{@error.backtrace.join("\n") if @error.backtrace}"
333
+ "#{@error.backtrace.join("\n") if @error.backtrace}"
283
334
  end
284
335
  end
285
336
 
286
337
  def raise_for_status
287
338
  raise @error
288
339
  end
289
-
290
- # rubocop:disable Style/MissingRespondToMissing
291
- def method_missing(meth, *, &block)
292
- raise NoMethodError, "undefined response method `#{meth}' for error response" if @options.response_class.public_method_defined?(meth)
293
-
294
- super
295
- end
296
- # rubocop:enable Style/MissingRespondToMissing
297
340
  end
298
341
  end
299
342
 
@@ -2,20 +2,6 @@
2
2
 
3
3
  require "io/wait"
4
4
 
5
- module IOExtensions
6
- refine IO do
7
- # provides a fallback for rubies where IO#wait isn't implemented,
8
- # but IO#wait_readable and IO#wait_writable are.
9
- def wait(timeout = nil, _mode = :read_write)
10
- r, w = IO.select([self], [self], nil, timeout)
11
-
12
- return unless r || w
13
-
14
- self
15
- end
16
- end
17
- end
18
-
19
5
  class HTTPX::Selector
20
6
  READABLE = %i[rw r].freeze
21
7
  WRITABLE = %i[rw w].freeze
@@ -23,7 +9,7 @@ class HTTPX::Selector
23
9
  private_constant :READABLE
24
10
  private_constant :WRITABLE
25
11
 
26
- using IOExtensions unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
12
+ using HTTPX::IOExtensions
27
13
 
28
14
  def initialize
29
15
  @selectables = []
@@ -43,9 +29,6 @@ class HTTPX::Selector
43
29
 
44
30
  private
45
31
 
46
- READ_INTERESTS = %i[r rw].freeze
47
- WRITE_INTERESTS = %i[w rw].freeze
48
-
49
32
  def select_many(interval, &block)
50
33
  selectables, r, w = nil
51
34
 
@@ -61,11 +44,13 @@ class HTTPX::Selector
61
44
  selectables = @selectables
62
45
  @selectables = []
63
46
 
64
- selectables.each do |io|
47
+ selectables.delete_if do |io|
65
48
  interests = io.interests
66
49
 
67
- (r ||= []) << io if READ_INTERESTS.include?(interests)
68
- (w ||= []) << io if WRITE_INTERESTS.include?(interests)
50
+ (r ||= []) << io if READABLE.include?(interests)
51
+ (w ||= []) << io if WRITABLE.include?(interests)
52
+
53
+ io.state == :closed
69
54
  end
70
55
 
71
56
  if @selectables.empty?
@@ -73,7 +58,7 @@ class HTTPX::Selector
73
58
 
74
59
  # do not run event loop if there's nothing to wait on.
75
60
  # this might happen if connect failed and connection was unregistered.
76
- return if (!r || r.empty?) && (!w || w.empty?)
61
+ return if (!r || r.empty?) && (!w || w.empty?) && !selectables.empty?
77
62
 
78
63
  break
79
64
  else
@@ -89,7 +74,7 @@ class HTTPX::Selector
89
74
 
90
75
  readers, writers = IO.select(r, w, nil, interval)
91
76
 
92
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
77
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil? && interval
93
78
  rescue IOError, SystemCallError
94
79
  @selectables.reject!(&:closed?)
95
80
  retry
@@ -112,6 +97,8 @@ class HTTPX::Selector
112
97
  def select_one(interval)
113
98
  io = @selectables.first
114
99
 
100
+ return unless io
101
+
115
102
  interests = io.interests
116
103
 
117
104
  result = case interests
@@ -121,7 +108,7 @@ class HTTPX::Selector
121
108
  when nil then return
122
109
  end
123
110
 
124
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result
111
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result || interval.nil?
125
112
 
126
113
  yield io
127
114
  rescue IOError, SystemCallError