httpx 0.16.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_17_0.md +49 -0
  3. data/lib/httpx/adapters/webmock.rb +2 -2
  4. data/lib/httpx/chainable.rb +1 -1
  5. data/lib/httpx/connection/http1.rb +15 -9
  6. data/lib/httpx/connection/http2.rb +13 -10
  7. data/lib/httpx/connection.rb +4 -5
  8. data/lib/httpx/headers.rb +1 -1
  9. data/lib/httpx/options.rb +28 -6
  10. data/lib/httpx/parser/http1.rb +10 -6
  11. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  12. data/lib/httpx/plugins/h2c.rb +7 -3
  13. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  14. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  15. data/lib/httpx/plugins/multipart/part.rb +2 -2
  16. data/lib/httpx/plugins/multipart.rb +14 -0
  17. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  18. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  19. data/lib/httpx/plugins/proxy.rb +6 -4
  20. data/lib/httpx/plugins/stream.rb +2 -3
  21. data/lib/httpx/registry.rb +1 -1
  22. data/lib/httpx/request.rb +6 -7
  23. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  24. data/lib/httpx/response.rb +37 -30
  25. data/lib/httpx/selector.rb +4 -2
  26. data/lib/httpx/session.rb +15 -13
  27. data/lib/httpx/transcoder/form.rb +20 -0
  28. data/lib/httpx/transcoder/json.rb +12 -0
  29. data/lib/httpx/transcoder.rb +62 -1
  30. data/lib/httpx/utils.rb +2 -2
  31. data/lib/httpx/version.rb +1 -1
  32. data/sig/buffer.rbs +2 -2
  33. data/sig/chainable.rbs +6 -1
  34. data/sig/connection/http1.rbs +10 -4
  35. data/sig/connection/http2.rbs +16 -5
  36. data/sig/connection.rbs +4 -4
  37. data/sig/headers.rbs +19 -18
  38. data/sig/options.rbs +13 -5
  39. data/sig/parser/http1.rbs +3 -3
  40. data/sig/plugins/aws_sigv4.rbs +12 -3
  41. data/sig/plugins/basic_authentication.rbs +1 -1
  42. data/sig/plugins/multipart.rbs +64 -8
  43. data/sig/plugins/proxy.rbs +6 -6
  44. data/sig/request.rbs +11 -8
  45. data/sig/resolver/native.rbs +4 -2
  46. data/sig/resolver/resolver_mixin.rbs +1 -1
  47. data/sig/resolver/system.rbs +1 -1
  48. data/sig/response.rbs +8 -2
  49. data/sig/selector.rbs +8 -6
  50. data/sig/session.rbs +8 -14
  51. data/sig/transcoder/form.rbs +1 -0
  52. data/sig/transcoder/json.rbs +1 -0
  53. data/sig/transcoder.rbs +5 -4
  54. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 005e6856a12b549480cb2fcf8becd893e041026e9df634fa102fc410ab3527ef
4
- data.tar.gz: 00722d3ffe0822eb6f434df84db78e9ffb9b53cbd103220546dc611f3763117c
3
+ metadata.gz: f6b2befec2e4b0093acd45f7ef9ef448ad41516510710616aded46934f1e3981
4
+ data.tar.gz: a9858adfacbdc27e1097b98b958f7dd1614f1207c74ec4290a54268df6270f69
5
5
  SHA512:
