excon 0.38.0 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of excon might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- N2Q1NWY3Zjk1ZmMzZTc5ZTFjODAzZGMyZmUwMTljYTkyYTFjNjAxMw==
4
+ YzFlMDNlOTJjZTE5MGExNDI4OWNiNWIyZDNkZTZmNWQ4MjA0M2RkOQ==
5
5
  data.tar.gz: !binary |-
6
- NjFkNTZhNTlmMGFkN2MwNjJjNDEzZjM2MGYyYjg3OTNhNTM5NjM5Yw==
6
+ ZmZkMzdhZTA0MWNiZWU3MWU0MTMyMzU1Y2QzYWMzOGE2NzcyNjdjNg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NjE0NGE3NzdiMDA4YTg0NWIyNDMxNGMyMDBiY2I4MDkzZjRjYWU4NzE1Nzhl
10
- Yjk5N2YyYjRlYzdlMDBlM2VhYTU1MTQ4OTU5Zjc2YTc4OWY1OWMwMTNjODdm
11
- ZDkxZWVhODdiYmVkNzgzMGU2MGY1NjNmMjBhOTQ2MThmNWQzNGY=
9
+ MDc2MTgzZWEwMzIwNDA0MzM1Mjk4NTM5MGNlODU1YmYwNmUyNGRlM2EyZmY5
10
+ YWZmNTI1YzhmZjI2MjViOWE2NGVjNjY4YWRjOTY1YzI5YmZjMTM3NGZiNTY1
11
+ YjM2NjM5ZDUyY2EyMDRjMjgxMWYxMWE2YjNjZWFlZTY0YzgzZWI=
12
12
  data.tar.gz: !binary |-
