httpx 0.10.0 → 0.10.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_10_1.md +39 -0
  3. data/lib/httpx/chainable.rb +7 -6
  4. data/lib/httpx/connection.rb +4 -15
  5. data/lib/httpx/connection/http1.rb +14 -1
  6. data/lib/httpx/connection/http2.rb +11 -12
  7. data/lib/httpx/errors.rb +1 -1
  8. data/lib/httpx/plugins/multipart.rb +15 -1
  9. data/lib/httpx/plugins/proxy.rb +16 -2
  10. data/lib/httpx/plugins/proxy/socks4.rb +14 -16
  11. data/lib/httpx/pool.rb +8 -14
  12. data/lib/httpx/request.rb +1 -1
  13. data/lib/httpx/resolver.rb +0 -2
  14. data/lib/httpx/resolver/https.rb +15 -22
  15. data/lib/httpx/resolver/native.rb +12 -13
  16. data/lib/httpx/resolver/resolver_mixin.rb +4 -2
  17. data/lib/httpx/resolver/system.rb +2 -2
  18. data/lib/httpx/selector.rb +8 -13
  19. data/lib/httpx/session.rb +9 -3
  20. data/lib/httpx/transcoder.rb +18 -0
  21. data/lib/httpx/transcoder/form.rb +9 -1
  22. data/lib/httpx/version.rb +1 -1
  23. data/sig/connection.rbs +84 -1
  24. data/sig/connection/http1.rbs +66 -0
  25. data/sig/connection/http2.rbs +74 -0
  26. data/sig/httpx.rbs +1 -0
  27. data/sig/options.rbs +3 -3
  28. data/sig/plugins/basic_authentication.rbs +1 -1
  29. data/sig/plugins/compression.rbs +1 -1
  30. data/sig/plugins/compression/brotli.rbs +1 -1
  31. data/sig/plugins/compression/deflate.rbs +1 -1
  32. data/sig/plugins/compression/gzip.rbs +1 -1
  33. data/sig/plugins/h2c.rbs +1 -1
  34. data/sig/plugins/multipart.rbs +4 -2
  35. data/sig/plugins/persistent.rbs +1 -1
  36. data/sig/plugins/proxy.rbs +2 -2
  37. data/sig/plugins/proxy/ssh.rbs +1 -1
  38. data/sig/plugins/rate_limiter.rbs +1 -1
  39. data/sig/pool.rbs +36 -2
  40. data/sig/request.rbs +1 -1
  41. data/sig/resolver.rbs +26 -0
  42. data/sig/resolver/https.rbs +49 -0
  43. data/sig/resolver/native.rbs +60 -0
  44. data/sig/resolver/resolver_mixin.rbs +27 -0
  45. data/sig/resolver/system.rbs +17 -0
  46. data/sig/response.rbs +1 -1
  47. data/sig/selector.rbs +20 -0
  48. data/sig/session.rbs +2 -2
  49. data/sig/transcoder.rbs +4 -2
  50. data/sig/transcoder/form.rbs +1 -1
  51. metadata +11 -4
  52. data/lib/httpx/resolver/options.rb +0 -25
  53. data/sig/test.rbs +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e3727a5374ad3c2d6d7c5dbfd61040e3aa3ea6c74aa6ad12d662a5bd0b9a7af
4
- data.tar.gz: 7b1a2aa15418b03fc276b81cfaf0565bbce59dd531c2bfc10f19ddc60506213b
3
+ metadata.gz: ed5ebccf22cf5c4719a81db15ee29a7d05e08fa43bd83b14c3b2682c083615be
4
+ data.tar.gz: 2befada7d0c17d0c20d0afce44fc0a9d3192ffb2bb311322ee171107d285d050
5
5
  SHA512:
6
- metadata.gz: cda3ab9929396bdbad4ff38472dea4d7af34946e34cf995228ad5d52d05e37664b53f66e0a0ac8dd10193e55de52b4f7575f203bea8202b5474f0c3cca450d24
7
- data.tar.gz: 025a985fc7c05abbb1bd66d23356d5165f7536d6565464e0729c618da624fc9f428e1046bba7c09dc5d7c23ada0587f73fb23e42128c96847857bcc1ccd6d408
6
+ metadata.gz: 9abffdd09d11f8c22c51f38064525d6d5250fe1e72a2e369f79c98e7d02ee6fe7a7442ac76758bf60cbc87eabe51a3faa30a675f4a30cb83a01e09aa98334d0a
7
+ data.tar.gz: 9c0706ac930dc0e082845795784a5007d0cd9bb25f4a81435be6d8e081ba29528a0df05fc03713dc07615329944ca586e4df8386766c6b5ff440c96d90dc472e
@@ -0,0 +1,39 @@
1
+ # 0.10.1
2
+
3
+ ## Improvements
4
+
5
+ ### URL-encoded nested params
6
+
7
+ url encoder now supports nested params, which is a standard of rack-based frameworks:
8
+
9
+ ```ruby
10
+ HTTPX.post("https://httpbin.org/post", form: { a: { b: 1 }, c: [2, 3] })
11
+ # a[b]=1&c[]=2&c[]=3
12
+ ```
13
+
14
+ This encoding scheme is now the standard for URL-encoded request bodies, query params, and `:multipart` plugin requests.
15
+
16
+ ### Socks4 IPv6 addresses
17
+
18
+ HTTPX supports IPv6 Socks4 proxies now. This support is restricted to rubies where `IPAddr#hton` is implemented though, so you are encouraged to upgrade.
19
+
20
+ ## More verbose HTTP Errors
21
+
22
+ `HTTPX::Response#raise_for_status` was raising exceptions for client/server HTTP errors codes (4xx/5xx). However, only the status code was part of the message.
23
+
24
+ From now on, both headers and the responnse payload will also appear, so expected more verbosity, but also more meaningful information.
25
+
26
+ ## Bugfixes
27
+
28
+ * HTTP/2 and HTTP/1.1 exhausted connections now get properly migrated into a new connection;
29
+ * HTTP/2 421 responses will now correctly migrate the connection and pendign requests to HTTP/1.1 (a hanging loop was being caused);
30
+ * HTTP/2 connection failed with a GOAWAY settings timeout will now return error responses (instead of hanging indefinitely);
31
+ * Non-IP proxy name-resolving errors will now move on to the next available proxy in the list (instead of hanging indefinitely);
32
+ * Non-IP DNS resolve errors for `native` and `https` variannts will now return the appropriate error response (instead of hanging indefinitely);
33
+ *
34
+
35
+
36
+ ## Chore
37
+
38
+ * `HTTPX.plugins` is now officially deprecated (use `HTTPX.plugin` instead);
39
+
@@ -42,12 +42,15 @@ module HTTPX
42
42
  end
43
43
 
44
44
  # deprecated
45
+ # :nocov:
45
46
  def plugins(*args, **opts)
47
+ warn ":#{__method__} is deprecated, use :plugin instead"
46
48
  klass = is_a?(Session) ? self.class : Session
47
49
  klass = Class.new(klass)
48
50
  klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
49
51
  klass.plugins(*args, **opts).new
50
52
  end
53
+ # :nocov:
51
54
 
52
55
  def with(options, &blk)
53
56
  branch(default_options.merge(options), &blk)
@@ -66,12 +69,10 @@ module HTTPX
66
69
  end
67
70
 
68
71
  def method_missing(meth, *args, **options)
69
- if meth =~ /\Awith_(.+)/
70
- option = Regexp.last_match(1).to_sym
71
- with(option => (args.first || options))
72
- else
73
- super
74
- end
72
+ return super unless meth =~ /\Awith_(.+)/
73
+
74
+ option = Regexp.last_match(1).to_sym
75
+ with(option => (args.first || options))
75
76
  end
76
77
 
77
78
  def respond_to_missing?(meth, *args)
@@ -120,8 +120,8 @@ module HTTPX
120
120
  end
121
121
  end
122
122
 
123
- def create_idle
124
- self.class.new(@type, @origin, @options)
123
+ def create_idle(options = {})
124
+ self.class.new(@type, @origin, @options.merge(options))
125
125
  end