6
- metadata.gz: e8c2c8c1ff44c845a9773e79b3564ed7491cbbcbfd6aa20ea8e93a7a4a029f5afe177c53693173b493efe54122324448ee14e34807aa37db77d3699cc7bb75e2
7
- data.tar.gz: bad8e79d228c37dd3efafcf530ba7188e4759e5aca807c7b693a7102b6a2bc339663bade32d78c80a62e691f8a8378731f864639965554ab78f4f031994f0626
6
+ metadata.gz: 86276d59efaf3a15efe0a27fbd59bff2d005bb3bab83ee9917599854bf46eba1bfbb016b9df55172c41799d1fe82195e4bb7d82c008c1996814ec2e393be71b3
7
+ data.tar.gz: bd113e65cf5700f231992bb485f6c59511f114372d53078ea53c019a654e449e5012b2a1a19f889d79cee1eecd24345d3e03f3f9fa50aa6c69060138327e1d09
@@ -0,0 +1,49 @@
1
+ # 0.17.0
2
+
3
+ ## Features
4
+
5
+ ### Response mime type decoders (#json, #form)
6
+
7
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Response-Handling#response-decoding
8
+
9
+ Two new methods, `#json` and `#form`, were added to `HTTPX::Response`. As the name implies, they'll decode the raw payload into ruby objects you can work with.
10
+
11
+ ```ruby
12
+ # after HTTPX.get("https://api.smth/endpoint-returning-json")
13
+ response.json # same as JSON.dump(response.to_s)
14
+ ```
15
+
16
+ Although not yet documented, integrating custom decoders is also possible (i.e. parsing HTML with `nokogiri` or something similar).
17
+
18
+ ## Improvements
19
+
20
+ ### Connection: reduce interest calculations
21
+
22
+ Due to it being an intensive task, internal interest calculation in connections was reduce to the bare minimum.
23
+
24
+ ### Immutable Options, internal recycling of instances, improves memory usage in the happy path
25
+
26
+ A lot of effort went into avoiding generating options objects internally whenever necessary. This means, when sending several requests with the same set of options (the most common case in `httpx` usage), internally only one object is passed around. For that, the following improvements were done:
27
+
28
+ * `Options#merge` returns the same options the the options being merged are a subset of the current set of options (b126938a6547e09b726dd64298fb488891d938e9).
29
+ * `Session#build_request` bypasses instantiation of options if it receives an `Options` object (which happens internally in the happy path, if users don't call `#build_request` directly) (3d549817cb41d4b904102fdc61afe3ecd9170893).
30
+ * Improving internal `Session` APIs to not pass around options, and instead rely on accessing request options.
31
+ * `Options#to_hash` does not build internal garbage arrays anymore (cc02679b804f63798f5d2136a039be1624e96ab6).
32
+
33
+ ### Reduce regexp operations in the HTTP/1 parser
34
+
35
+ Some code paths in the HTTP/1 parser still using regular expressions were replaced by string operations accomplishing the same.
36
+
37
+ ### HTTP/1 improvements on the complexity of connection accounting calculations
38
+
39
+ Managing open HTTP/1 connections relies on operations calculating whether there are requests waiting for completion. This relied on traversing all requests for that connectionn (O(n)); it now only checks the completion state of the first and last request of that connection, given that all requests in HTTP/1 are sequential (O(1)); this optimization brings a big improvement to persistent and pipelined requests (65261217b1270913e4bb93717e8b8dcfa775565a).
40
+
41
+ ## Bugfixes
42
+
43
+ * fixing HTTP/1 protocol uncompliant exposing multiple values for the "Host" header (e435dd0534314508262184fb03d83124d89d2079).
44
+
45
+ * Custom response finalizer introduced in 0.16.0 has been reverted. It was brought to my attention that `Tempfile` implementation already takes care of the file on GC (and `httpx` was duplicating), and the approach taken in `httpx` was buggy in several ways (not tolerant to forks, never recycled finalizers...) (aa3be21c890f92a41afcc7931f01dd24cc801f7c).
46
+
47
+ ## Chore
48
+
49
+ RBS Typing improvements based on latest stdlib signatures additions, such as `openssl`, `digest`, `socket` and others.
@@ -19,7 +19,7 @@ module WebMock
19
19
  module InstanceMethods
20
20
  private
21
21
 
22
- def send_requests(*requests, options)
22
+ def send_requests(*requests)
23
23
  request_signatures = requests.map do |request|
24
24
  request_signature = _build_webmock_request_signature(request)
25
25
  WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
@@ -47,7 +47,7 @@ module WebMock
47
47
 
48
48
  unless real_requests.empty?
49
49
  reqs = real_requests.keys
50
- reqs.zip(super(*reqs, options)).each do |req, res|
50
+ reqs.zip(super(*reqs)).each do |req, res|
51
51
  idx = real_requests[req]
52
52
 
53
53
  if WebMock::CallbackRegistry.any_callbacks?
@@ -78,7 +78,7 @@ module HTTPX
78
78
  with(option.to_sym => (args.first || options))
79
79
  end
80
80
 
81
- def respond_to_missing?(meth)
81
+ def respond_to_missing?(meth, *)
82
82
  return super unless meth =~ /\Awith_(.+)/
83
83
 
84
84
  option = Regexp.last_match(1)
@@ -59,7 +59,12 @@ module HTTPX
59
59
  def empty?
60
60
  # this means that for every request there's an available
61
61
  # partial response, so there are no in-flight requests waiting.
62
- @requests.empty? || @requests.all? { |request| !request.response.nil? }
62
+ @requests.empty? || (
63
+ # checking all responses can be time-consuming. Alas, as in HTTP/1, responses
64
+ # do not come out of order, we can get away with checking first and last.
65
+ !@requests.first.response.nil? &&
66
+ (@requests.size == 1 || !@requests.last.response.nil?)
67
+ )
63
68
  end
64
69
 
65
70
  def <<(data)
@@ -260,7 +265,7 @@ module HTTPX
260
265
  def set_protocol_headers(request)
261
266
  if !request.headers.key?("content-length") &&
262
267
  request.body.bytesize == Float::INFINITY
263
- request.chunk!
268
+ request.body.chunk!
264
269
  end
265
270
 
266
271
  connection = request.headers["connection"]
@@ -285,10 +290,9 @@ module HTTPX
285
290
  end
286
291
  end
287
292
 
288
- {
289
- "host" => (request.headers["host"] || request.authority),
290
- "connection" => connection,
291
- }
293
+ extra_headers = { "connection" => connection }
294
+ extra_headers["host"] = request.authority unless request.headers.key?("host")
295
+ extra_headers
292
296
  end
293
297
 
294
298
  def headline_uri(request)
@@ -318,7 +322,7 @@ module HTTPX
318
322
  end
319
323
 
320
324
  def join_body(request)
321
- return if request.empty?
325
+ return if request.body.empty?
322
326
 
323
327
  while (chunk = request.drain_body)
324
328
  log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
@@ -327,7 +331,9 @@ module HTTPX
327
331
  throw(:buffer_full, request) if @buffer.full?
328
332
  end
329
333
 
330
- raise request.drain_error if request.drain_error
334
+ return unless (error = request.drain_error)
335
+
336
+ raise error
331
337
  end
332
338
 
333
339
  def join_trailers(request)
@@ -354,7 +360,7 @@ module HTTPX
354
360
  }.freeze