13
- NWQ5OTYyMzhlNzMwZWRlMGY1ODUxMWFkZjU0MWQ1MmRiMjUyMGNjMDA1ZjIw
14
- ZjI2YTlhNzZiN2Y1ODg4MGFjMTNjNDljMmU5NzFiNmFhNzMyMjk1YTQxMzg0
15
- YjdhYWNlZmUwYjI4MTc1ZTU2NDAwNThlMjljODVjMTc1ZjIyNTg=
13
+ NDk1YTIzM2RhZjJmY2ViYzBhMGY2Y2FkYTViNzRmMzBhOTZjMjgxNDEyMjMw
14
+ MTgwMGNiYzcyYmVlY2U0NGRlZTE4OTA1ZWVjNGMyNGVhOTgxYmMxNjJlMGYz
15
+ ZTAwYjg5MjgxYzYxYTA2MmNmNTMzMzQxMzk1MDNjOTkzNTU4Zjg=
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- excon (0.38.0)
4
+ excon (0.39.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.md CHANGED
@@ -85,6 +85,9 @@ Both one-off and persistent connections support many other options. The final op
85
85
  Here are a few common examples:
86
86
 
87
87
  ```ruby
88
+ # Output debug info, similar to ENV['EXCON_DEBUG']
89
+ connection = Excon.new('http://geemus.com/', :debug => true)
90
+
88
91
  # Custom headers
89
92
  Excon.get('http://geemus.com', :headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
90
93
  connection.get(:headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
@@ -1,3 +1,26 @@
1
+ 0.39.0 08/01/2014
2
+ =================
3
+
4
+ revert to a blocking readline, for performance
5
+ simplify status lookup
6
+ consolidate proxy code
7
+ store defaults as a constant
8
+ avoid setting nil user/pass vs just no setting keys
9
+ move idempotent warnings in to middleware
10
+ simplify validations
11
+ use constants in utils
12
+ group non-chunk response paring
13
+ optimize/simplify socket local lookup
14
+ simplify to pro-actively build downcased headers instead of lazily do so
15
+ add version to options (so it will appear in debug)
16
+ add OS/Ruby version info to options/version for debugging
17
+ more consistent output styling for errors
18
+ remove TE stuff to simplify
19
+ shorten timeout/sleep in streaming tests
20
+ remove transfer-encoding altogether if it only includes chunked
21
+ only rescue http status errors in relevant tests
22
+ use case-insensitive headers in stubs also
23
+
1
24
  0.38.0 07/09/2014
2
25
  =================
3
26
 
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'excon'
16
- s.version = '0.38.0'
17
- s.date = '2014-07-09'
16
+ s.version = '0.39.0'
17
+ s.date = '2014-08-01'
18
18
  s.rubyforge_project = 'excon'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -110,6 +110,7 @@ Gem::Specification.new do |s|
110
110
  lib/excon/middlewares/mock.rb
111
111
  lib/excon/middlewares/redirect_follower.rb
112
112
  lib/excon/middlewares/response_parser.rb
113
+ lib/excon/pretty_printer.rb
113
114
  lib/excon/response.rb
114
115
  lib/excon/socket.rb
115
116
  lib/excon/ssl_socket.rb
@@ -11,75 +11,44 @@ require 'uri'
11
11
  require 'zlib'
12
12
  require 'stringio'
13
13
 
14
- # Define defaults first so they will be available to other files
15
- module Excon
16
- class << self
17
-
18
- # @return [Hash] defaults for Excon connections
19
- def defaults
20
- @defaults ||= {
21
- :chunk_size => CHUNK_SIZE || DEFAULT_CHUNK_SIZE,
22
- :ciphers => 'HIGH:!SSLv2:!aNULL:!eNULL:!3DES',
23
- :connect_timeout => 60,
24
- :debug_request => false,
25
- :debug_response => false,
26
- :headers => {
27
- 'User-Agent' => USER_AGENT
28
- },
29
- :idempotent => false,
30
- :instrumentor_name => 'excon',
31
- :middlewares => [
32
- Excon::Middleware::ResponseParser,
33
- Excon::Middleware::Expects,
34
- Excon::Middleware::Idempotent,
35
- Excon::Middleware::Instrumentor,
36
- Excon::Middleware::Mock
37
- ],
38
- :mock => false,
39
- :nonblock => true,
40
- :omit_default_port => false,
41
- :persistent => false,
42
- :read_timeout => 60,
43
- :retry_limit => DEFAULT_RETRY_LIMIT,
44
- :ssl_verify_peer => true,
45
- :tcp_nodelay => false,
46
- :uri_parser => URI,
47
- :write_timeout => 60
48
- }
49
- end
50
-
51
- # Change defaults for Excon connections
52
- # @return [Hash] defaults for Excon connections
53
- def defaults=(new_defaults)
54
- @defaults = new_defaults
55
- end
56
-
57
- end
58
- end
59
-
60
- require 'excon/utils'
61
- require 'excon/constants'
62
- require 'excon/connection'
63
- require 'excon/errors'
64
14
  require 'excon/middlewares/base'
65
- require 'excon/middlewares/decompress'
66
- require 'excon/middlewares/escape_path'
67
15
  require 'excon/middlewares/expects'
68
16
  require 'excon/middlewares/idempotent'
69
17
  require 'excon/middlewares/instrumentor'
70
18
  require 'excon/middlewares/mock'
71
- require 'excon/middlewares/redirect_follower'
72
19
  require 'excon/middlewares/response_parser'
73
- require 'excon/response'
20
+
21
+ require 'excon/constants'
22
+ require 'excon/utils'
23
+
24
+ require 'excon/connection'
25
+ require 'excon/errors'
74
26
  require 'excon/headers'
27
+ require 'excon/response'
28
+ require 'excon/middlewares/decompress'
29
+ require 'excon/middlewares/escape_path'
30
+ require 'excon/middlewares/redirect_follower'
31
+ require 'excon/pretty_printer'
75
32
  require 'excon/socket'
76
33
  require 'excon/ssl_socket'
77
- require 'excon/unix_socket'
78
34
  require 'excon/standard_instrumentor'
35
+ require 'excon/unix_socket'
79
36
 
37
+ # Define defaults first so they will be available to other files
80
38
  module Excon
81
39
  class << self
82
40
 
41
+ # @return [Hash] defaults for Excon connections
42
+ def defaults
43
+ @defaults ||= DEFAULTS
44
+ end
45
+
46
+ # Change defaults for Excon connections
47
+ # @return [Hash] defaults for Excon connections
48
+ def defaults=(new_defaults)
49
+ @defaults = new_defaults
50
+ end
51
+
83
52
  def display_warning(warning)
84
53
  # Respect Ruby's $VERBOSE setting, unless EXCON_DEBUG is set
85
54
  if !$VERBOSE.nil? || ENV['EXCON_DEBUG']
@@ -135,16 +104,22 @@ module Excon
135
104
  def new(url, params = {})
136
105
  uri_parser = params[:uri_parser] || Excon.defaults[:uri_parser]
137
106
  uri = uri_parser.parse(url)
138
- raise ArgumentError.new("Invalid URI: #{uri}") unless uri.scheme
107
+ unless uri.scheme
108
+ raise ArgumentError.new("Invalid URI: #{uri}")
109
+ end
139
110
  params = {
140
111
  :host => uri.host,
141
112
  :path => uri.path,
142
113
  :port => uri.port,
143
114
  :query => uri.query,
144
- :scheme => uri.scheme,
145
- :user => (Utils.unescape_uri(uri.user) if uri.user),
146
- :password => (Utils.unescape_uri(uri.password) if uri.password)
115
+ :scheme => uri.scheme
147
116
  }.merge!(params)
117
+ if uri.password
118
+ params[:password] = Utils.unescape_uri(uri.password)
119
+ end
120
+ if uri.user
121
+ params[:user] = Utils.unescape_uri(uri.user)
122
+ end
148
123
  Excon::Connection.new(params)
149
124
  end
150
125
 
@@ -170,6 +145,13 @@ module Excon
170
145
  request_params[:headers]['Authorization'] ||= 'Basic ' << ['' << user << ':' << pass].pack('m').delete(Excon::CR_NL)
171
146
  end
172
147
  end
148
+ if request_params.has_key?(:headers)
149
+ headers = Excon::Headers.new
150
+ request_params[:headers].each do |key, value|
151
+ headers[key] = value
152
+ end
153
+ request_params[:headers] = headers
154
+ end
173
155
  if block_given?
174
156
  if response_params
175
157
  raise(ArgumentError.new("stub requires either response_params OR a block"))
@@ -57,40 +57,19 @@ module Excon
57
57
  params = validate_params(:connection, params)
58
58
  @data.merge!(params)
59
59
 
60
- unless @data[:scheme] == UNIX
61
- no_proxy_env = ENV["no_proxy"] || ENV["NO_PROXY"] || ""
62
- no_proxy_list = no_proxy_env.scan(/\*?\.?([^\s,:]+)(?::(\d+))?/i).map { |s| [s[0], s[1]] }
63
- unless no_proxy_list.index { |h| /(^|\.)#{h[0]}$/.match(@data[:host]) && (h[1].nil? || h[1].to_i == @data[:port]) }
64
- if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
65
- @data[:proxy] = setup_proxy(ENV['https_proxy'] || ENV['HTTPS_PROXY'])
66
- elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
67
- @data[:proxy] = setup_proxy(ENV['http_proxy'] || ENV['HTTP_PROXY'])
68
- elsif @data.has_key?(:proxy)
69
- @data[:proxy] = setup_proxy(@data[:proxy])
70
- end
71
- end
72
- end
60
+ setup_proxy
73
61
 
74
- if @data[:proxy] && @data[:scheme] == 'http'
75
- @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
76
- # https credentials happen in handshake
77
- if @data[:proxy][:user] || @data[:proxy][:password]
78
- user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
79
- auth = ['' << user.to_s << ':' << pass.to_s].pack('m').delete(Excon::CR_NL)
80
- @data[:headers]['Proxy-Authorization'] = 'Basic ' << auth
81
- end
62
+ if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
63
+ @data[:instrumentor] = Excon::StandardInstrumentor
82
64
  end
83
65
 
84
- if ENV.has_key?('EXCON_DEBUG') || ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
66
+ if @data[:debug] != false && @data[:debug] || ENV.has_key?('EXCON_DEBUG')
67
+ @data[:debug_request] = @data[:debug_response] = true
85
68
  @data[:instrumentor] = Excon::StandardInstrumentor
86
-
87
- if ENV.has_key?('EXCON_DEBUG')
88
- @data[:debug_request] = @data[:debug_response] = true
89
- end
90
69
  end
91
70
 
92
71
  # Use Basic Auth if url contains a login
93
- if @data[:user] || @data[:password]
72
+ if @data.has_key?(:user) || @data.has_key?(:password)
94
73
  user, pass = Utils.unescape_form(@data[:user].to_s), Utils.unescape_form(@data[:password].to_s)
95
74
  @data[:headers]['Authorization'] ||= 'Basic ' << ['' << user.to_s << ':' << pass.to_s].pack('m').delete(Excon::CR_NL)
96
75
  end
@@ -151,13 +130,6 @@ module Excon
151
130
  end
152
131
  end
153
132
 
154
- if datum[:response_block]
155
- datum[:headers]['TE'] = 'trailers'
156
- else
157
- datum[:headers]['TE'] = 'trailers, deflate, gzip'
158
- end
159
- datum[:headers]['Connection'] = datum[:persistent] ? 'TE' : 'TE, close'
160
-
161
133
  # add headers to request
162
134
  datum[:headers].each do |key, values|
163
135
  [values].flatten.each do |value|
@@ -248,17 +220,6 @@ module Excon
248
220
  datum[:response_block] = Proc.new
249
221
  end
250
222
 
251
- if datum[:idempotent]
252
- if datum[:request_block]
253
- Excon.display_warning('Excon requests with a :request_block can not be :idempotent.')
254
- datum[:idempotent] = false
255
- end
256
- if datum[:pipeline]
257
- Excon.display_warning("Excon requests can not be :idempotent when pipelining.")
258
- datum[:idempotent] = false
259
- end
260
- end
261
-
262
223
  datum[:connection] = self
263
224
 
264
225
  datum[:stack] = datum[:middlewares].map do |middleware|
@@ -273,7 +234,7 @@ module Excon
273
234
 
274
235
  if datum[:persistent]
275
236
  if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
276
- if split_header_value(datum[:response][:headers][key]).any? {|t| t.casecmp('close') == 0 }
237
+ if datum[:response][:headers][key].casecmp('close') == 0
277
238
  reset
278
239
  end
279
240
  end
@@ -309,7 +270,7 @@ module Excon
309
270
 
310
271
  if @data[:persistent]
311
272
  if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
312
- if split_header_value(responses.last[:headers][key]).any? {|t| t.casecmp('close') == 0 }
273
+ if responses.last[:headers][key].casecmp('close') == 0
313
274
  reset
314
275
  end
315
276
  end
@@ -384,9 +345,9 @@ module Excon
384
345
  def validate_params(validation, params)
385
346
  valid_keys = case validation
386
347
  when :connection
387
- valid_connection_keys(params)
348
+ Excon::VALID_CONNECTION_KEYS
388
349
  when :request
389
- valid_request_keys(params)
350
+ Excon::VALID_REQUEST_KEYS
390
351
  end
391
352
  invalid_keys = params.keys - valid_keys
392
353
  unless invalid_keys.empty?
@@ -423,22 +384,48 @@ module Excon
423
384
  Thread.current[:_excon_sockets] ||= {}
424
385
  end
425
386
 
426
- def setup_proxy(proxy)
427
- case proxy
428
- when String
429
- uri = URI.parse(proxy)
430
- unless uri.host and uri.port and uri.scheme
431
- raise Excon::Errors::ProxyParseError, "Proxy is invalid"
387
+ def setup_proxy
388
+ unless @data[:scheme] == UNIX
389
+ if no_proxy_env = ENV["no_proxy"] || ENV["NO_PROXY"]
390
+ no_proxy_list = no_proxy_env.scan(/\*?\.?([^\s,:]+)(?::(\d+))?/i).map { |s| [s[0], s[1]] }
391
+ end
392
+
393
+ unless no_proxy_env && no_proxy_list.index { |h| /(^|\.)#{h[0]}$/.match(@data[:host]) && (h[1].nil? || h[1].to_i == @data[:port]) }
394
+ if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
395
+ @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
396
+ elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
397
+ @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
398
+ end
399
+ end
400
+
401
+ case @data[:proxy]
402
+ when String
403
+ uri = URI.parse(@data[:proxy])
404
+ unless uri.host && uri.port && uri.scheme
405
+ raise Excon::Errors::ProxyParseError, "Proxy is invalid"
406
+ end
407
+ @data[:proxy] = {
408
+ :host => uri.host,
409
+ :port => uri.port,
410
+ :scheme => uri.scheme,
411
+ }
412
+ if uri.password
413
+ @data[:proxy][:password] = uri.password
414
+ end
415
+ if uri.user
416
+ @data[:proxy][:user] = uri.user
417
+ end
418
+ end
419
+
420
+ if @data.has_key?(:proxy) && @data[:scheme] == 'http'
421
+ @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
422
+ # https credentials happen in handshake
423
+ if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
424
+ user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
425
+ auth = ['' << user << ':' << pass].pack('m').delete(Excon::CR_NL)
426
+ @data[:headers]['Proxy-Authorization'] = 'Basic ' << auth
427
+ end
432
428
  end
433
- {
434
- :host => uri.host,
435
- :password => uri.password,
436
- :port => uri.port,
437
- :scheme => uri.scheme,
438
- :user => uri.user
439
- }
440
- else
441
- proxy
442
429
  end
443
430
  end
444
431
  end
@@ -1,6 +1,6 @@
1
1
  module Excon
2
2
 
3
- VERSION = '0.38.0'
3
+ VERSION = '0.39.0'
4
4
 
5
5
  CR_NL = "\r\n"
6
6
 
@@ -31,6 +31,8 @@ module Excon
31
31
 
32
32
  USER_AGENT = 'excon/' << VERSION
33
33
 
34
+ VERSIONS = USER_AGENT + ' (' << RUBY_PLATFORM << ') ruby/' << RUBY_VERSION
35
+
34
36
  VALID_REQUEST_KEYS = [
35
37
  :body,
36
38
  :captures,
@@ -54,6 +56,7 @@ module Excon
54
56
  :response_block,
55
57
  :retries_remaining, # used internally
56
58
  :retry_limit,
59
+ :versions,
57
60
  :write_timeout
58
61
  ]
59
62
 
@@ -96,5 +99,36 @@ module Excon
96
99
  module WaitWritable; end
97
100
  end
98
101
  end
102
+ # these come last as they rely on the above
103
+ DEFAULTS = {
104
+ :chunk_size => CHUNK_SIZE || DEFAULT_CHUNK_SIZE,
105
+ :ciphers => 'HIGH:!SSLv2:!aNULL:!eNULL:!3DES',
106
+ :connect_timeout => 60,
107
+ :debug_request => false,
108
+ :debug_response => false,
109
+ :headers => {
110
+ 'User-Agent' => USER_AGENT
111
+ },
112
+ :idempotent => false,
113
+ :instrumentor_name => 'excon',
114
+ :middlewares => [
115
+ Excon::Middleware::ResponseParser,
116
+ Excon::Middleware::Expects,
117
+ Excon::Middleware::Idempotent,
118
+ Excon::Middleware::Instrumentor,
119
+ Excon::Middleware::Mock
120
+ ],
121
+ :mock => false,
122
+ :nonblock => true,
123
+ :omit_default_port => false,
124
+ :persistent => false,
125
+ :read_timeout => 60,
126
+ :retry_limit => DEFAULT_RETRY_LIMIT,
127
+ :ssl_verify_peer => true,
128
+ :tcp_nodelay => false,
129
+ :uri_parser => URI,
130
+ :versions => VERSIONS,
131
+ :write_timeout => 60
132
+ }
99
133
 
100
134
  end
@@ -36,7 +36,7 @@ module Excon
36
36
  @response = response
37
37
  end
38
38
  end
39
-
39
+
40
40
  # HTTP Error classes
41
41
  class Informational < HTTPStatusError; end
42
42
  class Success < HTTPStatusError; end
@@ -130,24 +130,23 @@ module Excon
130
130
  504 => [Excon::Errors::GatewayTimeout, 'Gateway Timeout']
131
131
  }
132
132
 
133
- error, message = @errors[response[:status]] || [Excon::Errors::HTTPStatusError, 'Unknown']
133
+ error_class, error_message = @errors[response[:status]] || [Excon::Errors::HTTPStatusError, 'Unknown']
134
134
 
135
- message = "Expected(#{request[:expects].inspect}) <=> Actual(#{response[:status]} #{message})"
135
+ message = StringIO.new
136
+ message.puts("Expected(#{request[:expects].inspect}) <=> Actual(#{response[:status]} #{error_message})")
136
137
 
137
138
  if request[:debug_request]
138
- # scrub authorization
139
- req = request.dup
140
- req.reject! {|key, value| [:connection, :stack].include?(key)}
141
- if req.has_key?(:headers) && req[:headers].has_key?('Authorization')
142
- req[:headers] = req[:headers].dup
143
- req[:headers]['Authorization'] = REDACTED
144
- end
145
- message << "\n request => #{req.inspect}"
139
+ message.puts('excon.error.request')
140
+ Excon::PrettyPrinter.pp(message, request)
146
141
  end
147
142
 
148
- message << "\n response => #{response.inspect}" if request[:debug_response]
143
+ if request[:debug_response]
144
+ message.puts('excon.error.response')
145
+ Excon::PrettyPrinter.pp(message, response.data)
146
+ end
149
147
 
150
- error.new(message, request, response)
148
+ message.rewind
149
+ error_class.new(message.read, request, response)
151
150
  end
152
151
 
153
152
  end
@@ -18,99 +18,52 @@ module Excon
18
18
  alias_method :raw_store, :store
19
19
  alias_method :raw_values_at, :values_at
20
20
 
21
+ def initialize
22
+ @downcased = {}
23
+ end
24
+
21
25
  def [](key)
22
- if should_delegate?(key)
23
- @downcased[key.downcase]
24
- else
25
- raw_reader(key)
26
- end
26
+ @downcased[key.downcase]
27
27
  end
28
28
 
29
29
  alias_method :[]=, :store
30
30
  def []=(key, value)
31
31
  raw_writer(key, value)
32
- unless @downcased.nil?
33
- @downcased[key.downcase] = value
34
- end
32
+ @downcased[key.downcase] = value
35
33
  end
36
34
 
37
35
  if SENTINEL.respond_to? :assoc
38
36
  def assoc(obj)
39
- if should_delegate?(obj)
40
- @downcased.assoc(obj.downcase)
41
- else
42
- raw_assoc(obj)
43
- end
37
+ @downcased.assoc(obj.downcase)
44
38
  end
45
39
  end
46
40
 
47
41
  def delete(key, &proc)
48
- if should_delegate?(key)
49
- @downcased.delete(key.downcase, &proc)
50
- else
51
- raw_delete(key, &proc)
52
- end
42
+ raw_delete(key, &proc)
43
+ @downcased.delete(key.downcase, &proc)
53
44
  end
54
45
 
55
46
  def fetch(key, default = nil, &proc)
56
- if should_delegate?(key)
57
- if proc
58
- @downcased.fetch(key.downcase, &proc)
59
- else
60
- @downcased.fetch(key.downcase, default)
61
- end
47
+ if proc
48
+ @downcased.fetch(key.downcase, &proc)
62
49
  else
63
- if proc
64
- raw_fetch(key, &proc)
65
- else
66
- raw_fetch(key, default)
67
- end
50
+ @downcased.fetch(key.downcase, default)
68
51
  end
69
52
  end
70
53
 
71
54
  alias_method :has_key?, :key?
72
55
  alias_method :has_key?, :member?
73
56
  def has_key?(key)
74
- raw_has_key?(key) || begin
75
- index_case_insensitive
76
- @downcased.has_key?(key.downcase)
77
- end
57
+ raw_key?(key) || @downcased.has_key?(key.downcase)
78
58
  end
79
59
 
80
60
  def rehash
81
61
  raw_rehash
82
- if @downcased
83
- @downcased.rehash
84
- end
62
+ @downcased.rehash
85
63
  end
86
64
 
87
65
  def values_at(*keys)
88
- raw_values_at(*keys).zip(keys).map do |v, k|
89
- if v.nil?
90
- index_case_insensitive
91
- @downcased[k.downcase]
92
- end
93
- end
94
- end
95
-
96
- private
97
-
98
- def should_delegate?(key)
99
- if raw_has_key?(key)
100
- false
101
- else
102
- index_case_insensitive
103
- true
104
- end
105
- end
106
-
107
- def index_case_insensitive
108
- if @downcased.nil?
109
- @downcased = {}
110
- each_pair do |key, value|
111
- @downcased[key.downcase] = value
112
- end
113
- end
66
+ @downcased.values_at(*keys.map {|key| key.downcase})
114
67
  end
115
68
 
116
69
  end
@@ -2,13 +2,23 @@ module Excon
2
2
  module Middleware
3
3
  class Idempotent < Excon::Middleware::Base
4
4
  def error_call(datum)
5
+ if datum[:idempotent]
6
+ if datum.has_key?(:request_block)
7
+ Excon.display_warning('Excon requests with a :request_block can not be :idempotent.')
8
+ datum[:idempotent] = false
9
+ end
10
+ if datum.has_key?(:pipeline)
11
+ Excon.display_warning("Excon requests can not be :idempotent when pipelining.")
12
+ datum[:idempotent] = false
13
+ end
14
+ end
15
+
5
16
  if datum[:idempotent] && [Excon::Errors::Timeout, Excon::Errors::SocketError,
6
17
  Excon::Errors::HTTPStatusError].any? {|ex| datum[:error].kind_of?(ex) } && datum[:retries_remaining] > 1
7
18
  # reduces remaining retries, reset connection, and restart request_call
8
19
  datum[:retries_remaining] -= 1
9
20
  connection = datum.delete(:connection)
10
- request_keys = Utils.valid_request_keys(datum)
11
- datum.reject! {|key, _| !request_keys.include?(key) }
21
+ datum.reject! {|key, _| !Excon::VALID_REQUEST_KEYS.include?(key) }
12
22
  connection.request(datum)
13
23
  else
14
24
  @stack.error_call(datum)
@@ -4,24 +4,6 @@ module Excon
4
4
  def response_call(datum)
5
5
  unless datum.has_key?(:response)
6
6
  datum = Excon::Response.parse(datum[:connection].send(:socket), datum)
7
-
8
- # only requests without a :response_block add 'deflate, gzip' to the TE header.
9
- unless datum[:response_block]
10
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 }
11
- encodings = Utils.split_header_value(datum[:response][:headers][key])
12
- if encoding = encodings.last
13
- if encoding.casecmp('deflate') == 0
14
- # assume inflate omits header
15
- datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(datum[:response][:body])
16
- encodings.pop
17
- elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
18
- datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(datum[:response][:body])).read
19
- encodings.pop
20
- end
21
- datum[:response][:headers][key] = encodings.join(', ')
22
- end
23
- end
24
- end
25
7
  end
26
8
  @stack.response_call(datum)
27
9
  end
@@ -0,0 +1,45 @@
1
+ module Excon
2
+ class PrettyPrinter
3
+ def self.pp(io, datum, indent=0)
4
+ datum = datum.dup
5
+
6
+ # reduce duplication/noise of output
7
+ unless datum.is_a?(Excon::Headers)
8
+ datum.delete(:connection)
9
+ datum.delete(:stack)
10
+
11
+ if datum.has_key?(:headers) && datum[:headers].has_key?('Authorization')
12
+ datum[:headers] = datum[:headers].dup
13
+ datum[:headers]['Authorization'] = REDACTED
14
+ end
15
+
16
+ if datum.has_key?(:password)
17
+ datum[:password] = REDACTED
18
+ end
19
+ end
20
+
21
+ indent += 2
22
+ max_key_length = datum.keys.map {|key| key.inspect.length}.max
23
+ datum.keys.sort_by {|key| key.to_s}.each do |key|
24
+ value = datum[key]
25
+ io.write("#{' ' * indent}#{key.inspect.ljust(max_key_length)} => ")
26
+ case value
27
+ when Array
28
+ io.puts("[")
29
+ value.each do |v|
30
+ io.puts("#{' ' * indent} #{v.inspect}")
31
+ end
32
+ io.write("#{' ' * indent}]")
33
+ when Hash
34
+ io.puts("{")
35
+ self.pp(io, value, indent)
36
+ io.write("#{' ' * indent}}")
37
+ else
38
+ io.write("#{value.inspect}")
39
+ end
40
+ io.puts
41
+ end
42
+ indent -= 2
43
+ end
44
+ end
45
+ end
@@ -37,18 +37,23 @@ module Excon
37
37
 
38
38
  def self.parse(socket, datum)
39
39
  # this will discard any trailing lines from the previous response if any.
40
- until match = /^HTTP\/\d+\.\d+\s(\d{3})\s/.match(socket.readline); end
41
- status = match[1].to_i
40
+ until status = socket.readline[9,11].to_i
41
+ end
42
42
 
43
43
  datum[:response] = {
44
44
  :body => '',
45
45
  :headers => Excon::Headers.new,
46
- :status => status,
47
- :remote_ip => socket.respond_to?(:remote_ip) && socket.remote_ip,
48
- :local_port => socket.respond_to?(:local_port) && socket.local_port,
49
- :local_address => socket.respond_to?(:local_address) && socket.local_address
46
+ :status => status
50
47
  }
51
48
 
49
+ unless datum[:scheme] == UNIX
50
+ datum[:response].merge!(
51
+ :remote_ip => socket.remote_ip,
52
+ :local_port => socket.local_port,
53
+ :local_address => socket.local_address
54
+ )
55
+ end
56
+
52
57
  parse_headers(socket, datum)
53
58
 
54
59
  unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])
@@ -57,13 +62,11 @@ module Excon
57
62
  encodings = Utils.split_header_value(datum[:response][:headers][key])
58
63
  if (encoding = encodings.last) && encoding.casecmp('chunked') == 0
59
64
  transfer_encoding_chunked = true
60
- encodings.pop
61
- datum[:response][:headers][key] = encodings.join(', ')
62
- end
63
- end
64
- unless transfer_encoding_chunked
65
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 }
66
- content_length = datum[:response][:headers][key].to_i
65
+ if encodings.length == 1
66
+ datum[:response][:headers].delete(key)
67
+ else
68
+ datum[:response][:headers][key] = encodings[0...-1].join(', ')
69
+ end
67
70
  end
68
71
  end
69
72
 
@@ -102,28 +105,34 @@ module Excon
102
105
  end
103
106
  end
104
107
  parse_headers(socket, datum) # merge trailers into headers
105
- elsif remaining = content_length
106
- if response_block
107
- while remaining > 0
108
- chunk = socket.read([datum[:chunk_size], remaining].min)
109
- response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length)
110
- remaining -= chunk.bytesize
111
- end
112
- else
113
- while remaining > 0
114
- chunk = socket.read([datum[:chunk_size], remaining].min)
115
- datum[:response][:body] << chunk
116
- remaining -= chunk.bytesize
117
- end
118
- end
119
108
  else
120
- if response_block
121
- while chunk = socket.read(datum[:chunk_size])
122
- response_block.call(chunk, nil, nil)
109
+ if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 }
110
+ content_length = datum[:response][:headers][key].to_i
111
+ end
112
+
113
+ if remaining = content_length
114
+ if response_block
115
+ while remaining > 0
116
+ chunk = socket.read([datum[:chunk_size], remaining].min)
117
+ response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length)
118
+ remaining -= chunk.bytesize
119
+ end
120
+ else
121
+ while remaining > 0
122
+ chunk = socket.read([datum[:chunk_size], remaining].min)
123
+ datum[:response][:body] << chunk
124
+ remaining -= chunk.bytesize
125
+ end
123
126
  end
124
127
  else
125
- while chunk = socket.read(datum[:chunk_size])
126
- datum[:response][:body] << chunk
128
+ if response_block
129
+ while chunk = socket.read(datum[:chunk_size])
130
+ response_block.call(chunk, nil, nil)
131
+ end
132
+ else
133
+ while chunk = socket.read(datum[:chunk_size])
134
+ datum[:response][:body] << chunk
135
+ end
127
136
  end
128
137
  end
129
138
  end
@@ -170,6 +179,10 @@ module Excon
170
179
  data
171
180
  end
172
181
 
182
+ def pp
183
+ Excon::PrettyPrinter.pp($stdout, @data)
184
+ end
185
+
173
186
  # Retrieve a specific header value. Header names are treated case-insensitively.
174
187
  # @param [String] name Header name
175
188
  def get_header(name)
@@ -87,26 +87,12 @@ module Excon
87
87
  end
88
88
 
89
89
  def readline
90
- if @eof
91
- raise EOFError, 'end of file reached'
92
- else
93
- line = ''
94
- if @nonblock
95
- while char = read(1)
96
- line << char
97
- break if char == $/
98
- end
99
- raise EOFError, 'end of file reached' if line.empty?
100
- else
101
- begin
102
- Timeout.timeout(@data[:read_timeout]) do
103
- line = @socket.readline
104
- end
105
- rescue Timeout::Error
106
- raise Excon::Errors::Timeout.new('read timeout reached')
107
- end
90
+ begin
91
+ Timeout.timeout(@data[:read_timeout]) do
92
+ @socket.readline
108
93
  end
109
- line
94
+ rescue Timeout::Error
95
+ raise Excon::Errors::Timeout.new('read timeout reached')
110
96
  end
111
97
  end
112
98
 
@@ -153,16 +139,12 @@ module Excon
153
139
  end
154
140
  end
155
141
 
156
- def local_port
157
- ::Socket.unpack_sockaddr_in(@socket.to_io.getsockname)[0]
158
- rescue ArgumentError => e
159
- raise unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
142
+ def local_address
143
+ unpacked_sockaddr[1]
160
144
  end
161
145
 
162
- def local_address
163
- ::Socket.unpack_sockaddr_in(@socket.to_io.getsockname)[1]
164
- rescue ArgumentError => e
165
- raise unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
146
+ def local_port
147
+ unpacked_sockaddr[0]
166
148
  end
167
149
 
168
150
  private
@@ -247,5 +229,13 @@ module Excon
247
229
  end
248
230
  end
249
231
 
232
+ def unpacked_sockaddr
233
+ @unpacked_sockaddr ||= ::Socket.unpack_sockaddr_in(@socket.to_io.getsockname)
234
+ rescue ArgumentError => e
235
+ unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
236
+ raise
237
+ end
238
+ end
239
+
250
240
  end
251
241
  end
@@ -17,32 +17,7 @@ module Excon
17
17
  end
18
18
 
19
19
  $stderr.puts(name)
20
- indent = 0
21
- pretty_printer = lambda do |hash|
22
- indent += 2
23
- max_key_length = hash.keys.map {|key| key.inspect.length}.max
24
- hash.keys.sort_by {|key| key.to_s}.each do |key|
25
- value = hash[key]
26
- $stderr.write("#{' ' * indent}#{key.inspect.ljust(max_key_length)} => ")
27
- case value
28
- when Array
29
- $stderr.puts("[")
30
- value.each do |v|
31
- $stderr.puts("#{' ' * indent} #{v.inspect}")
32
- end
33
- $stderr.write("#{' ' * indent}]")
34
- when Hash
35
- $stderr.puts("{")
36
- pretty_printer.call(value)
37
- $stderr.write("#{' ' * indent}}")
38
- else
39
- $stderr.write("#{value.inspect}")
40
- end
41
- $stderr.puts
42
- end
43
- indent -= 2
44
- end
45
- pretty_printer.call(params)
20
+ Excon::PrettyPrinter.pp($stderr, params)
46
21
 
47
22
  if block_given?
48
23
  yield
@@ -2,21 +2,13 @@ module Excon
2
2
  module Utils
3
3
  extend self
4
4
 
5
- control = (0x0..0x1f).map {|c| c.chr }.join + "\x7f"
6
- delims = '<>#%"'
7
- unwise = '{}|\\^[]`'
8
- nonascii = (0x80..0xff).map {|c| c.chr }.join
9
- UNESCAPED = /([#{ Regexp.escape(control + ' ' + delims + unwise + nonascii) }])/
5
+ CONTROL = (0x0..0x1f).map {|c| c.chr }.join + "\x7f"
6
+ DELIMS = '<>#%"'
7
+ UNWISE = '{}|\\^[]`'
8
+ NONASCII = (0x80..0xff).map {|c| c.chr }.join
9
+ UNESCAPED = /([#{ Regexp.escape(CONTROL + ' ' + DELIMS + UNWISE + NONASCII) }])/
10
10
  ESCAPED = /%([0-9a-fA-F]{2})/
11
11
 
12
- def valid_connection_keys(params = {})
13
- Excon::VALID_CONNECTION_KEYS
14
- end
15
-
16
- def valid_request_keys(params = {})
17
- Excon::VALID_REQUEST_KEYS
18
- end
19
-
20
12
  def connection_uri(datum = @data)
21
13
  unless datum
22
14
  raise ArgumentError, '`datum` must be given unless called on a Connection'
@@ -23,7 +23,7 @@ Shindo.tests('Excon streaming basics') do
23
23
  with_unicorn('streaming.ru') do
24
24
  # expected values: the response, in pieces, and a timeout after each piece
25
25
  res = %w{Hello streamy world}
26
- timeout = 1
26
+ timeout = 0.1
27
27
 
28
28
  # expect the full response as a string
29
29
  # and expect it to take a (timeout * pieces) seconds
@@ -14,32 +14,32 @@ Shindo.tests('HTTPStatusError request/response debugging') do
14
14
  tests('message does not include response or response info').returns(true) do
15
15
  begin
16
16
  Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200)
17
- rescue => err
17
+ rescue Excon::Errors::HTTPStatusError => err
18
18
  err.message.include?('Expected(200) <=> Actual(404 Not Found)') &&
19
- !err.message.include?('request =>') &&
20
- !err.message.include?('response =>')
19
+ !err.message.include?('excon.error.request') &&
20
+ !err.message.include?('excon.error.response')
21
21
  end
22
22
  end
23
23
 
24
- tests('message includes only response info').returns(true) do
24
+ tests('message includes only request info').returns(true) do
25
25
  begin
26
26
  Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200,
27
- :debug_response => true)
28
- rescue => err
27
+ :debug_request => true)
28
+ rescue Excon::Errors::HTTPStatusError => err
29
29
  err.message.include?('Expected(200) <=> Actual(404 Not Found)') &&
30
- !err.message.include?('request =>') &&
31
- !!(err.message =~ /response =>(.*)server says not found/)
30
+ err.message.include?('excon.error.request') &&
31
+ !err.message.include?('excon.error.response')
32
32
  end
33
33
  end
34
34
 
35
- tests('message includes only request info').returns(true) do
35
+ tests('message includes only response info').returns(true) do
36
36
  begin
37
37
  Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200,
38
- :debug_request => true)
39
- rescue => err
38
+ :debug_response => true)
39
+ rescue Excon::Errors::HTTPStatusError => err
40
40
  err.message.include?('Expected(200) <=> Actual(404 Not Found)') &&
