httpx 0.10.2 → 0.12.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -5
  3. data/doc/release_notes/0_11_0.md +76 -0
  4. data/doc/release_notes/0_11_1.md +5 -0
  5. data/doc/release_notes/0_11_2.md +5 -0
  6. data/doc/release_notes/0_11_3.md +5 -0
  7. data/doc/release_notes/0_12_0.md +55 -0
  8. data/lib/httpx.rb +2 -1
  9. data/lib/httpx/adapters/datadog.rb +205 -0
  10. data/lib/httpx/adapters/faraday.rb +4 -8
  11. data/lib/httpx/adapters/webmock.rb +123 -0
  12. data/lib/httpx/altsvc.rb +1 -0
  13. data/lib/httpx/chainable.rb +1 -1
  14. data/lib/httpx/connection.rb +63 -15
  15. data/lib/httpx/connection/http1.rb +16 -5
  16. data/lib/httpx/connection/http2.rb +36 -29
  17. data/lib/httpx/domain_name.rb +1 -3
  18. data/lib/httpx/errors.rb +2 -0
  19. data/lib/httpx/headers.rb +1 -0
  20. data/lib/httpx/io.rb +16 -3
  21. data/lib/httpx/io/ssl.rb +7 -13
  22. data/lib/httpx/io/tcp.rb +9 -8
  23. data/lib/httpx/io/tls.rb +218 -0
  24. data/lib/httpx/io/tls/box.rb +365 -0
  25. data/lib/httpx/io/tls/context.rb +199 -0
  26. data/lib/httpx/io/tls/ffi.rb +390 -0
  27. data/lib/httpx/io/udp.rb +4 -3
  28. data/lib/httpx/parser/http1.rb +4 -4
  29. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  30. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  31. data/lib/httpx/plugins/compression.rb +1 -1
  32. data/lib/httpx/plugins/compression/deflate.rb +2 -5
  33. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  34. data/lib/httpx/plugins/expect.rb +33 -8
  35. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  36. data/lib/httpx/plugins/multipart.rb +42 -35
  37. data/lib/httpx/plugins/multipart/encoder.rb +110 -0
  38. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  39. data/lib/httpx/plugins/multipart/part.rb +34 -0
  40. data/lib/httpx/plugins/proxy.rb +1 -1
  41. data/lib/httpx/plugins/proxy/http.rb +1 -1
  42. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  43. data/lib/httpx/plugins/proxy/socks5.rb +11 -2
  44. data/lib/httpx/plugins/push_promise.rb +5 -4
  45. data/lib/httpx/plugins/retries.rb +1 -1
  46. data/lib/httpx/plugins/stream.rb +3 -5
  47. data/lib/httpx/pool.rb +0 -1
  48. data/lib/httpx/registry.rb +1 -7
  49. data/lib/httpx/request.rb +32 -12
  50. data/lib/httpx/resolver.rb +7 -4
  51. data/lib/httpx/resolver/https.rb +7 -13
  52. data/lib/httpx/resolver/native.rb +10 -6
  53. data/lib/httpx/resolver/system.rb +1 -1
  54. data/lib/httpx/response.rb +9 -2
  55. data/lib/httpx/selector.rb +6 -0
  56. data/lib/httpx/session.rb +40 -20
  57. data/lib/httpx/transcoder.rb +6 -4
  58. data/lib/httpx/transcoder/body.rb +3 -5
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/connection/http1.rbs +2 -2
  61. data/sig/connection/http2.rbs +8 -7
  62. data/sig/headers.rbs +3 -0
  63. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  64. data/sig/plugins/aws_sigv4.rbs +65 -0
  65. data/sig/plugins/multipart.rbs +27 -4
  66. data/sig/plugins/push_promise.rbs +1 -1
  67. data/sig/request.rbs +1 -1
  68. data/sig/resolver/https.rbs +2 -0
  69. data/sig/response.rbs +1 -1
  70. data/sig/session.rbs +1 -1
  71. data/sig/transcoder.rbs +2 -2
  72. data/sig/transcoder/body.rbs +2 -0
  73. data/sig/transcoder/form.rbs +7 -1
  74. data/sig/transcoder/json.rbs +3 -1
  75. metadata +50 -47
  76. data/sig/missing.rbs +0 -12
