httpx 0.16.1 → 0.17.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 (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