httpx 0.6.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/doc/release_notes/0_0_1.md +7 -0
  4. data/doc/release_notes/0_0_2.md +9 -0
  5. data/doc/release_notes/0_0_3.md +9 -0
  6. data/doc/release_notes/0_0_4.md +7 -0
  7. data/doc/release_notes/0_0_5.md +5 -0
  8. data/doc/release_notes/0_1_0.md +9 -0
  9. data/doc/release_notes/0_2_0.md +5 -0
  10. data/doc/release_notes/0_2_1.md +16 -0
  11. data/doc/release_notes/0_3_0.md +12 -0
  12. data/doc/release_notes/0_3_1.md +6 -0
  13. data/doc/release_notes/0_4_0.md +51 -0
  14. data/doc/release_notes/0_4_1.md +3 -0
  15. data/doc/release_notes/0_5_0.md +15 -0
  16. data/doc/release_notes/0_5_1.md +14 -0
  17. data/doc/release_notes/0_6_0.md +5 -0
  18. data/doc/release_notes/0_6_1.md +6 -0
  19. data/doc/release_notes/0_6_2.md +6 -0
  20. data/doc/release_notes/0_6_3.md +13 -0
  21. data/doc/release_notes/0_6_4.md +21 -0
  22. data/doc/release_notes/0_6_5.md +22 -0
  23. data/doc/release_notes/0_6_6.md +19 -0
  24. data/doc/release_notes/0_6_7.md +5 -0
  25. data/doc/release_notes/0_7_0.md +46 -0
  26. data/doc/release_notes/0_8_0.md +27 -0
  27. data/doc/release_notes/0_8_1.md +8 -0
  28. data/doc/release_notes/0_8_2.md +7 -0
  29. data/doc/release_notes/0_9_0.md +38 -0
  30. data/lib/httpx/adapters/faraday.rb +2 -2
  31. data/lib/httpx/altsvc.rb +18 -2
  32. data/lib/httpx/chainable.rb +27 -9
  33. data/lib/httpx/connection.rb +215 -65
  34. data/lib/httpx/connection/http1.rb +54 -18
  35. data/lib/httpx/connection/http2.rb +100 -37
  36. data/lib/httpx/extensions.rb +2 -2
  37. data/lib/httpx/headers.rb +2 -2
  38. data/lib/httpx/io/ssl.rb +11 -3
  39. data/lib/httpx/io/tcp.rb +12 -2
  40. data/lib/httpx/loggable.rb +6 -6
  41. data/lib/httpx/options.rb +43 -28
  42. data/lib/httpx/plugins/authentication.rb +1 -1
  43. data/lib/httpx/plugins/compression.rb +28 -8
  44. data/lib/httpx/plugins/compression/gzip.rb +22 -12
  45. data/lib/httpx/plugins/cookies.rb +12 -8
  46. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  47. data/lib/httpx/plugins/expect.rb +79 -0
  48. data/lib/httpx/plugins/follow_redirects.rb +1 -2
  49. data/lib/httpx/plugins/h2c.rb +0 -1
  50. data/lib/httpx/plugins/proxy.rb +23 -20
  51. data/lib/httpx/plugins/proxy/http.rb +9 -6
  52. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  53. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  54. data/lib/httpx/plugins/proxy/ssh.rb +0 -4
  55. data/lib/httpx/plugins/push_promise.rb +2 -2
  56. data/lib/httpx/plugins/retries.rb +32 -29
  57. data/lib/httpx/pool.rb +15 -10
  58. data/lib/httpx/registry.rb +2 -1
  59. data/lib/httpx/request.rb +8 -6
  60. data/lib/httpx/resolver.rb +7 -8
  61. data/lib/httpx/resolver/https.rb +15 -3
  62. data/lib/httpx/resolver/native.rb +22 -32
  63. data/lib/httpx/resolver/options.rb +2 -2
  64. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  65. data/lib/httpx/response.rb +17 -3
  66. data/lib/httpx/selector.rb +96 -95
  67. data/lib/httpx/session.rb +33 -34
  68. data/lib/httpx/timeout.rb +7 -1
  69. data/lib/httpx/version.rb +1 -1
  70. metadata +77 -20
@@ -64,7 +64,8 @@ module HTTPX
64
64
 
65
65
  case handler
66
66
  when Symbol, String
67
- const_get(handler)
67
+ obj = const_get(handler)
68
+ @registry[tag] = obj
68
69
  else
69
70
  handler
