excon 0.30.0 → 0.31.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.

@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- excon (0.30.0)
4
+ excon (0.31.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.md CHANGED
@@ -45,6 +45,41 @@ post_response = connection.post(:path => '/foo')
45
45
  delete_response = connection.delete(:path => '/bar')
46
46
  ```
47
47
 
48
+ By default, each connection is non-persistent. This means that each request made against a connection behaves like a
49
+ one-off request. Each request will establish a socket connection to the server, then close the socket once the request
50
+ is complete.
51
+
52
+ To use a persistent connection, use the `:persistent` option:
53
+
54
+ ```ruby
55
+ connection = Excon.new('http://geemus.com', :persistent => true)
56
+ ```
57
+
58
+ The initial request will establish a socket connection to the server and leave the socket open. Subsequent requests
59
+ will reuse that socket. You may call `Connection#reset` at any time to close the underlying socket, and the next request
60
+ will establish a new socket connection.
61
+
62
+ You may also control persistence on a per-request basis by setting the `:persistent` option for each request.
63
+
64
+ ```ruby
65
+ connection = Excon.new('http://geemus.com') # non-persistent by default
66
+ connection.get # socket established, then closed
67
+ connection.get(:persistent => true) # socket established, left open
68
+ connection.get(:persistent => true) # socket reused
69
+ connection.get # socket reused, then closed
70
+
71
+ connection = Excon.new('http://geemus.com', :persistent => true)
72
+ connection.get # socket established, left open
73
+ connection.get(:persistent => false) # socket reused, then closed
74
+ connection.get(:persistent => false) # socket established, then closed
75
+ connection.get # socket established, left open
76
+ connection.get # socket reused
77
+ ```
78
+
79
+ Note that sending a request with `:persistent => false` to close the socket will also send `Connection: close` to inform
80
+ the server the connection is no longer needed. `Connection#reset` will simply close our end of the socket.
81
+
82
+
48
83
  Options
49
84
  -------
50
85
 
@@ -131,13 +166,16 @@ Iterating in this way allows you to have more granular control over writes and t
131
166
  Pipelining Requests
132
167
  ------------------
133
168
 
134
- You can make use of HTTP pipelining to improve performance. Instead of the normal request/response cyle, pipelining sends a series of requests and then receives a series of responses. You can take advantage of this using the `requests` method, which takes an array of params where each is a hash like request would receive and returns an array of responses.
169
+ You can make use of HTTP pipelining to improve performance. Instead of the normal request/response cycle, pipelining sends a series of requests and then receives a series of responses. You can take advantage of this using the `requests` method, which takes an array of params where each is a hash like request would receive and returns an array of responses.
135
170
 
136
171
  ```ruby
137
172
  connection = Excon.new('http://geemus.com/')
138
173
  connection.requests([{:method => :get}, {:method => :get}])
139
174
  ```
140
175
 
176
+ By default, each call to `requests` will use a separate persistent socket connection. To make multiple `requests` calls
177
+ using a single persistent connection, set `:persistent => true` when establishing the connection.
178
+
141
179
  Streaming Responses
142
180
  -------------------
143
181
 
@@ -307,6 +345,35 @@ Excon.defaults[:ssl_verify_peer] = false
307
345
 
308
346
  Either of these should allow you to work around the socket error and continue with your work.
309
347
 
348
+ ## Getting Help
349
+
350
+ * [General Documentation](http://excon.io).
351
+ * Ask specific questions on [Stack Overflow](http://stackoverflow.com/questions/tagged/excon).
352
+ * Report bugs and discuss potential features in [Github issues](https://github.com/geemus/excon/issues).
353
+
354
+ ## Getting Involved
355
+
356
+ New contributors are always welcome, when it doubt please ask questions. We strive to be an open and welcoming community. Please be nice to one another.
357
+
358
+ ### Coding
359
+
360
+ * Pick a task:
361
+ * Offer feedback on open [pull requests](https://github.com/geemus/excon/pulls).
362
+ * Review open [issues](https://github.com/geemus/excon/issues) for things to help on.
363
+ * [Create an issue](https://github.com/geemus/excon/issues/new) to start a discussion on additions or features.
364
+ * Fork the project, add your changes and tests to cover them in a topic branch.
365
+ * Commit your changes and rebase against `geemus/excon` to ensure everything is up to date.
366
+ * [Submit a pull request](https://github.com/geemus/excon/compare/).
367
+
368
+ ### Non-Coding
369
+
370
+ * Offer feedback on open [issues](https://github.com/geemus/excon/issues).
371
+ * Write and help edit [documentation](https://github.com/geemus/excon.github.com).
372
+ * Translate [documentation](https://github.com/geemus/excon.github.com) in to other languages.
373
+ * Organize or volunteer at events.
374
+ * [Donate](https://www.gittip.com/geemus/)!
375
+ * Discuss other ideas for contribution with [geemus](mailto:geemus+excon@gmail.com).
376
+
310
377
  Copyright
311
378
  ---------
312
379
 
@@ -1,3 +1,17 @@
1
+ 0.31.0 12/16/2013
2
+ =================
3
+
4
+ test fixes for Bundler.require usage and Rack::Lint
5
+ use production mode + dump errors for tests
6
+ use Utils in Excon.stub
7
+ add implementation of stuff from WEBrick directly to utils
8
+ update test server to send connection close and process buffer after response
9
+ add :persistent option, defaults true
10
+ group HTTP errors by type
11
+ patch to webrick to workaround intermitent test failures
12
+ only use Open4 for 1.8.7
13
+ update/expand getting help/getting involved in readme
14
+
1
15
  0.30.0 11/25/2013
2
16
  =================
3
17
 
@@ -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.30.0'
17
- s.date = '2013-11-25'
16
+ s.version = '0.31.0'
17
+ s.date = '2013-12-16'
18
18
  s.rubyforge_project = 'excon'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -125,6 +125,7 @@ Gem::Specification.new do |s|
125
125
  tests/middlewares/instrumentation_tests.rb
126
126
  tests/middlewares/mock_tests.rb
127
127
  tests/middlewares/redirect_follower_tests.rb
128
+ tests/pipeline_tests.rb
128
129
  tests/proxy_tests.rb
129
130
  tests/query_string_tests.rb
130
131
  tests/rackups/basic.rb
@@ -140,9 +141,10 @@ Gem::Specification.new do |s|
140
141
  tests/rackups/ssl_verify_peer.ru
141
142
  tests/rackups/thread_safety.ru
142
143
  tests/rackups/timeout.ru
144
+ tests/rackups/webrick_patch.rb
143
145
  tests/request_headers_tests.rb
144
146
  tests/request_method_tests.rb
145
- tests/requests_tests.rb
147
+ tests/request_tests.rb
146
148
  tests/response_tests.rb
147
149
  tests/servers/bad.rb
148
150
  tests/servers/eof.rb
@@ -10,7 +10,6 @@ require 'timeout'
10
10
  require 'uri'
11
11
  require 'zlib'
12
12
  require 'stringio'
13
- require 'webrick/httputils'
14
13
 
15
14
  BasicSocket.do_not_reverse_lookup = true
16
15
 
@@ -41,6 +40,7 @@ module Excon
41
40
  :mock => false,
42
41
  :nonblock => true,
43
42
  :omit_default_port => false,
43
+ :persistent => false,
44
44
  :read_timeout => 60,
45
45
  :retry_limit => DEFAULT_RETRY_LIMIT,
46
46
  :ssl_verify_peer => true,
@@ -166,7 +166,7 @@ module Excon
166
166
  )
167
167
  if uri.user || uri.password
168
168
  request_params[:headers] ||= {}
169
- user, pass = URI.decode_www_form_component(uri.user.to_s), URI.decode_www_form_component(uri.password.to_s)
169
+ user, pass = Utils.unescape_form(uri.user.to_s), Utils.unescape_form(uri.password.to_s)
170
170
  request_params[:headers]['Authorization'] ||= 'Basic ' << ['' << user << ':' << pass].pack('m').delete(Excon::CR_NL)
171
171
  end
172
172
  end
@@ -152,7 +152,7 @@ module Excon
152
152
  else
153
153
  datum[:headers]['TE'] = 'trailers, deflate, gzip'
154
154
  end
155
- datum[:headers]['Connection'] = 'TE'
155
+ datum[:headers]['Connection'] = datum[:persistent] ? 'TE' : 'TE, close'
156
156
 
157
157
  # add headers to request
158
158
  datum[:headers].each do |key, values|
@@ -267,10 +267,14 @@ module Excon
267
267
  unless datum[:pipeline]
268
268
  datum = response(datum)
269
269
 
270
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
271
- if split_header_value(datum[:response][:headers][key]).any? {|t| t.casecmp('close') }
272
- reset
270
+ if datum[:persistent]
271
+ if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
272
+ if split_header_value(datum[:response][:headers][key]).any? {|t| t.casecmp('close') }
273
+ reset
274
+ end
273
275
  end
276
+ else
277
+ reset
274
278
  end
275
279
 
276
280
  Excon::Response.new(datum[:response])
@@ -290,16 +294,23 @@ module Excon
290
294
  # Sends the supplied requests to the destination host using pipelining.
291
295
  # @pipeline_params [Array<Hash>] pipeline_params An array of one or more optional params, override defaults set in Connection.new, see #request for details
292
296
  def requests(pipeline_params)
297
+ pipeline_params.each {|params| params.merge!(:pipeline => true, :persistent => true) }
298
+ pipeline_params.last.merge!(:persistent => @data[:persistent])
299
+
293
300
  responses = pipeline_params.map do |params|
294
- request(params.merge!(:pipeline => true))
301
+ request(params)
295
302
  end.map do |datum|
296
303
  Excon::Response.new(response(datum)[:response])
297
304
  end
298
305
 
299
- if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
300
- if split_header_value(responses.last[:headers][key]).any? {|t| t.casecmp('close') }
301
- reset
306
+ if @data[:persistent]
307
+ if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
308
+ if split_header_value(responses.last[:headers][key]).any? {|t| t.casecmp('close') }
309
+ reset
310
+ end
302
311
  end
312
+ else
313
+ reset
303
314
  end
304
315
 
305
316
  responses
@@ -1,6 +1,6 @@
1
1
  module Excon
2
2
 
3
- VERSION = '0.30.0'
3
+ VERSION = '0.31.0'
4
4
 
5
5
  CR_NL = "\r\n"
6
6
 
@@ -46,6 +46,7 @@ module Excon
46
46
  :middlewares,
47
47
  :mock,
48
48
  :path,
49
+ :persistent,
49
50
  :pipeline,
50
51
  :query,
51
52
  :read_timeout,
@@ -35,47 +35,54 @@ module Excon
35
35
  @response = response
36
36
  end
37
37
  end
38
-
39
- class Continue < HTTPStatusError; end # 100
40
- class SwitchingProtocols < HTTPStatusError; end # 101
41
- class OK < HTTPStatusError; end # 200
42
- class Created < HTTPStatusError; end # 201
43
- class Accepted < HTTPStatusError; end # 202
44
- class NonAuthoritativeInformation < HTTPStatusError; end # 203
45
- class NoContent < HTTPStatusError; end # 204
46
- class ResetContent < HTTPStatusError; end # 205
47
- class PartialContent < HTTPStatusError; end # 206
48
- class MultipleChoices < HTTPStatusError; end # 300
49
- class MovedPermanently < HTTPStatusError; end # 301
50
- class Found < HTTPStatusError; end # 302
51
- class SeeOther < HTTPStatusError; end # 303
52
- class NotModified < HTTPStatusError; end # 304
53
- class UseProxy < HTTPStatusError; end # 305
54
- class TemporaryRedirect < HTTPStatusError; end # 307
55
- class BadRequest < HTTPStatusError; end # 400
56
- class Unauthorized < HTTPStatusError; end # 401
57
- class PaymentRequired < HTTPStatusError; end # 402
58
- class Forbidden < HTTPStatusError; end # 403
59
- class NotFound < HTTPStatusError; end # 404
60
- class MethodNotAllowed < HTTPStatusError; end # 405
61
- class NotAcceptable < HTTPStatusError; end # 406
62
- class ProxyAuthenticationRequired < HTTPStatusError; end # 407
63
- class RequestTimeout < HTTPStatusError; end # 408
64
- class Conflict < HTTPStatusError; end # 409
65
- class Gone < HTTPStatusError; end # 410
66
- class LengthRequired < HTTPStatusError; end # 411
67
- class PreconditionFailed < HTTPStatusError; end # 412
68
- class RequestEntityTooLarge < HTTPStatusError; end # 413
69
- class RequestURITooLong < HTTPStatusError; end # 414
70
- class UnsupportedMediaType < HTTPStatusError; end # 415
71
- class RequestedRangeNotSatisfiable < HTTPStatusError; end # 416
72
- class ExpectationFailed < HTTPStatusError; end # 417
73
- class UnprocessableEntity < HTTPStatusError; end # 422
74
- class InternalServerError < HTTPStatusError; end # 500
75
- class NotImplemented < HTTPStatusError; end # 501
76
- class BadGateway < HTTPStatusError; end # 502
77
- class ServiceUnavailable < HTTPStatusError; end # 503
78
- class GatewayTimeout < HTTPStatusError; end # 504
38
+
39
+ # HTTP Error classes
40
+ class Informational < HTTPStatusError; end
41
+ class Success < HTTPStatusError; end
42
+ class Redirection < HTTPStatusError; end
43
+ class ClientError < HTTPStatusError; end
44
+ class ServerError < HTTPStatusError; end
45
+
46
+ class Continue < Informational; end # 100
47
+ class SwitchingProtocols < Informational; end # 101
48
+ class OK < Success; end # 200
49
+ class Created < Success; end # 201
50
+ class Accepted < Success; end # 202
51
+ class NonAuthoritativeInformation < Success; end # 203
52
+ class NoContent < Success; end # 204
53
+ class ResetContent < Success; end # 205
54
+ class PartialContent < Success; end # 206
55
+ class MultipleChoices < Redirection; end # 300
56
+ class MovedPermanently < Redirection; end # 301
57
+ class Found < Redirection; end # 302
58
+ class SeeOther < Redirection; end # 303
59
+ class NotModified < Redirection; end # 304
60
+ class UseProxy < Redirection; end # 305
61
+ class TemporaryRedirect < Redirection; end # 307
62
+ class BadRequest < ClientError; end # 400
63
+ class Unauthorized < ClientError; end # 401
64
+ class PaymentRequired < ClientError; end # 402
65
+ class Forbidden < ClientError; end # 403
66
+ class NotFound < ClientError; end # 404
67
+ class MethodNotAllowed < ClientError; end # 405
68
+ class NotAcceptable < ClientError; end # 406
69
+ class ProxyAuthenticationRequired < ClientError; end # 407
70
+ class RequestTimeout < ClientError; end # 408
71
+ class Conflict < ClientError; end # 409
72
+ class Gone < ClientError; end # 410
73
+ class LengthRequired < ClientError; end # 411
74
+ class PreconditionFailed < ClientError; end # 412
75
+ class RequestEntityTooLarge < ClientError; end # 413
76
+ class RequestURITooLong < ClientError; end # 414
77
+ class UnsupportedMediaType < ClientError; end # 415
78
+ class RequestedRangeNotSatisfiable < ClientError; end # 416
79
+ class ExpectationFailed < ClientError; end # 417
80
+ class UnprocessableEntity < ClientError; end # 422
81
+ class InternalServerError < ServerError; end # 500
82
+ class NotImplemented < ServerError; end # 501
83
+ class BadGateway < ServerError; end # 502
84
+ class ServiceUnavailable < ServerError; end # 503
85
+ class GatewayTimeout < ServerError; end # 504
79
86
 
80
87
  # Messages for nicer exceptions, from rfc2616
81
88
  def self.status_error(request, response)
@@ -2,6 +2,8 @@ module Excon
2
2
  module Utils
3
3
  extend self
4
4
 
5
+ ESCAPED = /%([0-9a-fA-F]{2})/
6
+
5
7
  def valid_connection_keys(params = {})
6
8
  Excon::VALID_CONNECTION_KEYS
7
9
  end
@@ -57,17 +59,27 @@ module Excon
57
59
  # Splits a header value +str+ according to HTTP specification.
58
60
  def split_header_value(str)
59
61
  return [] if str.nil?
60
- WEBrick::HTTPUtils.split_header_value(str.strip)
62
+ str = str.strip
63
+ str.force_encoding('BINARY') if FORCE_ENC
64
+ str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
65
+ (?:,\s*|\Z)'xn).flatten
61
66
  end
62
67
 
63
68
  # Unescapes HTTP reserved and unwise characters in +str+
64
69
  def unescape_uri(str)
65
- WEBrick::HTTPUtils.unescape(str)
70
+ str = str.dup
71
+ str.force_encoding('BINARY') if FORCE_ENC
72
+ str.gsub!(ESCAPED) { $1.hex.chr }
73
+ str
66
74
  end
67
75
 
68
76
  # Unescape form encoded values in +str+
69
77
  def unescape_form(str)
70
- WEBrick::HTTPUtils.unescape_form(str)
78
+ str = str.dup
79
+ str.force_encoding('BINARY') if FORCE_ENC
80
+ str.gsub!(/\+/, ' ')
81
+ str.gsub!(ESCAPED) { $1.hex.chr }
82
+ str
71
83
  end
72
84
  end
73
85
  end
@@ -1,15 +1,16 @@
1
1
  Shindo.tests('Excon basics (Authorization data redacted)') do
2
2
  with_rackup('basic_auth.ru') do
3
3
  cases = [
4
- ['user & pass', 'http://user1:pass1@foo.com/', 'Basic dXNlcjE6cGFzczE='],
5
- ['email & pass', 'http://foo%40bar.com:pass1@foo.com/', 'Basic Zm9vQGJhci5jb206cGFzczE='],
6
- ['user no pass', 'http://three_user@foo.com/', 'Basic dGhyZWVfdXNlcjo='],
7
- ['pass no user', 'http://:derppass@foo.com/', 'Basic OmRlcnBwYXNz']
8
- ]
4
+ ['user & pass', 'http://user1:pass1@foo.com/', 'Basic dXNlcjE6cGFzczE='],
5
+ ['email & pass', 'http://foo%40bar.com:pass1@foo.com/', 'Basic Zm9vQGJhci5jb206cGFzczE='],
6
+ ['user no pass', 'http://three_user@foo.com/', 'Basic dGhyZWVfdXNlcjo='],
7
+ ['pass no user', 'http://:derppass@foo.com/', 'Basic OmRlcnBwYXNz']
8
+ ]
9
9
  cases.each do |desc,url,auth_header|
10
- conn = Excon.new(url)
10
+ conn = nil
11
11
 
12
12
  test("authorization header concealed for #{desc}") do
13
+ conn = Excon.new(url)
13
14
  !conn.inspect.include?(auth_header)
14
15
  end
15
16
 
@@ -61,8 +61,7 @@ Shindo.tests('Excon basics (ssl file)',['focus']) do
61
61
 
62
62
  basic_tests('https://127.0.0.1:8443',
63
63
  :client_key => File.join(File.dirname(__FILE__), 'data', 'excon.cert.key'),
64
- :client_cert => File.join(File.dirname(__FILE__), 'data', 'excon.cert.crt'),
65
- :reset_connection => RUBY_VERSION == '1.9.2'
64
+ :client_cert => File.join(File.dirname(__FILE__), 'data', 'excon.cert.crt')
66
65
  )
67
66
 
68
67
  end
@@ -4,12 +4,15 @@ Shindo.tests('Excon response header support') do
4
4
  with_rackup('response_header.ru') do
5
5
 
6
6
  tests('Response#get_header') do
7
- connection = Excon.new('http://foo.com:8080', :proxy => 'http://127.0.0.1:9292')
8
- response = connection.request(:method => :get, :path => '/foo')
7
+ connection = nil
8
+ response = nil
9
9
 
10
10
  tests('with variable header capitalization') do
11
11
 
12
12
  tests('response.get_header("mixedcase-header")').returns('MixedCase') do
13
+ connection = Excon.new('http://foo.com:8080', :proxy => 'http://127.0.0.1:9292')
14
+ response = connection.request(:method => :get, :path => '/foo')
15
+
13
16
  response.get_header("mixedcase-header")
14
17
  end
15
18
 
@@ -12,15 +12,12 @@ Shindo.tests("Excon support for middlewares that return canned responses") do
12
12
  end
13
13
  end
14
14
 
15
- connection = Excon.new(
16
- 'http://some-host.com/some-path',
17
- :method => :get,
18
- :middlewares => [canned_response_middleware] + Excon.defaults[:middlewares],
19
- :response_block => Proc.new { } # to force streaming
20
- )
21
-
22
15
  tests('does not mutate the canned response body').returns("canned") do
23
- connection.request
16
+ Excon.get(
17
+ 'http://some-host.com/some-path',
18
+ :middlewares => [canned_response_middleware] + Excon.defaults[:middlewares],
19
+ :response_block => Proc.new { } # to force streaming
20
+ )
24
21
  the_body
25
22
  end
26
23
  end