355
361
 
356
362
  def capitalized(field)
357
- UPCASED[field] || field.to_s.split("-").map(&:capitalize).join("-")
363
+ UPCASED[field] || field.split("-").map(&:capitalize).join("-")
358
364
  end
359
365
  end
360
366
  Connection.register "http/1.1", Connection::HTTP1
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require "io/wait"
5
4
  require "http/2/next"
6
5
 
7
6
  module HTTPX
@@ -56,7 +55,7 @@ module HTTPX
56
55
 
57
56
  return :w if !@pending.empty? && can_buffer_more_requests?
58
57
 
59
- return :w if @streams.each_key.any? { |r| r.interests == :w }
58
+ return :w unless @drains.empty?
60
59
 
61
60
  if @buffer.empty?
62
61
  return if @streams.empty? && @pings.empty?
@@ -218,7 +217,7 @@ module HTTPX
218
217
  log(level: 1, color: :yellow) do
219
218
  request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
220
219
  end
221
- stream.headers(request.headers.each(extra_headers), end_stream: request.empty?)
220
+ stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
222
221
  end
223
222
 
224
223
  def join_trailers(stream, request)
@@ -234,7 +233,7 @@ module HTTPX
234
233
  end
235
234
 
236
235
  def join_body(stream, request)
237
- return if request.empty?
236
+ return if request.body.empty?
238
237
 
239
238
  chunk = @drains.delete(request) || request.drain_body
240
239
  while chunk
@@ -249,7 +248,9 @@ module HTTPX
249
248
  chunk = next_chunk
250
249
  end
251
250
 
252
- on_stream_refuse(stream, request, request.drain_error) if request.drain_error
251
+ return unless (error = request.drain_error)
252
+
253
+ on_stream_refuse(stream, request, error)
253
254
  end
254
255
 
255
256
  ######
@@ -257,8 +258,10 @@ module HTTPX
257
258
  ######
258
259
 
259
260
  def on_stream_headers(stream, request, h)
260
- if request.response && request.response.version == "2.0"
261
- on_stream_trailers(stream, request, h)
261
+ response = request.response
262
+
263
+ if response.is_a?(Response) && response.version == "2.0"
264
+ on_stream_trailers(stream, response, h)
262
265
  return
263
266
  end
264
267
 
@@ -274,11 +277,11 @@ module HTTPX
274
277
  handle(request, stream) if request.expects?
275
278
  end
276
279
 
277
- def on_stream_trailers(stream, request, h)
280
+ def on_stream_trailers(stream, response, h)
278
281
  log(color: :yellow) do