70
71
  end
@@ -37,8 +37,6 @@ module HTTPX
37
37
 
38
38
  attr_reader :options, :response
39
39
 
40
- attr_accessor :timer
41
-
42
40
  def_delegator :@body, :<<
43
41
 
44
42
  def_delegator :@body, :empty?
@@ -60,6 +58,12 @@ module HTTPX
60
58
  @state = :idle
61
59
  end
62
60
 
61
+ def interests
62
+ return :r if @state == :done || @state == :expect
63
+
64
+ :w
65
+ end
66
+
63
67
  # :nocov:
64
68
  if RUBY_VERSION < "2.2"
65
69
  # rubocop: disable Lint/UriEscapeUnescape:
@@ -83,7 +87,6 @@ module HTTPX
83
87
  def response=(response)
84
88
  return unless response
85
89
 
86
- @timer.cancel if @timer
87
90
  @response = response
88
91
  end
89
92
 
@@ -222,6 +225,7 @@ module HTTPX
222
225
  case nextstate
223
226
  when :idle
224
227
  @response = nil
228
+ @drainer = nil
225
229
  when :headers
226
230
  return unless @state == :idle
227
231
  when :body
@@ -238,15 +242,13 @@ module HTTPX
238
242
  when 100
239
243
  # deallocate
240
244
  @response = nil
241
- when 417
242
- @response = @response
243
- return
244
245
  end
245
246
  end
246
247
  when :done
247
248
  return if @state == :expect
248
249
  end
249
250
  @state = nextstate
251
+ emit(@state)
250
252
  nil
251
253
  end
252
254
 
@@ -1,19 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "resolv"
4
+ require "httpx/resolver/resolver_mixin"
5
+ require "httpx/resolver/system"
6
+ require "httpx/resolver/native"
7
+ require "httpx/resolver/https"
4
8
 
5
9
  module HTTPX
6
10
  module Resolver
7
- autoload :ResolverMixin, "httpx/resolver/resolver_mixin"
8
- autoload :System, "httpx/resolver/system"
9
- autoload :Native, "httpx/resolver/native"
10
- autoload :HTTPS, "httpx/resolver/https"
11
-
12
11
  extend Registry
13
12
 
14
- register :system, :System
15
- register :native, :Native
16
- register :https, :HTTPS
13
+ register :system, System
14
+ register :native, Native
15
+ register :https, HTTPS
17
16
 
18
17
  @lookup_mutex = Mutex.new
19
18
  @lookups = Hash.new { |h, k| h[k] = [] }
@@ -25,7 +25,7 @@ module HTTPX
25
25
 
26
26
  def_delegator :@connections, :empty?
27
27
 
28
- def_delegators :@resolver_connection, :to_io, :call, :interests, :close
28
+ def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
29
29
 
30
30
  def initialize(options)
31
31
  @options = Options.new(options)
@@ -62,8 +62,20 @@ module HTTPX
62
62
  resolver_connection.closed?
63
63
  end
64
64
 
65
+ def interests
66
+ return if @queries.empty?
67
+
68
+ resolver_connection.__send__(__method__)
69
+ end
70
+
65
71
  private
66
72
 
73
+ def connect
74
+ return if @queries.empty?
75
+
76
+ resolver_connection.__send__(__method__)
77
+ end
78
+
67
79
  def pool
68
80
  Thread.current[:httpx_connection_pool] ||= Pool.new
69
81
  end
@@ -84,7 +96,7 @@ module HTTPX
84
96
 
85
97
  hostname = hostname || @queries.key(connection) || connection.origin.host
86
98
  type = @_record_types[hostname].first
87
- log(label: "resolver: ") { "query #{type} for #{hostname}" }
99
+ log { "resolver: query #{type} for #{hostname}" }
88
100
  begin
89
101
  request = build_request(hostname, type)
90
102
  @requests[request] = connection
@@ -111,7 +123,7 @@ module HTTPX
111
123
  end
112
124
 
113
125
  def on_promise(_, stream)
114
- log(level: 2, label: "#{stream.id}: ") { "refusing stream!" }
126
+ log(level: 2) { "#{stream.id}: refusing stream!" }
115
127
  stream.refuse
116
128
  end
117
129
 
@@ -73,14 +73,6 @@ module HTTPX
73
73
  end
74
74
 
75
75
  def to_io
76
- case @state
77
- when :idle
78
- transition(:open)
79
- when :closed
80
- transition(:idle)
81
- transition(:open)
82
- end
83
- resolve if @queries.empty?
84
76
  @io.to_io