41
- !!(err.message =~ /request =>(.*)error\/not_found/) &&
42
- !err.message.include?('response =>')
41
+ !err.message.include?('excon.error.request') &&
42
+ err.message.include?('excon.error.response')
43
43
  end
44
44
  end
45
45
 
@@ -47,10 +47,10 @@ Shindo.tests('HTTPStatusError request/response debugging') do
47
47
  begin
48
48
  Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200,
49
49
  :debug_request => true, :debug_response => true)
50
- rescue => err
50
+ rescue Excon::Errors::HTTPStatusError => err
51
51
  err.message.include?('Expected(200) <=> Actual(404 Not Found)') &&
52
- !!(err.message =~ /request =>(.*)not_found/) &&
53
- !!(err.message =~ /response =>(.*)server says not found/)
52
+ err.message.include?('excon.error.request') &&
53
+ err.message.include?('excon.error.response')
54
54
  end
55
55
  end
56
56
 
@@ -16,7 +16,7 @@ app = lambda do |env|
16
16
  begin
17
17
  # return the response in pieces
18
18
  pieces.each do |x|
19
- sleep 1
19
+ sleep(0.1)
20
20
  io.write(x)
21
21
  io.flush
22
22
  end
@@ -1,45 +1,6 @@
1
1
  Shindo.tests('Request Tests') do