279
282
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
280
283
  end
281
- request.response.merge_headers(h)
284
+ response.merge_headers(h)
282
285
  end
283
286
 
284
287
  def on_stream_data(stream, request, data)
@@ -304,7 +307,7 @@ module HTTPX
304
307
  emit(:response, request, response)
305
308
  else
306
309
  response = request.response
307
- if response.status == 421
310
+ if response && response.status == 421
308
311
  ex = MisdirectedRequestError.new(response)
309
312
  ex.set_backtrace(caller)
310
313
  emit(:error, request, ex)
@@ -313,7 +313,7 @@ module HTTPX
313
313
 
314
314
  # exit #consume altogether if all outstanding requests have been dealt with
315
315
  return if @pending.size.zero? && @inflight.zero?
316
- end unless (interests.nil? || interests == :w || @state == :closing) && !epiped
316
+ end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
317
317
 
318
318
  #
319
319
  # tight write loop.
@@ -360,19 +360,18 @@ module HTTPX
360
360
  break if interests == :r || @state == :closing || @state == :closed
361
361
 
362
362
  write_drained = false
363
- end unless interests == :r
363
+ end unless (ints = interests) == :r
364
364
 
365
365
  send_pending if @state == :open
366
366
 
367
367
  # return if socket is drained
368
- next unless (interests != :r || read_drained) &&
369
- (interests != :w || write_drained)
368
+ next unless (ints != :r || read_drained) && (ints != :w || write_drained)
370
369
 
371
370
  # gotta go back to the event loop. It happens when:
372
371
  #
373
372
  # * the socket is drained of bytes or it's not the interest of the conn to read;
374
373
  # * theres nothing more to write, or it's not in the interest of the conn to write;
375
- log(level: 3) { "(#{interests}): WAITING FOR EVENTS..." }
374
+ log(level: 3) { "(#{ints}): WAITING FOR EVENTS..." }
376
375
  return
377
376
  end
378
377
  end
data/lib/httpx/headers.rb CHANGED
@@ -57,7 +57,7 @@ module HTTPX
57
57
  def merge(other)
58
58
  headers = dup
59
59
  other.each do |field, value|
60
- headers[field] = value
60
+ headers[downcased(field)] = value
61
61
  end
62
62
  headers
63
63
  end
data/lib/httpx/options.rb CHANGED
@@ -39,6 +39,28 @@ module HTTPX
39
39
  :resolver_options => { cache: true },
40
40
  }.freeze
41
41
 
42
+ begin
43
+ module HashExtensions
44
+ refine Hash do
45
+ def >=(other)
46
+ Hash[other] <= self
47
+ end
48
+
49
+ def <=(other)
50
+ other = Hash[other]
51
+ return false unless size <= other.size
52
+
53
+ each do |k, v|
54
+ v2 = other.fetch(k) { return false }
55
+ return false unless v2 == v
56
+ end
57
+ true
58
+ end
59
+ end
60
+ end
61
+ using HashExtensions
62
+ end unless Hash.method_defined?(:>=)
63
+
42
64
  class << self
43
65
  def new(options = {})
44
66
  # let enhanced options go through
@@ -89,7 +111,7 @@ module HTTPX
89
111
 
90
112
  def initialize(options = {})
91
113
  defaults = DEFAULT_OPTIONS.merge(options)
92
- defaults.each do |(k, v)|
114
+ defaults.each do |k, v|
93
115
  next if v.nil?
94
116
 
95
117
  begin
@@ -163,6 +185,7 @@ module HTTPX
163
185
  end
164
186
 
165
187
  REQUEST_IVARS = %i[@params @form @json @body].freeze
188
+ private_constant :REQUEST_IVARS
166
189
 
167
190
  def ==(other)
168
191
  ivars = instance_variables | other.instance_variables
@@ -180,14 +203,14 @@ module HTTPX
180
203
  end
181
204
 
182
205
  def merge(other)
183
- raise ArgumentError, "#{other.inspect} is not a valid set of options" unless other.respond_to?(:to_hash)
206
+ raise ArgumentError, "#{other} is not a valid set of options" unless other.respond_to?(:to_hash)
184
207
 
185
208
  h2 = other.to_hash
186
209
  return self if h2.empty?
187
210
 
188
211
  h1 = to_hash
189
212
 
190
- return self if h1 == h2
213
+ return self if h1 >= h2
191
214
 
192
215
  merged = h1.merge(h2) do |_k, v1, v2|