85
77
  end
86
78
 
@@ -93,11 +85,7 @@ module HTTPX
93
85
  rescue Errno::EHOSTUNREACH => e
94
86
  @ns_index += 1
95
87
  if @ns_index < @nameserver.size
96
- log(label: "resolver: ") do
97
- # :nocov:
98
- "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
99
- # :nocov:
100
- end
88
+ log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
101
89
  transition(:idle)
102
90
  else
103
91
  handle_error(e)
@@ -107,6 +95,14 @@ module HTTPX
107
95
  end
108
96
 
109
97
  def interests
98
+ case @state
99
+ when :idle
100
+ transition(:open)
101
+ when :closed
102
+ transition(:idle)
103
+ transition(:open)
104
+ end
105
+
110
106
  !@write_buffer.empty? || @queries.empty? ? :w : :r
111
107
  end
112
108
 
@@ -160,11 +156,7 @@ module HTTPX
160
156
  raise NativeResolveError.new(connection, host)
161
157
  else
162
158
  connections << connection
163
- log(label: "resolver: ") do
164
- # :nocov:
165
- "timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..."
166
- # :nocov:
167
- end
159
+ log { "resolver: timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..." }
168
160
  end
169
161
  end
170
162
  @queries = queries
@@ -174,14 +166,11 @@ module HTTPX
174
166
  def dread(wsize = @resolver_options.packet_size)
175
167
  loop do
176
168
  siz = @io.read(wsize, @read_buffer)
177
- unless siz
178
- emit(:close)
179
- return
180
- end
181
- return if siz.zero?
169
+ return unless siz && siz.positive?
182
170
 
183
- log(label: "resolver: ") { "READ: #{siz} bytes..." }
171
+ log { "resolver: READ: #{siz} bytes..." }
184
172
  parse(@read_buffer)
173
+ return if @state == :closed
185
174
  end
186
175
  end
187
176
 
@@ -190,12 +179,10 @@ module HTTPX
190
179
  return if @write_buffer.empty?
191
180
 
192
181
  siz = @io.write(@write_buffer)
193
- unless siz
194
- emit(:close)
195
- return
196
- end
197
- log(label: "resolver: ") { "WRITE: #{siz} bytes..." }
198
- return if siz.zero?
182
+ return unless siz && siz.positive?
183
+
184
+ log { "resolver: WRITE: #{siz} bytes..." }
185
+ return if @state == :closed
199
186
  end
200
187
  end
201
188
 
@@ -253,7 +240,7 @@ module HTTPX
253
240
  hostname = hostname || @queries.key(connection) || connection.origin.host
254
241
  @queries[hostname] = connection
255
242
  type = @_record_types[hostname].first
256
- log(label: "resolver: ") { "query #{type} for #{hostname}" }
243
+ log { "resolver: query #{type} for #{hostname}" }
257
244
  begin
258
245
  @write_buffer << Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
259
246
  rescue Resolv::DNS::EncodeError => e
@@ -269,7 +256,7 @@ module HTTPX
269
256
  uri = URI::Generic.build(scheme: "udp", port: port)
270
257
  uri.hostname = ip
271
258
  type = IO.registry(uri.scheme)
272
- log(label: "resolver: ") { "server: #{uri}..." }
259
+ log { "resolver: server: #{uri}..." }
273
260
  @io = type.new(uri, [IPAddr.new(ip)], @options)
274
261
  end
275
262
 
@@ -285,8 +272,11 @@ module HTTPX
285
272
  return unless @state == :idle
286
273
 
287
274
  build_socket
275
+
288
276
  @io.connect
289
277
  return unless @io.connected?
278
+
279
+ resolve if @queries.empty?
290
280
  when :closed
291
281
  return unless @state == :open
292
282
 
@@ -6,7 +6,7 @@ module HTTPX
6
6
  @options = options
7
7
  end
8
8
 
9
- def method_missing(m, *args, &block)
9
+ def method_missing(m, *, &block)
10
10
  if @options.key?(m)
11
11
  @options[m]
12
12
  else
@@ -14,7 +14,7 @@ module HTTPX
14
14
  end
15
15
  end
16
16
 
17
- def respond_to_missing?(m)
17
+ def respond_to_missing?(m, *)
18
18
  @options.key?(m) || super
19
19
  end
20
20
 
@@ -30,7 +30,7 @@ module HTTPX
30
30
  addresses.map! do |address|