2
2
  with_server('good') do
3
3
 
4
- tests('sets transfer-coding and connection options') do
5
-
6
- tests('without a :response_block') do
7
- request = nil
8
-
9
- returns('trailers, deflate, gzip', 'sets encoding options') do
10
- request = Marshal.load(
11
- Excon.get('http://127.0.0.1:9292/echo/request').body
12
- )
13
-
14
- request[:headers]['TE']
15
- end
16
-
17
- returns(true, 'TE added to Connection header') do
18
- request[:headers]['Connection'].include?('TE')
19
- end
20
- end
21
-
22
- tests('with a :response_block') do
23
- request = nil
24
-
25
- returns('trailers', 'does not set encoding options') do
26
- captures = capture_response_block do |block|
27
- Excon.get('http://127.0.0.1:9292/echo/request',
28
- :response_block => block)
29
- end
30
- data = captures.map {|capture| capture[0] }.join
31
- request = Marshal.load(data)
32
-
33
- request[:headers]['TE']
34
- end
35
-
36
- returns(true, 'TE added to Connection header') do
37
- request[:headers]['Connection'].include?('TE')
38
- end
39
- end
40
-
41
- end
42
-
43
4
  tests('persistent connections') do
44
5
 
45
6
  tests('with default :persistent => true') do