126
126
 
127
127
  def merge(connection)
@@ -131,17 +131,6 @@ module HTTPX
131
131
  end
132
132
  end
133
133
 
134
- def unmerge(connection)
135
- @origins -= connection.instance_variable_get(:@origins)
136
- purge_pending do |request|
137
- request.uri.origin == connection.origin && begin
138
- request.transition(:idle)
139
- connection.send(request)
140
- true
141
- end
142
- end
143
- end
144
-
145
134
  def purge_pending(&block)
146
135
  pendings = []
147
136
  if @parser
@@ -399,7 +388,7 @@ module HTTPX
399
388
  parser.on(:error) do |request, ex|
400
389
  case ex
401
390
  when MisdirectedRequestError
402
- emit(:uncoalesce, request.uri)
391
+ emit(:misdirected, request)
403
392
  else
404
393
  response = ErrorResponse.new(request, ex, @options)
405
394
  request.emit(:response, response)
@@ -435,7 +424,7 @@ module HTTPX
435
424
  remove_instance_variable(:@total_timeout)
436
425
  end
437
426
 
438
- @io.close
427
+ @io.close if @io
439
428
  @read_buffer.clear
440
429
  if @keep_alive_timer
441
430
  @keep_alive_timer.cancel
@@ -21,6 +21,7 @@ module HTTPX
21
21
  @version = [1, 1]
22
22
  @pending = []
23
23
  @requests = []
24
+ @handshake_completed = false
24
25
  end
25
26
 
26
27
  def interests
@@ -155,6 +156,7 @@ module HTTPX
155
156
  @parser.reset!
156
157
  @max_requests -= 1
157
158
  manage_connection(response)
159
+
158
160
  send(@pending.shift) unless @pending.empty?
159
161
  end
160
162
 
@@ -182,17 +184,28 @@ module HTTPX
182
184
  connection = response.headers["connection"]
183
185
  case connection
184
186
  when /keep-alive/i
187
+ if @handshake_completed
188
+ if @max_requests.zero?
189
+ @pending.concat(@requests)
190
+ @requests.clear
191
+ emit(:exhausted)
192
+ end
193
+ return
194
+ end
195
+
185
196
  keep_alive = response.headers["keep-alive"]
186
197
  return unless keep_alive
187
198
 
188
199
  parameters = Hash[keep_alive.split(/ *, */).map do |pair|
189
200
  pair.split(/ *= */)
190
201
  end]
191
- @max_requests = parameters["max"].to_i if parameters.key?("max")
202
+ @max_requests = parameters["max"].to_i - 1 if parameters.key?("max")
203
+
192
204
  if parameters.key?("timeout")
193
205
  keep_alive_timeout = parameters["timeout"].to_i
194
206
  emit(:timeout, keep_alive_timeout)
195
207
  end
208
+ @handshake_completed = true
196
209
  when /close/i
197
210
  disable
198
211
  when nil
@@ -51,12 +51,8 @@ module HTTPX
51
51
  :rw
52
52
  end
53
53
 
54
- def reset
55
- init_connection
56
- end
57
-
58
- def close(*args)
59
- @connection.goaway(*args) unless @connection.state == :closed
54
+ def close
55
+ @connection.goaway unless @connection.state == :closed
60
56
  emit(:close)
61
57
  end
62
58
 
@@ -163,6 +159,9 @@ module HTTPX
163
159
  @connection.send_connection_preface
164
160
  end
165
161
 
162
+ alias_method :reset, :init_connection
163
+ public :reset
164
+
166
165
  def handle_stream(stream, request)
167
166
  stream.on(:close, &method(:on_stream_close).curry[stream, request])
168
167
  stream.on(:half_close) do
@@ -270,16 +269,16 @@ module HTTPX
270
269
  end
271
270
 
272
271
  def on_close(_last_frame, error, _payload)
272
+ is_connection_closed = @connection.state == :closed
273
273
  if error && error != :no_error
274
+ @buffer.clear if is_connection_closed
274
275
  ex = Error.new(0, error)