31
31
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
32
32
  end
33
- log(label: "resolver: ") { "answer #{connection.origin.host}: #{addresses.inspect}" }
33
+ log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
34
34
  connection.addresses = addresses
35
35
  catch(:coalesced) { emit(:resolve, connection) }
36
36
  end
@@ -58,7 +58,7 @@ module HTTPX
58
58
  "HTTP/#{version} " \
59
59
  "@status=#{@status} " \
60
60
  "@headers=#{@headers} " \
61
- "@body=#{@body}>"
61
+ "@body=#{@body.bytesize}>"
62
62
  end
63
63
  # :nocov:
64
64
 
@@ -103,6 +103,8 @@ module HTTPX
103
103
  def read(*args)
104
104
  return unless @buffer
105
105
 
106
+ rewind
107
+
106
108
  @buffer.read(*args)
107
109
  end
108
110
 
@@ -148,6 +150,8 @@ module HTTPX
148
150
  def copy_to(dest)
149
151
  return unless @buffer
150
152
 
153
+ rewind
154
+
151
155
  if dest.respond_to?(:path) && @buffer.respond_to?(:path)
152
156
  FileUtils.mv(@buffer.path, dest.path)
153
157
  else
@@ -235,13 +239,11 @@ module HTTPX
235
239
 
236
240
  private
237
241
 
238
- # :nodoc:
239
242
  def mime_type(str)
240
243
  m = str.to_s[MIME_TYPE_RE, 1]
241
244
  m && m.strip.downcase
242
245
  end
243
246
 
244
- # :nodoc:
245
247
  def charset(str)
246
248
  m = str.to_s[CHARSET_RE, 1]
247
249
  m && m.strip.delete('"')
@@ -265,6 +267,18 @@ module HTTPX
265
267
  @error.message
266
268
  end
267
269
 
270
+ def reason
271
+ @error.class.name
272
+ end
273
+
274
+ def headers
275
+ {}
276
+ end
277
+
278
+ def body
279
+ @error.backtrace.join("\n")
280
+ end
281
+
268
282
  def raise_for_status
269
283
  raise @error
270
284
  end
@@ -1,5 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "io/wait"
4
+
5
+ module IOExtensions
6
+ refine IO do
7
+ def wait(timeout = nil, mode = :read)
8
+ case mode
9
+ when :read
10
+ wait_readable(timeout)
11
+ when :write
12
+ wait_writable(timeout)
13
+ when :read_write
14
+ r, w = IO.select([self], [self], nil, timeout)
15
+
16
+ return unless r || w
17
+
18
+ self
19
+ end
20
+ end
21
+ end
22
+ end
23
+
3
24
  class HTTPX::Selector
4
25
  READABLE = %i[rw r].freeze
5
26
  WRITABLE = %i[rw w].freeze
@@ -7,129 +28,109 @@ class HTTPX::Selector
7
28
  private_constant :READABLE
8
29
  private_constant :WRITABLE
9
30
 
10
- #
11
- # I/O monitor
12
- #
13
- class Monitor
14
- attr_accessor :io, :interests, :readiness
15
-
16
- def initialize(io, interests, reactor)
17
- @io = io
18
- @interests = interests
19
- @reactor = reactor
20
- @closed = false
21
- end
22
-
23
- def readable?
24
- READABLE.include?(@interests)
25
- end
26
-
27
- def writable?
28
- WRITABLE.include?(@interests)
29
- end
30
-
31
- # closes +@io+, deregisters from reactor (unless +deregister+ is false)
32
- def close(deregister = true)
33
- return if @closed
34
-
35
- @closed = true
36
- @reactor.deregister(@io) if deregister
37
- end
38
-
39
- def closed?
40
- @closed
41
- end
42
-
43
- # :nocov:
44
- def to_s
45
- "#<#{self.class}: #{@io}(closed:#{@closed}) #{@interests} #{object_id.to_s(16)}>"
46
- end
47
- # :nocov:
48
- end
31
+ using IOExtensions unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
49
32
 
50
33
  def initialize
51
- @selectables = {}
52
- @__r__, @__w__ = IO.pipe
53
- @closed = false
34
+ @selectables = []
54
35
  end
55
36
 
56
37
  # deregisters +io+ from selectables.
57
38
  def deregister(io)
58
- monitor = @selectables.delete(io)
59
- monitor.close(false) if monitor
39
+ @selectables.delete(io)
60
40
  end
