httpx 0.6.7 → 0.9.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 (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