193
216
  if v1.respond_to?(:merge) && v2.respond_to?(:merge)
@@ -201,10 +224,9 @@ module HTTPX
201
224
  end
202
225
 
203
226
  def to_hash
204
- hash_pairs = instance_variables.map do |ivar|
205
- [ivar[1..-1].to_sym, instance_variable_get(ivar)]
227
+ instance_variables.each_with_object({}) do |ivar, hs|
228
+ hs[ivar[1..-1].to_sym] = instance_variable_get(ivar)
206
229
  end
207
- Hash[hash_pairs]
208
230
  end
209
231
 
210
232
  if RUBY_VERSION > "2.4.0"
@@ -60,7 +60,7 @@ module HTTPX
60
60
  (m = %r{\AHTTP(?:/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
61
61
  raise(Error, "wrong head line format")
62
62
  version, code, _ = m.captures
63
- raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
63
+ raise(Error, "unsupported HTTP version (HTTP/#{version})") unless version && VERSIONS.include?(version)
64
64
 
65
65
  @http_version = version.split(".").map(&:to_i)
66
66
  @status_code = code.to_i
@@ -72,9 +72,14 @@ module HTTPX
72
72
 
73
73
  def parse_headers
74
74
  headers = @headers
75
- while (idx = @buffer.index("\n"))
76
- line = @buffer.byteslice(0..idx).sub(/\s+\z/, "")
77
- @buffer = @buffer.byteslice((idx + 1)..-1)
75
+ buffer = @buffer
76
+
77
+ while (idx = buffer.index("\n"))
78
+ line = buffer.byteslice(0..idx)
79
+ raise Error, "wrong header format" if line.start_with?("\s", "\t")
80
+
81
+ line.lstrip!
82
+ buffer = @buffer = buffer.byteslice((idx + 1)..-1)
78
83
  if line.empty?
79
84
  case @state
80
85
  when :headers
@@ -97,9 +102,8 @@ module HTTPX
97
102
  raise Error, "wrong header format" unless separator_index
98
103
 
99
104
  key = line.byteslice(0..(separator_index - 1))
100
- raise Error, "wrong header format" if key.start_with?("\s", "\t")
101
105
 
102
- key.strip!
106
+ key.rstrip! # was lstripped previously!
103
107
  value = line.byteslice((separator_index + 1)..-1)
104
108
  value.strip!
105
109
  raise Error, "wrong header format" if value.nil?
@@ -40,12 +40,12 @@ module HTTPX
40
40
 
41
41
  alias_method :digest_auth, :digest_authentication
42
42
 
43
- def send_requests(*requests, options)
43
+ def send_requests(*requests)
44
44
  requests.flat_map do |request|
45
45
  digest = request.options.digest
46
46
 
47
47
  if digest
48
- probe_response = wrap { super(request, options).first }
48
+ probe_response = wrap { super(request).first }
49
49
 
50
50
  if digest && !probe_response.is_a?(ErrorResponse) &&
51
51
  probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
@@ -56,12 +56,12 @@ module HTTPX
56
56
  token = digest.generate_header(request, probe_response)
57
57
  request.headers["authorization"] = "Digest #{token}"
58
58
 
59
- super(request, options)
59
+ super(request)
60
60
  else
61
61
  probe_response
62
62
  end
63
63
  else
64
- super(request, options)
64
+ super(request)
65
65
  end
66
66
  end
67
67
  end
@@ -24,15 +24,19 @@ module HTTPX
24
24
  def call(connection, request, response)
25
25
  connection.upgrade_to_h2c(request, response)
26
26
  end
27
+
28
+ def extra_options(options)
29
+ options.merge(max_concurrent_requests: 1)
30
+ end
27
31
  end
28
32
 
29
33
  module InstanceMethods
30
- def send_requests(*requests, options)
34
+ def send_requests(*requests)
31
35
  upgrade_request, *remainder = requests
32
36
 
33
37
  return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
34
38
 
35
- connection = pool.find_connection(upgrade_request.uri, @options.merge(options))
39
+ connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
36
40
 
37
41
  return super if connection && connection.upgrade_protocol == :h2c
38
42
 
@@ -42,7 +46,7 @@ module HTTPX
42
46
  upgrade_request.headers["upgrade"] = "h2c"
43
47
  upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
44
48
 
45
- super(upgrade_request, *remainder, options.merge(max_concurrent_requests: 1))
49
+ super(upgrade_request, *remainder)
46
50
  end
47
51
  end
48
52