@@ -89,24 +50,6 @@ Shindo.tests('Request Tests') do
89
50
  end
90
51
  end
91
52
 
92
- tests('sends `Connection: close`') do
93
- returns(true, 'when :persistent => false') do
94
- request = Marshal.load(
95
- Excon.get('http://127.0.0.1:9292/echo/request',
96
- :persistent => false).body
97
- )
98
- request[:headers]['Connection'].include?('close')
99
- end
100
-
101
- returns(false, 'not when :persistent => true') do
102
- request = Marshal.load(
103
- Excon.get('http://127.0.0.1:9292/echo/request',
104
- :persistent => true).body
105
- )
106
- request[:headers]['Connection'].include?('close')
107
- end
108
- end
109
-
110
53
  end
111
54
 
112
55
  end
@@ -46,7 +46,7 @@ Shindo.tests('Excon Response Parsing') do
46
46
  Excon.get('http://127.0.0.1:9292/chunked/trailers').headers['Test-Header']
47
47
  end
48
48
 
49
- tests("removes 'chunked' from Transfer-Encoding").returns('') do
49
+ tests("removes 'chunked' from Transfer-Encoding").returns(nil) do
50
50
  Excon.get('http://127.0.0.1:9292/chunked/simple').headers['Transfer-Encoding']