@@ -43,7 +43,7 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def __on_promise_request(parser, stream, h)
46
- log(level: 1) do
46
+ log(level: 1, color: :yellow) do
47
47
  # :nocov:
48
48
  h.map { |k, v| "#{stream.id}: -> PROMISE HEADER: #{k}: #{v}" }.join("\n")
49
49
  # :nocov:
@@ -57,6 +57,8 @@ module HTTPX
57
57
  request.merge_headers(headers)
58
58
  promise_headers[stream] = request
59
59
  parser.pending.delete(request)
60
+ parser.streams[request] = stream
61
+ request.transition(:done)
60
62
  else
61
63
  stream.refuse
62
64
  end
@@ -67,11 +69,10 @@ module HTTPX
67
69
  return unless request
68
70
 
69
71
  parser.__send__(:on_stream_headers, stream, request, h)
70
- request.transition(:done)
71
72
  response = request.response
72
73
  response.mark_as_pushed!
73
- stream.on(:data, &parser.method(:on_stream_data).curry[stream, request])
74
- stream.on(:close, &parser.method(:on_stream_close).curry[stream, request])
74
+ stream.on(:data, &parser.method(:on_stream_data).curry(3)[stream, request])
75
+ stream.on(:close, &parser.method(:on_stream_close).curry(3)[stream, request])
75
76
  end
76
77
  end
77
78
  end
@@ -17,7 +17,7 @@ module HTTPX
17
17
  Errno::ECONNRESET,
18
18
  Errno::ECONNABORTED,
19
19
  Errno::EPIPE,
20
- (OpenSSL::SSL::SSLError if defined?(OpenSSL)),
20
+ (TLSError if defined?(TLSError)),
21
21
  TimeoutError,
22
22
  Parser::Error,
23
23
  Errno::EINVAL,
@@ -119,11 +119,9 @@ module HTTPX
119
119
  end
120
120
 
121
121
  def method_missing(meth, *args, &block)
122
- if @options.response_class.public_method_defined?(meth)
123
- response.__send__(meth, *args, &block)
124
- else
125
- super
126
- end
122
+ return super unless @options.response_class.public_method_defined?(meth)
123
+
124
+ response.__send__(meth, *args, &block)
127
125
  end
128
126
  end
129
127
  end
data/lib/httpx/pool.rb CHANGED
@@ -135,7 +135,6 @@ module HTTPX
135
135
  connection.on(:close) do
136
136
  unregister_connection(connection)
137
137
  end
138
- return if connection.state == :open
139
138
  end
140
139
 
141
140
  def unregister_connection(connection)
@@ -62,13 +62,7 @@ module HTTPX
62
62
  handler = @registry.fetch(tag)
63
63
  raise(Error, "#{tag} is not registered in #{self}") unless handler
64
64
 
65
- case handler
66
- when Symbol, String
67
- obj = const_get(handler)
68
- @registry[tag] = obj
69
- else
70
- handler
71
- end
65
+ handler
72
66
  end
73
67
 
74
68
  # @param [Object] tag the identifier for the handler in the registry
data/lib/httpx/request.rb CHANGED
@@ -81,6 +81,10 @@ module HTTPX
81
81
  def response=(response)
82
82
  return unless response
83
83
 
84
+ if response.status == 100
85
+ @informational_status = response.status
86
+ return
87
+ end
84
88
  @response = response
85
89
  end
86
90
 
@@ -170,6 +174,12 @@ module HTTPX
170
174
  end
171
175
  end
172
176
 
177
+ def rewind
178
+ return if empty?
179
+
180
+ @body.rewind if @body.respond_to?(:rewind)
181
+ end
182
+
173
183
  def empty?
174
184
  return true if @body.nil?
175
185
  return false if chunked?
@@ -207,11 +217,22 @@ module HTTPX
207
217
  "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
208
218
  end
209
219
  # :nocov:
220
+
221
+ def respond_to_missing?(meth, *args)
222
+ @body.respond_to?(meth, *args) || super
223
+ end
224
+
225
+ def method_missing(meth, *args, &block)
226
+ return super unless @body.respond_to?(meth)
227
+
228
+ @body.__send__(meth, *args, &block)
229
+ end
210
230
  end
211
231
 
212
232
  def transition(nextstate)
213
233
  case nextstate
214
234
  when :idle
235
+ @body.rewind
215
236
  @response = nil
216
237
  @drainer = nil
217
238
  when :headers
@@ -221,28 +242,27 @@ module HTTPX
221
242
  @state == :expect
222
243
 
223
244
  if @headers.key?("expect")
224
- unless @response
225
- @state = :expect
226
- return
227
- end
228
-
229
- case @response.status
230
- when 100
231
- # deallocate
232
- @response = nil
245
+ if @informational_status && @informational_status == 100
246
+ # check for 100 Continue response, and deallocate the var
247
+ # if @informational_status == 100
248
+ # @response = nil
249
+ # end
250
+ else
251
+ return if @state == :expect # do not re-set it
252
+
253
+ nextstate = :expect
233
254
  end
234
255
  end
235
256
  when :done
236
257
  return if @state == :expect
237
258
  end
238
259
  @state = nextstate
239
- emit(@state)
260
+ emit(@state, self)
240
261
  nil
241
262
  end
242
263
 
243
264
  def expects?
244
- @headers["expect"] == "100-continue" &&
245
- @response && @response.status == 100
265
+ @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
246
266
  end
247
267
 
248
268
  class ProcIO
@@ -1,15 +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"
8
4
 
9
5
  module HTTPX
10
6
  module Resolver
11
7
  extend Registry
12
8
 
9
+ RESOLVE_TIMEOUT = 5
10
+
11
+ require "httpx/resolver/resolver_mixin"
12
+ require "httpx/resolver/system"
13
+ require "httpx/resolver/native"
14
+ require "httpx/resolver/https"
15
+
13
16
  register :system, System
14
17
  register :native, Native
15
18
  register :https, HTTPS
@@ -37,12 +37,14 @@ module HTTPX
37
37
  @connections = []
38
38
  @uri = URI(@resolver_options[:uri])
39
39
  @uri_addresses = nil
40
+ @resolver = Resolv::DNS.new
41
+ @resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
40
42
  end
41
43
 
42
44
  def <<(connection)
43
45
  return if @uri.origin == connection.origin.to_s
44
46
 
45
- @uri_addresses ||= Resolv.getaddresses(@uri.host)
47
+ @uri_addresses ||= ip_resolve(@uri.host) || system_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
46
48
 
47
49
  if @uri_addresses.empty?
48
50
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
@@ -53,20 +55,12 @@ module HTTPX
53
55
  early_resolve(connection) || resolve(connection)
54
56
  end
55
57
 
56
- def timeout
57
- @connections.map(&:timeout).min
58
- end
59
-
60
58
  def closed?
61
- return true unless @resolver_connection
62
-
63
- resolver_connection.closed?
59
+ true
64
60
  end
65
61
 
66
- def interests
67
- return if @queries.empty?
68
-
69
- resolver_connection.__send__(__method__)
62
+ def empty?
63
+ true
70
64
  end
71
65
 
72
66
  private
@@ -99,7 +93,7 @@ module HTTPX
99
93
  log { "resolver: query #{type} for #{hostname}" }
100
94
  begin
101
95
  request = build_request(hostname, type)
102
- request.on(:response, &method(:on_response).curry[request])
96
+ request.on(:response, &method(:on_response).curry(2)[request])
103
97
  request.on(:promise, &method(:on_promise))
104
98
  @requests[request] = connection
105
99
  resolver_connection.send(request)
@@ -9,7 +9,6 @@ module HTTPX
9
9
  include Resolver::ResolverMixin
10
10
  using URIExtensions
11
11
 