275
276
  ex.set_backtrace(caller)
276
- @streams.each_key do |request|
277
- emit(:error, request, ex)
278
- end
277
+ handle_error(ex)
279
278
  end
280
- return unless @connection.state == :closed && @streams.size.zero?
279
+ return unless is_connection_closed && @streams.size.zero?
281
280
 
282
- emit(:close)
281
+ emit(:close, is_connection_closed)
283
282
  end
284
283
 
285
284
  def on_frame_sent(frame)
@@ -317,7 +316,7 @@ module HTTPX
317
316
  end
318
317
 
319
318
  def on_pong(ping)
320
- if !@pings.delete(ping)
319
+ if !@pings.delete(ping.to_s)
321
320
  close(:protocol_error, "ping payload did not match")
322
321
  else
323
322
  emit(:pong)
@@ -41,7 +41,7 @@ module HTTPX
41
41
 
42
42
  def initialize(response)
43
43
  @response = response
44
- super("HTTP Error: #{@response.status}")
44
+ super("HTTP Error: #{@response.status} #{@response.headers}\n#{@response.body}")
45
45
  end
46
46
 
47
47
  def status
@@ -23,12 +23,26 @@ module HTTPX
23
23
  def_delegator :@raw, :read
24
24
 
25
25
  def initialize(form)
26
- @raw = HTTP::FormData.create(form)
26
+ @raw = if multipart?(form)
27
+ HTTP::FormData::Multipart.new(Hash[*form.map { |k, v| Transcoder.enum_for(:normalize_keys, k, v).to_a }])
28
+ else
29
+ HTTP::FormData::Urlencoded.new(form, :encoder => Transcoder::Form.method(:encode))
30
+ end
27
31
  end
28
32
 
29
33
  def bytesize
30
34
  @raw.content_length
31
35
  end
36
+
37
+ private
38
+
39
+ def multipart?(data)
40
+ data.any? do |_, v|
41
+ v.is_a?(HTTP::FormData::Part) ||
42
+ (v.respond_to?(:to_ary) && v.to_ary.any? { |e| e.is_a?(HTTP::FormData::Part) }) ||
43
+ (v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| e.is_a?(HTTP::FormData::Part) })
44
+ end
45
+ end
32
46
  end
33
47
 
34
48
  def encode(form)
@@ -121,8 +121,7 @@ module HTTPX
121
121
  def fetch_response(request, connections, options)
122
122
  response = super
123
123
  if response.is_a?(ErrorResponse) &&
124
- # either it was a timeout error connecting, or it was a proxy error
125
- PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
124
+ __proxy_error?(response) && !@_proxy_uris.empty?
126
125
  @_proxy_uris.shift
127
126
  log { "failed connecting to proxy, trying next..." }
128
127
  request.transition(:idle)
@@ -139,6 +138,21 @@ module HTTPX
139
138
 
140
139
  super
141
140
  end
141
+
142
+ def __proxy_error?(response)
143
+ error = response.error
144
+ case error
145
+ when ResolveError
146
+ # failed resolving proxy domain
147
+ proxy_uri = error.connection.options.proxy.uri
148
+ proxy_uri.to_s == @_proxy_uris.first
149
+ when *PROXY_ERRORS
150
+ # timeout errors connecting to proxy
151
+ true
152
+ else
153
+ false
154
+ end
155
+ end
142
156
  end
143
157
 
144
158
  module ConnectionMethods
@@ -10,7 +10,7 @@ module HTTPX
10
10
  module Socks4
11
11
  VERSION = 4
12
12
  CONNECT = 1
13
- GRANTED = 90
13
+ GRANTED = 0x5A
14
14
  PROTOCOLS = %w[socks4 socks4a].freeze
15
15
 
16
16
  Error = Socks4Error
@@ -91,27 +91,25 @@ module HTTPX
91
91
  end
92
92
 
93
93
  module Packet
94
- using(RegexpExtensions) unless Regexp.method_defined?(:match?)
95
-
96
94
  module_function
97
95
 
98
96
  def connect(parameters, uri)
99
97
  packet = [VERSION, CONNECT, uri.port].pack("CCn")
