httpx 0.10.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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