12
- RESOLVE_TIMEOUT = 5
13
12
  RECORD_TYPES = {
14
13
  "A" => Resolv::DNS::Resource::IN::A,
15
14
  "AAAA" => Resolv::DNS::Resource::IN::AAAA,
@@ -19,7 +18,7 @@ module HTTPX
19
18
  {
20
19
  **Resolv::DNS::Config.default_config_hash,
21
20
  packet_size: 512,
22
- timeouts: RESOLVE_TIMEOUT,
21
+ timeouts: Resolver::RESOLVE_TIMEOUT,
23
22
  record_types: RECORD_TYPES.keys,
24
23
  }.freeze
25
24
  else
@@ -27,7 +26,7 @@ module HTTPX
27
26
  nameserver: nil,
28
27
  **Resolv::DNS::Config.default_config_hash,
29
28
  packet_size: 512,
30
- timeouts: RESOLVE_TIMEOUT,
29
+ timeouts: Resolver::RESOLVE_TIMEOUT,
31
30
  record_types: RECORD_TYPES.keys,
32
31
  }.freeze
33
32
  end
@@ -148,14 +147,19 @@ module HTTPX
148
147
  queries[h] = connection
149
148
  next
150
149
  end
150
+
151
151
  @timeouts[host].shift
152
152
  if @timeouts[host].empty?
153
153
  @timeouts.delete(host)
154
154
  @connections.delete(connection)
155
- raise NativeResolveError.new(connection, host)
155
+ # This loop_time passed to the exception is bogus. Ideally we would pass the total
156
+ # resolve timeout, including from the previous retries.
157
+ raise ResolveTimeoutError.new(loop_time, "Timed out")
158
+ # raise NativeResolveError.new(connection, host)
156
159
  else
160
+ log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
157
161
  connections << connection
158
- log { "resolver: timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..." }
162
+ queries[h] = connection
159
163
  end
160
164
  end
161
165
  @queries = queries
@@ -279,7 +283,7 @@ module HTTPX
279
283
  @io.connect
280
284
  return unless @io.connected?
281
285
 
282
- resolve if @queries.empty?
286
+ resolve if @queries.empty? && !@connections.empty?
283
287
  when :closed
284
288
  return unless @state == :open
285
289
 
@@ -20,7 +20,7 @@ module HTTPX
20
20
  timeouts = resolv_options.delete(:timeouts)
21
21
  resolv_options.delete(:cache)
22
22
  @resolver = Resolv::DNS.new(resolv_options.empty? ? nil : resolv_options)
23
- @resolver.timeouts = timeouts if timeouts
23
+ @resolver.timeouts = timeouts || Resolver::RESOLVE_TIMEOUT
24
24
  end
25
25
 
26
26
  def closed?
@@ -268,8 +268,15 @@ module HTTPX
268
268
  @error.message
269
269
  end
270
270
 
271
- def to_s
272
- @error.backtrace.join("\n")
271
+ if Exception.method_defined?(:full_message)
272
+ def to_s
273
+ @error.full_message
274
+ end
275
+ else
276
+ def to_s
277
+ "#{@error.message} (#{@error.class})\n" \
278
+ "#{@error.backtrace.join("\n") if @error.backtrace}"
279
+ end
273
280
  end
274
281
 
275
282
  def raise_for_status
@@ -69,6 +69,11 @@ class HTTPX::Selector
69
69
 
70
70
  if @selectables.empty?
71
71
  @selectables = selectables
72
+
73
+ # do not run event loop if there's nothing to wait on.
74
+ # this might happen if connect failed and connection was unregistered.
75
+ return if (!r || r.empty?) && (!w || w.empty?)
76
+
72
77
  break
73
78
  else
74
79
  @selectables = [*selectables, @selectables]
@@ -117,6 +122,7 @@ class HTTPX::Selector
117
122
  yield io
118
123
  rescue IOError, SystemCallError
119
124
  @selectables.reject!(&:closed?)
125
+ raise unless @selectables.empty?
120
126
  end
121
127
 
122
128
  def select(interval, &block)
data/lib/httpx/session.rb CHANGED
@@ -41,7 +41,7 @@ module HTTPX
41
41
  def build_request(verb, uri, options = EMPTY_HASH)
42
42
  rklass = @options.request_class
43
43
  request = rklass.new(verb, uri, @options.merge(options).merge(persistent: @persistent))