100
- begin
101
- ip = IPAddr.new(uri.host)
102
- raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
103
-
104
- packet << [ip.to_i].pack("N")
105
- rescue IPAddr::InvalidAddressError
106
- if /^socks4a?$/.match?(parameters.uri.scheme)
107
- # resolv defaults to IPv4, and socks4 doesn't support IPv6 otherwise
108
- ip = IPAddr.new(Resolv.getaddress(uri.host))
109
- packet << [ip.to_i].pack("N")
110
- else
111
- packet << "\x0\x0\x0\x1" << "\x7\x0" << uri.host
98
+
99
+ case parameters.uri.scheme
100
+ when "socks4"
101
+ socks_host = uri.host
102
+ begin
103
+ ip = IPAddr.new(socks_host)
104
+ packet << ip.hton
105
+ rescue IPAddr::InvalidAddressError
106
+ socks_host = Resolv.getaddress(socks_host)
107
+ retry
112
108
  end
109
+ packet << [parameters.username].pack("Z*")
110
+ when "socks4a"
111
+ packet << "\x0\x0\x0\x1" << [parameters.username].pack("Z*") << uri.host << "\x0"
113
112
  end
114
- packet << [parameters.username].pack("Z*")
115
113
  packet
116
114
  end
117
115
  end
@@ -108,10 +108,10 @@ module HTTPX
108
108
  end
109
109
  end
110
110
 
111
- def on_resolver_error(ch, error)
112
- ch.emit(:error, error)
111
+ def on_resolver_error(connection, error)
112
+ connection.emit(:error, error)
113
113
  # must remove connection by hand, hasn't been started yet
114
- unregister_connection(ch)
114
+ unregister_connection(connection)
115
115
  end
116
116
 
117
117
  def on_resolver_close(resolver)
@@ -144,12 +144,12 @@ module HTTPX
144
144
  @connected_connections -= 1
145
145
  end
146
146
 
147
- def coalesce_connections(ch1, ch2)
148
- if ch1.coalescable?(ch2)
149
- ch1.merge(ch2)
150
- @connections.delete(ch2)
147
+ def coalesce_connections(conn1, conn2)
148
+ if conn1.coalescable?(conn2)
149
+ conn1.merge(conn2)
150
+ @connections.delete(conn2)
151
151
  else
152
- register_connection(ch2)
152
+ register_connection(conn2)
153
153
  end
154
154
  end
155
155
 
@@ -168,12 +168,6 @@ module HTTPX
168
168
  resolver.on(:error, &method(:on_resolver_error))
169
169
  resolver.on(:close) { on_resolver_close(resolver) }
170
170
  resolver
171
- rescue ArgumentError
172
- # this block is here because of an error which happens on CI from time to time
173
- warn "tried resolver: #{resolver_type}"
174
- warn "initialize: #{resolver_type.instance_method(:initialize).source_location}"
175
- warn "new: #{resolver_type.method(:new).source_location}"
176
- raise
177
171
  end
178
172
  end
179
173
  end
@@ -106,7 +106,7 @@ module HTTPX
106
106
 
107
107
  query = []
108
108
  if (q = @options.params)
109
- query << URI.encode_www_form(q)
109
+ query << Transcoder.registry("form").encode(q)
110
110
  end
111
111
  query << @uri.query if @uri.query
112
112
  @query = query.join("&")
@@ -101,5 +101,3 @@ module HTTPX
101
101
  end
102
102
  end
103
103
  end
104
-
105
- require "httpx/resolver/options"
@@ -21,6 +21,7 @@ module HTTPX
21
21
  DEFAULTS = {
22
22
  uri: NAMESERVER,
23
23
  use_get: false,
24
+ record_types: RECORD_TYPES.keys,
24
25
  }.freeze
25
26
 
26
27
  def_delegator :@connections, :empty?
@@ -29,12 +30,12 @@ module HTTPX
29
30
 
30
31
  def initialize(options)
31
32
  @options = Options.new(options)