61
41
 
62
- # register +io+ for +interests+ events.
63
- def register(io, interests)
64
- monitor = @selectables[io]
65
- if monitor
66
- monitor.interests = interests
67
- else
68
- monitor = Monitor.new(io, interests, self)
69
- @selectables[io] = monitor
70
- end
71
- monitor
72
- end
42
+ # register +io+.
43
+ def register(io)
44
+ return if @selectables.include?(io)
73
45
 
74
- # waits for read/write events for +interval+. Yields for monitors of
75
- # selected IO objects.
76
- #
77
- def select(interval)
78
- begin
79
- r = [@__r__]
80
- w = []
46
+ @selectables << io
47
+ end
81
48
 
82
- @selectables.each do |io, monitor|
83
- r << io if monitor.interests == :r || monitor.interests == :rw
84
- w << io if monitor.interests == :w || monitor.interests == :rw
85
- monitor.readiness = nil
49
+ private
50
+
51
+ READ_INTERESTS = %i[r rw].freeze
52
+ WRITE_INTERESTS = %i[w rw].freeze
53
+
54
+ def select_many(interval)
55
+ selectables, r, w = nil
56
+
57
+ # first, we group IOs based on interest type. On call to #interests however,
58
+ # things might already happen, and new IOs might be registered, so we might
59
+ # have to start all over again. We do this until we group all selectables
60
+ loop do
61
+ begin
62
+ r = nil
63
+ w = nil
64
+
65
+ selectables = @selectables
66
+ @selectables = []
67
+
68
+ selectables.each do |io|
69
+ interests = io.interests
70
+
71
+ (r ||= []) << io if READ_INTERESTS.include?(interests)
72
+ (w ||= []) << io if WRITE_INTERESTS.include?(interests)
73
+ end
74
+
75
+ if @selectables.empty?
76
+ @selectables = selectables
77
+ break
78
+ else
79
+ @selectables = [*selectables, @selectables]
80
+ end
81
+ rescue StandardError
82
+ @selectables = selectables if selectables
83
+ raise
86
84
  end
85
+ end
86
+
87
+ # TODO: what to do if there are no selectables?
87
88
 
89
+ begin
88
90
  readers, writers = IO.select(r, w, nil, interval)
89
91
 
90
92
  raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
91
93
  rescue IOError, SystemCallError
92
- @selectables.reject! { |io, _| io.closed? }
94
+ @selectables.reject!(&:closed?)
93
95
  retry
94
96
  end
95
97
 
96
98
  readers.each do |io|
97
- if io == @__r__
98
- # clean up wakeups
99
- @__r__.read(@__r__.stat.size)
100
- else
101
- monitor = io.closed? ? @selectables.delete(io) : @selectables[io]
102
- next unless monitor
103
-
104
- monitor.readiness = writers.delete(io) ? :rw : :r
105
- yield monitor
106
- end
99
+ yield io
100
+
101
+ # so that we don't yield 2 times
102
+ writers.delete(io)
107
103
  end if readers
108
104
 
109
105
  writers.each do |io|
110
- monitor = io.closed? ? @selectables.delete(io) : @selectables[io]
111
- next unless monitor
112
-
113
- # don't double run this, the last iteration might have run this task already
114
- monitor.readiness = :w
115
- yield monitor
106
+ yield io
116
107
  end if writers
117
108
  end
118
109
 
119
- # Closes the selector.
120
- #
121
- def close
122
- return if @closed
110
+ def select_one(interval)
111
+ io = @selectables.first
112
+
113
+ interests = io.interests
114
+
115
+ result = case interests
116
+ when :r then io.to_io.wait_readable(interval)
117
+ when :w then io.to_io.wait_writable(interval)
118
+ when :rw then io.to_io.wait(interval, :read_write)
119
+ when nil then return
120
+ end
121
+
122
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result
123
123
 
124
- @__r__.close
125
- @__w__.close
126
- rescue IOError
127
- ensure
128
- @closed = true
124
+ yield io
125
+ rescue IOError, SystemCallError
126
+ @selectables.reject!(&:closed?)
129
127
  end
130
128
 
131
- # interrupts the select call.
132
- def wakeup
133
- @__w__.write_nonblock("\0", exception: false)
129
+ def select(interval, &block)
130
+ return select_one(interval, &block) if @selectables.size == 1
131
+
132
+ select_many(interval, &block)
134
133
  end
134
+
135
+ public :select
135
136
  end