44
- request.on(:response, &method(:on_response).curry[request])
44
+ request.on(:response, &method(:on_response).curry(2)[request])
45
45
  request.on(:promise, &method(:on_promise))
46
46
  request
47
47
  end
@@ -136,23 +136,20 @@ module HTTPX
136
136
  def build_requests(*args, options)
137
137
  request_options = @options.merge(options)
138
138
 
139
- requests = case args.size
140
- when 1
141
- reqs = args.first
142
- reqs.map do |verb, uri, opts = EMPTY_HASH|
143
- build_request(verb, uri, request_options.merge(opts))
144
- end
145
- when 2
146
- verb, uris = args
147
- if uris.respond_to?(:each)
148
- uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
149
- build_request(verb, uri, request_options.merge(opts))
150
- end
151
- else
152
- [build_request(verb, uris, request_options)]
153
- end
154
- else
155
- raise ArgumentError, "unsupported number of arguments"
139
+ requests = if args.size == 1
140
+ reqs = args.first
141
+ reqs.map do |verb, uri, opts = EMPTY_HASH|
142
+ build_request(verb, uri, request_options.merge(opts))
143
+ end
144
+ else
145
+ verb, uris = args
146
+ if uris.respond_to?(:each)
147
+ uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
148
+ build_request(verb, uri, request_options.merge(opts))
149
+ end
150
+ else
151
+ [build_request(verb, uris, request_options)]
152
+ end
156
153
  end
157
154
  raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty?
158
155
 
@@ -202,7 +199,18 @@ module HTTPX
202
199
  responses << response
203
200
  requests.shift
204
201
 
205
- break if requests.empty? || pool.empty?
202
+ break if requests.empty?
203
+
204
+ next unless pool.empty?
205
+
206
+ # in some cases, the pool of connections might have been drained because there was some
207
+ # handshake error, and the error responses have already been emitted, but there was no
208
+ # opportunity to traverse the requests, hence we're returning only a fraction of the errors
209
+ # we were supposed to. This effectively fetches the existing responses and return them.
210
+ while (request = requests.shift)
211
+ responses << fetch_response(request, connections, request_options)
212
+ end
213
+ break
206
214
  end
207
215
  responses
208
216
  ensure
@@ -272,7 +280,19 @@ module HTTPX
272
280
  end
273
281
  # :nocov:
274
282
  end
283
+ end
284
+
285
+ unless ENV.grep(/https?_proxy$/i).empty?
286
+ proxy_session = plugin(:proxy)
287
+ ::HTTPX.send(:remove_const, :Session)
288
+ ::HTTPX.send(:const_set, :Session, proxy_session.class)
289
+ end
275
290
 
276
- plugin(:proxy) unless ENV.grep(/https?_proxy$/i).empty?
291
+ # :nocov:
292
+ if Session.default_options.debug_level > 2
293
+ proxy_session = plugin(:internal_telemetry)
294
+ ::HTTPX.send(:remove_const, :Session)
295
+ ::HTTPX.send(:const_set, :Session, proxy_session.class)
277
296
  end
297
+ # :nocov:
278
298
  end
@@ -4,18 +4,20 @@ module HTTPX
4
4
  module Transcoder
5
5
  extend Registry
6
6
 
7
- def self.normalize_keys(key, value, &block)
8
- if value.respond_to?(:to_ary)
7
+ def self.normalize_keys(key, value, cond = nil, &block)
8
+ if (cond && cond.call(value))
9
+ block.call(key.to_s, value)
10
+ elsif value.respond_to?(:to_ary)
9
11
  if value.empty?
10
12
  block.call("#{key}[]")
11
13
  else
12
14
  value.to_ary.each do |element|
13
- normalize_keys("#{key}[]", element, &block)
15
+ normalize_keys("#{key}[]", element, cond, &block)
14
16
  end
15
17
  end
16
18
  elsif value.respond_to?(:to_hash)
17
19
  value.to_hash.each do |child_key, child_value|
18
- normalize_keys("#{key}[#{child_key}]", child_value, &block)
20
+ normalize_keys("#{key}[#{child_key}]", child_value, cond, &block)
19
21
  end
20
22
  else
21
23
  block.call(key.to_s, value)