32
- @resolver_options = Resolver::Options.new(DEFAULTS.merge(@options.resolver_options || {}))
33
- @_record_types = Hash.new { |types, host| types[host] = RECORD_TYPES.keys.dup }
33
+ @resolver_options = DEFAULTS.merge(@options.resolver_options)
34
+ @_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
34
35
  @queries = {}
35
36
  @requests = {}
36
37
  @connections = []
37
- @uri = URI(@resolver_options.uri)
38
+ @uri = URI(@resolver_options[:uri])
38
39
  @uri_addresses = nil
39
40
  end
40
41
 
@@ -44,12 +45,12 @@ module HTTPX
44
45
  @uri_addresses ||= Resolv.getaddresses(@uri.host)
45
46
 
46
47
  if @uri_addresses.empty?
47
- ex = ResolveError.new("Can't resolve #{connection.origin.host}")
48
+ ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
48
49
  ex.set_backtrace(caller)
49
- emit(:error, connection, ex)
50
- else
51
- early_resolve(connection) || resolve(connection)
50
+ throw(:resolve_error, ex)
52
51
  end
52
+
53
+ early_resolve(connection) || resolve(connection)
53
54
  end
54
55
 
55
56
  def timeout
@@ -70,12 +71,6 @@ module HTTPX
70
71
 
71
72
  private
72
73
 
73
- def connect
74
- return if @queries.empty?
75
-
76
- resolver_connection.__send__(__method__)
77
- end
78
-
79
74
  def pool
80
75
  Thread.current[:httpx_connection_pool] ||= Pool.new
81
76
  end
@@ -100,10 +95,12 @@ module HTTPX
100
95
  hostname = connection.origin.host
101
96
  log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
102
97
  end
103
- type = @_record_types[hostname].first
98
+ type = @_record_types[hostname].first || "A"
104
99
  log { "resolver: query #{type} for #{hostname}" }
105
100
  begin
106
101
  request = build_request(hostname, type)
102
+ request.on(:response, &method(:on_response).curry[request])
103
+ request.on(:promise, &method(:on_promise))
107
104
  @requests[request] = connection
108
105
  resolver_connection.send(request)
109
106
  @queries[hostname] = connection
@@ -118,9 +115,7 @@ module HTTPX
118
115
  rescue StandardError => e
119
116
  connection = @requests[request]
120
117
  hostname = @queries.key(connection)
121
- error = ResolveError.new("Can't resolve #{hostname}: #{e.message}")
122
- error.set_backtrace(e.backtrace)
123
- emit(:error, connection, error)
118
+ emit_resolve_error(connection, hostname, e)
124
119
  else
125
120
  parse(response)
126
121
  ensure
@@ -143,7 +138,7 @@ module HTTPX
143
138
  return
144
139
  end
145
140
  end
146
- if answers.empty?
141
+ if answers.nil? || answers.empty?
147
142
  host, connection = @queries.first
148
143
  @_record_types[host].shift
149
144
  if @_record_types[host].empty?
@@ -177,7 +172,7 @@ module HTTPX
177
172
  next unless connection # probably a retried query for which there's an answer
178
173
 
179
174
  @connections.delete(connection)
180
- Resolver.cached_lookup_set(hostname, addresses) if @resolver_options.cache
175
+ Resolver.cached_lookup_set(hostname, addresses) if @resolver_options[:cache]
181
176
  emit_addresses(connection, addresses.map { |addr| addr["data"] })
182
177
  end
183
178
  end
@@ -191,7 +186,7 @@ module HTTPX
191
186
  rklass = @options.request_class
192
187
  payload = Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
193
188
 
194
- if @resolver_options.use_get
189
+ if @resolver_options[:use_get]
195
190
  params = URI.decode_www_form(uri.query.to_s)
196
191
  params << ["type", type]
197
192
  params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
@@ -202,8 +197,6 @@ module HTTPX
202
197
  request.headers["content-type"] = "application/dns-message"
203
198
  end
204
199
  request.headers["accept"] = "application/dns-message"
205
- request.on(:response, &method(:on_response).curry[request])
206
- request.on(:promise, &method(:on_promise))
207
200
  request
208
201
  end
209
202