51
51
  end
52
52
 
@@ -153,73 +153,6 @@ Shindo.tests('Excon Response Parsing') do
153
153
 
154
154
  end
155
155
 
156
- tests('Transfer-Encoding') do
157
-
158
- tests('used with chunked response') do
159
- resp = nil
160
-
161
- tests('server sent transfer-encoding').returns('gzip, chunked') do
162
- resp = Excon.post(
163
- 'http://127.0.0.1:9292/echo/transfer-encoded/chunked',
164
- :body => 'hello world'
165
- )
166
-
167
- resp[:headers]['Transfer-Encoding-Sent']
168
- end
169
-
170
- tests('processed encodings removed from header').returns('') do
171
- resp[:headers]['Transfer-Encoding']
172
- end
173
-
174
- tests('response body decompressed').returns('hello world') do
175
- resp[:body]
176
- end
177
- end
178
-
179
- tests('used with non-chunked response') do
180
- resp = nil
181
-
182
- tests('server sent transfer-encoding').returns('gzip') do
183
- resp = Excon.post(
184
- 'http://127.0.0.1:9292/echo/transfer-encoded',
185
- :body => 'hello world'
186
- )
187
-
188
- resp[:headers]['Transfer-Encoding-Sent']
189
- end
190
-
191
- tests('processed encoding removed from header').returns('') do
192
- resp[:headers]['Transfer-Encoding']
193
- end
194
-
195
- tests('response body decompressed').returns('hello world') do
196
- resp[:body]
197
- end
198
- end
199
-
200
- # sends TE header without gzip/deflate accepted (see requests_tests)
201
- tests('with a :response_block') do
202
- captures = nil
203
-
204
- tests('server does not compress').returns('chunked') do
205
- resp = nil
206
- captures = capture_response_block do |block|
207
- resp = Excon.post('http://127.0.0.1:9292/echo/transfer-encoded/chunked',
208
- :body => 'hello world',
209
- :response_block => block)
210
- end
211
-
212
- resp[:headers]['Transfer-Encoding-Sent']
213
- end
214
-
215
- tests('block receives uncompressed response').returns('hello world') do
216
- captures.map {|capture| capture[0] }.join
217
- end
218
-
219
- end
220
-
221
- end
222
-
223
156
  end
224
157
 
225
158
  env_restore
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.0
4
+ version: 0.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dpiddy (Dan Peterson)
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2014-07-09 00:00:00.000000000 Z
13
+ date: 2014-08-01 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -173,6 +173,7 @@ files:
173
173
  - lib/excon/middlewares/mock.rb
174
174
  - lib/excon/middlewares/redirect_follower.rb
175
175
  - lib/excon/middlewares/response_parser.rb
176
+ - lib/excon/pretty_printer.rb
176
177
  - lib/excon/response.rb
177
178
  - lib/excon/socket.rb
178
179
  - lib/excon/ssl_socket.rb
@@ -245,7 +246,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
245
246
  version: '0'
246
247
  requirements: []
247
248
  rubyforge_project: excon
248
- rubygems_version: 2.2.2
249
+ rubygems_version: 2.3.0
249
250
  signing_key:
250
251
  specification_version: 2
251
252
  summary: speed, persistence, http(s)