excon 0.25.3 → 0.26.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.25.3)
4
+ excon (0.26.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -9,7 +9,7 @@ GEM
9
9
  activesupport (3.2.6)
10
10
  i18n (~> 0.6)
11
11
  multi_json (~> 1.0)
12
- bouncy-castle-java (1.5.0146.1)
12
+ bouncy-castle-java (1.5.0147)
13
13
  chronic (0.6.7)
14
14
  delorean (2.0.0)
15
15
  chronic
@@ -17,8 +17,8 @@ GEM
17
17
  eventmachine (1.0.0-java)
18
18
  formatador (0.2.3)
19
19
  i18n (0.6.0)
20
- jruby-openssl (0.7.7)
21
- bouncy-castle-java (>= 1.5.0146.1)
20
+ jruby-openssl (0.8.8)
21
+ bouncy-castle-java (>= 1.5.0147)
22
22
  json (1.7.3)
23
23
  json (1.7.3-java)
24
24
  multi_json (1.3.6)
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  excon
2
2
  =====
3
3
 
4
- Usable, fast, simple Ruby HTTP 1.0
4
+ Usable, fast, simple Ruby HTTP 1.1
5
+
6
+ Excon was designed to be simple, fast and performant. It works great as a general HTTP(s) client and is particularly well suited to usage in API clients.
5
7
 
6
8
  [![Build Status](https://secure.travis-ci.org/geemus/excon.png)](http://travis-ci.org/geemus/excon)
7
9
  [![Dependency Status](https://gemnasium.com/geemus/excon.png)](https://gemnasium.com/geemus/excon)
@@ -12,130 +14,142 @@ Getting Started
12
14
 
13
15
  Install the gem.
14
16
 
15
- $ sudo gem install excon
17
+ ```
18
+ $ sudo gem install excon
19
+ ```
16
20
 
17
21
  Require with rubygems.
18
22
 
19
- require 'rubygems'
20
- require 'excon'
21
-
22
- The simplest way to use excon is with one-off requests:
23
-
24
- response = Excon.get('http://geemus.com')
25
-
26
- Supported one-off request methods are #connect, #delete, #get, #head, #options, #post, #put, and #trace.
27
-
28
- The returned response object has #body, #headers and #status attributes.
29
-
30
- Alternately you can create a connection object which is reusable across multiple requests (more performant!).
23
+ ```ruby
24
+ require 'rubygems'
25
+ require 'excon'
26
+ ```
31
27
 
32
- connection = Excon.new('http://geemus.com')
33
- response_one = connection.get
34
- response_two = connection.post(:path => '/foo')
35
- response_three = connection.delete(:path => '/bar')
28
+ The easiest way to get started is by using one-off requests. Supported one-off request methods are `connect`, `delete`, `get`, `head`, `options`, `post`, `put`, and `trace`. Requests return a response object which has `body`, `headers`, `remote_ip` and `status` attributes.
36
29
 
37
- Sometimes it is more convenient to specify the request type as an argument:
30
+ ```ruby
31
+ response = Excon.get('http://geemus.com')
32
+ response.body # => "..."
33
+ response.headers # => {...}
34
+ response.remote_ip # => "..."
35
+ response.status # => 200
36
+ ```
38
37
 
39
- response_four = connection.request(:method => :get, :path => '/more')
38
+ For API clients or other ongoing usage, reuse a connection across multiple requests to share options and improve performance.
40
39
 
41
- Both one-off and persistent connections support many other options. Here are a few common examples:
40
+ ```ruby
41
+ connection = Excon.new('http://geemus.com')
42
+ get_response = connection.get
43
+ post_response = connection.post(:path => '/foo')
44
+ delete_response = connection.delete(:path => '/bar')
45
+ ```
42
46
 
43
- # Custom headers
44
- Excon.get('http://geemus.com', :headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
45
- connection.get(:headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
47
+ Options
48
+ -------
46
49
 
47
- # Changing query strings
48
- connection = Excon.new('http://geemus.com/')
49
- connection.get(:query => {:foo => 'bar'})
50
+ Both one-off and persistent connections support many other options. The final options for a request are built up by starting with `Excon.defaults`, then merging in options from the connection and finally merging in any request options. In this way you have plenty of options on where and how to set options and can easily setup connections or defaults to match common options for a particular endpoint.
50
51
 
51
- # POST body encoded with application/x-www-form-urlencoded
52
- Excon.post('http://geemus.com',
53
- :body => 'language=ruby&class=fog',
54
- :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
52
+ Here are a few common examples:
55
53
 
56
- # same again, but using URI to build the body of parameters
57
- Excon.post('http://geemus.com',
58
- :body => URI.encode_www_form(:language => 'ruby', :class => 'fog'),
59
- :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
54
+ ```ruby
55
+ # Custom headers
56
+ Excon.get('http://geemus.com', :headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
57
+ connection.get(:headers => {'Authorization' => 'Basic 0123456789ABCDEF'})
60
58
 
61
- # request accepts either symbols or strings
62
- connection.request(:method => :get)
63
- connection.request(:method => 'GET')
59
+ # Changing query strings
60
+ connection = Excon.new('http://geemus.com/')
61
+ connection.get(:query => {:foo => 'bar'})
64
62
 
65
- # this request can be repeated safely, so retry on errors up to 3 times
66
- connection.request(:idempotent => true)
63
+ # POST body encoded with application/x-www-form-urlencoded
64
+ Excon.post('http://geemus.com',
65
+ :body => 'language=ruby&class=fog',
66
+ :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
67
67
 
68
- # this request can be repeated safely, retry up to 6 times
69
- connection.request(:idempotent => true, :retry_limit => 6)
68
+ # same again, but using URI to build the body of parameters
69
+ Excon.post('http://geemus.com',
70
+ :body => URI.encode_www_form(:language => 'ruby', :class => 'fog'),
71
+ :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
70
72
 
71
- # opt-out of nonblocking operations for performance and/or as a workaround
72
- connection.request(:nonblock => false)
73
+ # request takes a method option, accepting either a symbol or string
74
+ connection.request(:method => :get)
75
+ connection.request(:method => 'GET')
73
76
 
74
- # opt-in to omitting port from http:80 and https:443
75
- connection.request(:omit_default_port => true)
77
+ # expect one or more status codes, or raise an error
78
+ connection.request(:expects => [200, 201], :method => :get)
76
79
 
77
- # set longer connect_timeout (default is 60 seconds)
78
- connection.request(:connect_timeout => 360)
80
+ # this request can be repeated safely, so retry on errors up to 3 times
81
+ connection.request(:idempotent => true)
79
82
 
80
- # set longer read_timeout (default is 60 seconds)
81
- connection.request(:read_timeout => 360)
83
+ # this request can be repeated safely, retry up to 6 times
84
+ connection.request(:idempotent => true, :retry_limit => 6)
82
85
 
83
- # set longer write_timeout (default is 60 seconds)
84
- connection.request(:write_timeout => 360)
86
+ # opt-out of nonblocking operations for performance and/or as a workaround
87
+ connection.request(:nonblock => false)
85
88
 
86
- # Enable the socket option TCP_NODELAY on the underlying socket.
87
- #
88
- # This can improve response time when sending frequent short
89
- # requests in time-sensitive scenarios.
90
- #
91
- connection = Excon.new('http://geemus.com/', :tcp_nodelay => true)
89
+ # opt-in to omitting port from http:80 and https:443
90
+ connection.request(:omit_default_port => true)
92
91
 
93
- These options can be combined to make pretty much any request you might need.
92
+ # set longer connect_timeout (default is 60 seconds)
93
+ connection.request(:connect_timeout => 360)
94
94
 
95
- Excon can also expect one or more HTTP status code in response, raising an exception if the response does not meet the criteria.
95
+ # set longer read_timeout (default is 60 seconds)
96
+ connection.request(:read_timeout => 360)
96
97
 
97
- If you need to accept as response one or more HTTP status codes you can declare them in an array:
98
+ # set longer write_timeout (default is 60 seconds)
99
+ connection.request(:write_timeout => 360)
98
100
 
99
- connection.request(:expects => [200, 201], :method => :get, :path => path, :query => {})
101
+ # Enable the socket option TCP_NODELAY on the underlying socket.
102
+ #
103
+ # This can improve response time when sending frequent short
104
+ # requests in time-sensitive scenarios.
105
+ #
106
+ connection = Excon.new('http://geemus.com/', :tcp_nodelay => true)
107
+ ```
100
108
 
101
109
  Chunked Requests
102
110
  ----------------
103
111
 
104
112
  You can make `Transfer-Encoding: chunked` requests by passing a block that will deliver chunks, delivering an empty chunk to signal completion.
105
113
 
106
- file = File.open('data')
114
+ ```ruby
115
+ file = File.open('data')
107
116
 
108
- chunker = lambda do
109
- # Excon.defaults[:chunk_size] defaults to 1048576, ie 1MB
110
- # to_s will convert the nil receieved after everything is read to the final empty chunk
111
- file.read(Excon.defaults[:chunk_size]).to_s
112
- end
117
+ chunker = lambda do
118
+ # Excon.defaults[:chunk_size] defaults to 1048576, ie 1MB
119
+ # to_s will convert the nil receieved after everything is read to the final empty chunk
120
+ file.read(Excon.defaults[:chunk_size]).to_s
121
+ end
113
122
 
114
- Excon.post('http://geemus.com', :request_block => chunker)
123
+ Excon.post('http://geemus.com', :request_block => chunker)
115
124
 
116
- file.close
125
+ file.close
126
+ ```
117
127
 
118
128
  Iterating in this way allows you to have more granular control over writes and to write things where you can not calculate the overall length up front.
119
129
 
120
130
  Pipelining Requests
121
131
  ------------------
122
132
 
123
- You can make use of HTTP pipelining to improve performance. Insead 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.
133
+ 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.
124
134
 
125
- connection = Excon.new('http://geemus.com/')
126
- connection.requests([{:method => :get}, {:method => :get}])
135
+ ```ruby
136
+ connection = Excon.new('http://geemus.com/')
137
+ connection.requests([{:method => :get}, {:method => :get}])
138
+ ```
127
139
 
128
140
  Streaming Responses
129
141
  -------------------
130
142
 
131
143
  You can stream responses by passing a block that will receive each chunk.
132
144
 
133
- streamer = lambda do |chunk, remaining_bytes, total_bytes|
134
- puts chunk
135
- puts "Remaining: #{remaining_bytes.to_f / total_bytes}%"
136
- end
145
+ ```ruby
146
+ streamer = lambda do |chunk, remaining_bytes, total_bytes|
147
+ puts chunk
148
+ puts "Remaining: #{remaining_bytes.to_f / total_bytes}%"
149
+ end
137
150
 
138
- Excon.get('http://geemus.com', :response_block => streamer)
151
+ Excon.get('http://geemus.com', :response_block => streamer)
152
+ ```
139
153
 
140
154
  Iterating over each chunk will allow you to do work on the response incrementally without buffering the entire response first. For very large responses this can lead to significant memory savings.
141
155
 
@@ -144,8 +158,10 @@ Proxy Support
144
158
 
145
159
  You can specify a proxy URL that Excon will use with both HTTP and HTTPS connections:
146
160
 
147
- connection = Excon.new('http://geemus.com', :proxy => 'http://my.proxy:3128')
148
- connection.request(:method => 'GET')
161
+ ```ruby
162
+ connection = Excon.new('http://geemus.com', :proxy => 'http://my.proxy:3128')
163
+ connection.request(:method => 'GET')
164
+ ```
149
165
 
150
166
  The proxy URL must be fully specified, including scheme (e.g. "http://") and port.
151
167
 
@@ -158,94 +174,96 @@ Stubs
158
174
 
159
175
  You can stub out requests for testing purposes by enabling mock mode on a connection.
160
176
 
161
- connection = Excon.new('http://example.com', :mock => true)
177
+ ```ruby
178
+ connection = Excon.new('http://example.com', :mock => true)
179
+ ```
162
180
 
163
181
  Or by enabling mock mode for a request.
164
182
 
165
- connection.request(:method => :get, :path => 'example', :mock => true)
166
-
167
- Then you can add stubs, for instance:
183
+ ```ruby
184
+ connection.request(:method => :get, :path => 'example', :mock => true)
185
+ ```
168
186
 
169
- # Excon.stub(request_attributes, response_attributes)
170
- Excon.stub({:method => :get}, {:body => 'body', :status => 200})
187
+ Add stubs by providing the request_attributes to match and response attributes to return. Response params can be specified as either a hash or block which will yield with response_params.
171
188
 
172
- Omitted attributes are assumed to match, so this stub will match any get request and return an Excon::Response with a body of 'body' and status of 200. You can add whatever stubs you might like this way and they will be checked against in the order they were added, if none of them match then excon will raise an error to let you know.
189
+ ```ruby
190
+ Excon.stub({}, {:body => 'body', :status => 200})
191
+ Excon.stub({}, lambda {|request_params| :body => request_params[:body], :status => 200})
192
+ ```
173
193
 
174
- Alternatively you can pass a block instead of `response_attributes` and it will be called with the request params. For example, you could create a stub that echoes the body given to it like this:
175
-
176
- # Excon.stub(request_attributes, &response_block)
177
- Excon.stub({:method => :put}) do |params|
178
- {:body => params[:body], :status => 200}
179
- end
194
+ Omitted attributes are assumed to match, so this stub will match *any* request and return an Excon::Response with a body of 'body' and status of 200. You can add whatever stubs you might like this way and they will be checked against in the order they were added, if none of them match then excon will raise an `Excon::Errors::StubNotFound` error to let you know.
180
195
 
181
- In order to clear all previously defined stubs you can use:
196
+ To remove a previously defined stub, or all stubs:
182
197
 
183
- Excon.stubs.clear
184
-
185
- Or to simply remove the last defined stub you can use:
186
-
187
- Excon.stubs.shift
198
+ ```ruby
199
+ Excon.unstub({}) # remove first/oldest stub matching {}
200
+ Excon.stubs.clear # remove all stubs
201
+ ```
188
202
 
189
203
  For example, if using RSpec for your test suite you can clear stubs after running each example:
190
204
 
191
- config.after(:each) do
192
- Excon.stubs.clear
193
- end
194
-
195
- You can also modify 'Excon.defaults` to set a default for all requests, so for a test suite you might do this:
196
-
197
- before(:all) { Excon.defaults[:mock] = true }
198
-
199
- For additional information on stubbing, read the pull request notes [here](https://github.com/geemus/excon/issues/29)
200
-
201
- HTTPS/SSL Issues
202
- ----------------
203
-
204
- By default excon will try to verify peer certificates when using SSL for HTTPS. Unfortunately on some operating systems the defaults will not work. This will likely manifest itself as something like `Excon::Errors::SocketError: SSL_connect returned=1 ...`
205
-
206
- If you have the misfortune of running into this problem you have a couple options. If you have certificates but they aren't being auto-discovered, you can specify the path to your certificates:
205
+ ```ruby
206
+ config.after(:each) do
207
+ Excon.stubs.clear
208
+ end
209
+ ```
207
210
 
208
- Excon.defaults[:ssl_ca_path] = '/path/to/certs'
211
+ You can also modify 'Excon.defaults` to set a stub for all requests, so for a test suite you might do this:
209
212
 
210
- Failing that, you can turn off peer verification (less secure):
211
-
212
- Excon.defaults[:ssl_verify_peer] = false
213
-
214
- Either of these should allow you to work around the socket error and continue with your work.
213
+ ```ruby
214
+ # Mock by default and stub any request as success
215
+ config.before(:all) do
216
+ Excon.defaults[:mock] = true
217
+ Excon.stub({}, {:body => 'Fallback', :status => 200})
218
+ # Add your own stubs here or in specific tests...
219
+ end
220
+ ```
215
221
 
216
222
  Instrumentation
217
223
  ---------------
218
224
 
219
225
  Excon calls can be timed using the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API.
220
226
 
221
- connection = Excon.new('http://geemus.com',
222
- :instrumentor => ActiveSupport::Notifications)
227
+ ```ruby
228
+ connection = Excon.new(
229
+ 'http://geemus.com',
230
+ :instrumentor => ActiveSupport::Notifications
231
+ )
232
+ ```
223
233
 
224
234
  Excon will then instrument each request, retry, and error. The corresponding events are named excon.request, excon.retry, and excon.error respectively.
225
235
 
226
- ActiveSupport::Notifications.subscribe(/excon/) do |*args|
227
- puts "Excon did stuff!"
228
- end
236
+ ```ruby
237
+ ActiveSupport::Notifications.subscribe(/excon/) do |*args|
238
+ puts "Excon did stuff!"
239
+ end
240
+ ```
229
241
 
230
242
  If you prefer to label each event with something other than "excon," you may specify
231
243
  an alternate name in the constructor:
232
244
 
233
- connection = Excon.new('http://geemus.com',
234
- :instrumentor => ActiveSupport::Notifications,
235
- :instrumentor_name => 'my_app')
245
+ ```ruby
246
+ connection = Excon.new(
247
+ 'http://geemus.com',
248
+ :instrumentor => ActiveSupport::Notifications,
249
+ :instrumentor_name => 'my_app'
250
+ )
251
+ ```
236
252
 
237
253
  If you don't want to add activesupport to your application, simply define a class which implements the same #instrument method like so:
238
254
 
239
- class SimpleInstrumentor
240
- class << self
241
- attr_accessor :events
255
+ ```ruby
256
+ class SimpleInstrumentor
257
+ class << self
258
+ attr_accessor :events
242
259
 
243
- def instrument(name, params = {}, &block)
244
- puts "#{name} just happened."
245
- yield if block_given?
246
- end
247
- end
260
+ def instrument(name, params = {}, &block)
261
+ puts "#{name} just happened."
262
+ yield if block_given?
248
263
  end
264
+ end
265
+ end
266
+ ```
249
267
 
250
268
  The #instrument method will be called for each HTTP request, response, retry, and error.
251
269
 
@@ -253,12 +271,31 @@ For debugging purposes you can also use Excon::StandardInstrumentor to output al
253
271
 
254
272
  See [the documentation for ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) for more detail on using the subscription interface. See excon's instrumentation_test.rb for more examples of instrumenting excon.
255
273
 
274
+ HTTPS/SSL Issues
275
+ ----------------
276
+
277
+ By default excon will try to verify peer certificates when using SSL for HTTPS. Unfortunately on some operating systems the defaults will not work. This will likely manifest itself as something like `Excon::Errors::SocketError: SSL_connect returned=1 ...`
278
+
279
+ If you have the misfortune of running into this problem you have a couple options. If you have certificates but they aren't being auto-discovered, you can specify the path to your certificates:
280
+
281
+ ```ruby
282
+ Excon.defaults[:ssl_ca_path] = '/path/to/certs'
283
+ ```
284
+
285
+ Failing that, you can turn off peer verification (less secure):
286
+
287
+ ```ruby
288
+ Excon.defaults[:ssl_verify_peer] = false
289
+ ```
290
+
291
+ Either of these should allow you to work around the socket error and continue with your work.
292
+
256
293
  Copyright
257
294
  ---------
258
295
 
259
296
  (The MIT License)
260
297
 
261
- Copyright (c) 2010-2013 {geemus (Wesley Beary)}[http://github.com/geemus]
298
+ Copyright (c) 2010-2013 [geemus (Wesley Beary)](http://github.com/geemus)
262
299
 
263
300
  Permission is hereby granted, free of charge, to any person obtaining
264
301
  a copy of this software and associated documentation files (the
@@ -1,3 +1,16 @@
1
+ 0.26.0 09/24/2013
2
+ =================
3
+
4
+ add basic decompress middleware
5
+ update readme mocking+stubbing info
6
+ add unstub functionality
7
+ avoid modifying original options in request
8
+ jruby fixes
9
+ misc cleanup/fixes
10
+ encoding/compatibility fixes
11
+ close sockets on error
12
+ warn when both request_block and idempotent are set
13
+
1
14
  0.25.3 07/18/2013
2
15
  =================
3
16
 
@@ -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.25.3'
17
- s.date = '2013-07-18'
16
+ s.version = '0.26.0'
17
+ s.date = '2013-09-24'
18
18
  s.rubyforge_project = 'excon'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -98,10 +98,12 @@ Gem::Specification.new do |s|
98
98
  lib/excon/constants.rb
99
99
  lib/excon/errors.rb
100
100
  lib/excon/middlewares/base.rb
101
+ lib/excon/middlewares/decompress.rb
101
102
  lib/excon/middlewares/expects.rb
102
103
  lib/excon/middlewares/idempotent.rb
103
104
  lib/excon/middlewares/instrumentor.rb
104
105
  lib/excon/middlewares/mock.rb
106
+ lib/excon/middlewares/redirect_follower.rb
105
107
  lib/excon/middlewares/response_parser.rb
106
108
  lib/excon/response.rb
107
109
  lib/excon/socket.rb
@@ -115,14 +117,17 @@ Gem::Specification.new do |s|
115
117
  tests/data/xs
116
118
  tests/errors_tests.rb
117
119
  tests/header_tests.rb
120
+ tests/middlewares/decompress_tests.rb
118
121
  tests/middlewares/idempotent_tests.rb
119
122
  tests/middlewares/instrumentation_tests.rb
120
123
  tests/middlewares/mock_tests.rb
124
+ tests/middlewares/redirect_follower.rb
121
125
  tests/proxy_tests.rb
122
126
  tests/query_string_tests.rb
123
127
  tests/rackups/basic.rb
124
128
  tests/rackups/basic.ru
125
129
  tests/rackups/basic_auth.ru
130
+ tests/rackups/deflater.ru
126
131
  tests/rackups/proxy.ru
127
132
  tests/rackups/query_string.ru
128
133
  tests/rackups/request_headers.ru
@@ -8,6 +8,8 @@ require 'rbconfig'
8
8
  require 'socket'
9
9
  require 'timeout'
10
10
  require 'uri'
11
+ require 'zlib'
12
+ require 'stringio'
11
13
 
12
14
  # Define defaults first so they will be available to other files
13
15
  module Excon
@@ -17,6 +19,7 @@ module Excon
17
19
  def defaults
18
20
  @defaults ||= {
19
21
  :chunk_size => CHUNK_SIZE || DEFAULT_CHUNK_SIZE,
22
+ :ciphers => 'HIGH:!SSLv2:!aNULL:!eNULL:!3DES',
20
23
  :connect_timeout => 60,
21
24
  :debug_request => false,
22
25
  :debug_response => false,
@@ -57,10 +60,12 @@ require 'excon/constants'
57
60
  require 'excon/connection'
58
61
  require 'excon/errors'
59
62
  require 'excon/middlewares/base'
63
+ require 'excon/middlewares/decompress'
60
64
  require 'excon/middlewares/expects'
61
65
  require 'excon/middlewares/idempotent'
62
66
  require 'excon/middlewares/instrumentor'
63
67
  require 'excon/middlewares/mock'
68
+ require 'excon/middlewares/redirect_follower'
64
69
  require 'excon/middlewares/response_parser'
65
70
  require 'excon/response'
66
71
  require 'excon/socket'
@@ -173,11 +178,13 @@ module Excon
173
178
  end
174
179
 
175
180
  # get a stub matching params or nil
181
+ # @param [Hash<Symbol, >] request params to match against, omitted params match all
182
+ # @return [Hash<Symbol, >] response params to return from matched request or block to call with params
176
183
  def stub_for(request_params={})
177
184
  if method = request_params.delete(:method)
178
185
  request_params[:method] = method.to_s.downcase.to_sym
179
186
  end
180
- Excon.stubs.each do |stub, response|
187
+ Excon.stubs.each do |stub, response_params|
181
188
  captures = { :headers => {} }
182
189
  headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
183
190
  case value = stub[:headers][key]
@@ -203,7 +210,7 @@ module Excon
203
210
  end
204
211
  if headers_match && non_headers_match
205
212
  request_params[:captures] = captures
206
- return response
213
+ return [stub, response_params]
207
214
  end
208
215
  end
209
216
  nil
@@ -214,6 +221,14 @@ module Excon
214
221
  @stubs ||= []
215
222
  end
216
223
 
224
+ # remove first/oldest stub matching request_params
225
+ # @param [Hash<Symbol, >] request params to match against, omitted params match all
226
+ # @return [Hash<Symbol, >] response params from deleted stub
227
+ def unstub(request_params = {})
228
+ stub = stub_for(request_params)
229
+ Excon.stubs.delete_at(Excon.stubs.index(stub))
230
+ end
231
+
217
232
  # Generic non-persistent HTTP methods
218
233
  HTTP_VERBS.each do |method|
219
234
  module_eval <<-DEF, __FILE__, __LINE__ + 1
@@ -39,6 +39,7 @@ module Excon
39
39
  # @option params [Fixnum] :port The port on which to connect, to the destination host
40
40
  # @option params [Hash] :query Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request
41
41
  # @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
42
+ # @option params [String] :ciphers Only use the specified SSL/TLS cipher suites; use OpenSSL cipher spec format e.g. 'HIGH:!aNULL:!3DES' or 'AES256-SHA:DES-CBC3-SHA'
42
43
  # @option params [String] :proxy Proxy server; e.g. 'http://myproxy.com:8888'
43
44
  # @option params [Fixnum] :retry_limit Set how many times we'll retry a failed request. (Default 4)
44
45
  # @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
@@ -133,10 +134,16 @@ module Excon
133
134
 
134
135
  if datum.has_key?(:request_block)
135
136
  datum[:headers]['Transfer-Encoding'] = 'chunked'
136
- elsif ! (datum[:method].to_s.casecmp('GET') == 0 && datum[:body].nil?)
137
+ else
138
+ body = datum[:body].is_a?(String) ? StringIO.new(datum[:body]) : datum[:body]
139
+
137
140
  # The HTTP spec isn't clear on it, but specifically, GET requests don't usually send bodies;
138
141
  # if they don't, sending Content-Length:0 can cause issues.
139
- datum[:headers]['Content-Length'] = detect_content_length(datum[:body])
142
+ unless datum[:method].to_s.casecmp('GET') == 0 && body.nil?
143
+ unless datum[:headers].has_key?('Content-Length')
144
+ datum[:headers]['Content-Length'] = detect_content_length(body)
145
+ end
146
+ end
140
147
  end
141
148
 
142
149
  # add headers to request
@@ -148,9 +155,9 @@ module Excon
148
155
 
149
156
  # add additional "\r\n" to indicate end of headers
150
157
  request << CR_NL
158
+ socket.write(request) # write out request + headers
151
159
 
152
160
  if datum.has_key?(:request_block)
153
- socket.write(request) # write out request + headers
154
161
  while true # write out body with chunked encoding
155
162
  chunk = datum[:request_block].call
156
163
  if FORCE_ENC
@@ -163,23 +170,16 @@ module Excon
163
170
  break
164
171
  end
165
172
  end
166
- elsif !datum[:body].nil?
167
- if datum[:body].is_a?(String) # write out string body
168
- socket.write(request << datum[:body]) # write out request + headers + body
169
- else # write out file body
170
- socket.write(request) # write out request + headers
171
- if datum[:body].respond_to?(:binmode)
172
- datum[:body].binmode
173
- end
174
- if datum[:body].respond_to?(:pos=)
175
- datum[:body].pos = 0
176
- end
177
- while chunk = datum[:body].read(datum[:chunk_size])
178
- socket.write(chunk)
179
- end
173
+ elsif !body.nil? # write out body
174
+ if body.respond_to?(:binmode)
175
+ body.binmode
176
+ end
177
+ if body.respond_to?(:pos=)
178
+ body.pos = 0
179
+ end
180
+ while chunk = body.read(datum[:chunk_size])
181
+ socket.write(chunk)
180
182
  end
181
- else # write out nil body
182
- socket.write(request) # write out request + headers
183
183
  end
184
184
  end
185
185
  rescue => error
@@ -215,7 +215,7 @@ module Excon
215
215
  # @option params [Fixnum] :port The port on which to connect, to the destination host
216
216
  # @option params [Hash] :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
217
217
  # @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
218
- def request(params, &block)
218
+ def request(params={}, &block)
219
219
  # @data has defaults, merge in new params to override
220
220
  datum = @data.merge(params)
221
221
  invalid_keys_warning(params, VALID_CONNECTION_KEYS)
@@ -226,7 +226,7 @@ module Excon
226
226
 
227
227
  # if path is empty or doesn't start with '/', insert one
228
228
  unless datum[:path][0, 1] == '/'
229
- datum[:path].insert(0, '/')
229
+ datum[:path] = datum[:path].dup.insert(0, '/')
230
230
  end
231
231
 
232
232
  if block_given?
@@ -234,6 +234,11 @@ module Excon
234
234
  datum[:response_block] = Proc.new
235
235
  end
236
236
 
237
+ if datum[:request_block] && datum[:idempotent]
238
+ Excon.display_warning("Excon requests with a :request_block can not be :idempotent (#{caller.first})")
239
+ datum[:idempotent] = false
240
+ end
241
+
237
242
  datum[:connection] = self
238
243
 
239
244
  datum[:stack] = datum[:middlewares].map do |middleware|
@@ -255,6 +260,7 @@ module Excon
255
260
  datum
256
261
  end
257
262
  rescue => error
263
+ reset
258
264
  datum[:error] = error
259
265
  if datum[:stack]
260
266
  datum[:stack].error_call(datum)
@@ -321,20 +327,14 @@ module Excon
321
327
  private
322
328
 
323
329
  def detect_content_length(body)
324
- if body.is_a?(String)
325
- if FORCE_ENC
326
- body.force_encoding('BINARY')
327
- end
328
- body.length
329
- elsif body.respond_to?(:size)
330
- # IO object: File, Tempfile, etc.
330
+ if body.respond_to?(:size)
331
+ # IO object: File, Tempfile, StringIO, etc.
331
332
  body.size
333
+ elsif body.respond_to?(:stat)
334
+ # for 1.8.7 where file does not have size
335
+ body.stat.size
332
336
  else
333
- begin
334
- File.size(body) # for 1.8.7 where file does not have size
335
- rescue
336
- 0
337
- end
337
+ 0
338
338
  end
339
339
  end
340
340
 
@@ -77,7 +77,7 @@ module Excon
77
77
  :write_timeout
78
78
  ]
79
79
 
80
- VERSION = '0.25.3'
80
+ VERSION = '0.26.0'
81
81
  USER_AGENT = 'excon/' << VERSION
82
82
 
83
83
  unless ::IO.const_defined?(:WaitReadable)
@@ -0,0 +1,18 @@
1
+ module Excon
2
+ module Middleware
3
+ class Decompress < Excon::Middleware::Base
4
+ def response_call(datum)
5
+ unless datum.has_key?(:response_block)
6
+ case datum[:response][:headers]['Content-Encoding']
7
+ when 'deflate'
8
+ # assume inflate omits header
9
+ datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(datum[:response][:body])
10
+ when 'gzip'
11
+ datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(datum[:response][:body])).read
12
+ end
13
+ end
14
+ @stack.response_call(datum)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -6,7 +6,6 @@ module Excon
6
6
  Excon::Errors::HTTPStatusError].any? {|ex| datum[:error].kind_of?(ex) } && datum[:retries_remaining] > 1
7
7
  # reduces remaining retries, reset connection, and restart request_call
8
8
  datum[:retries_remaining] -= 1
9
- datum[:connection].reset
10
9
  datum.delete(:response)
11
10
  datum.delete(:error)
12
11
  datum[:connection].request(datum)
@@ -14,7 +14,7 @@ module Excon
14
14
  datum[:body] = datum[:body].read
15
15
  end
16
16
 
17
- if response = Excon.stub_for(datum)
17
+ if stub = Excon.stub_for(datum)
18
18
  datum[:response] = {
19
19
  :body => '',
20
20
  :headers => {},
@@ -22,11 +22,11 @@ module Excon
22
22
  :remote_ip => '127.0.0.1'
23
23
  }
24
24
 
25
- stub_datum = case response
25
+ stub_datum = case stub.last
26
26
  when Proc
27
- response.call(datum)
27
+ stub.last.call(datum)
28
28
  else
29
- response
29
+ stub.last
30
30
  end
31
31
 
32
32
  datum[:response].merge!(stub_datum.reject {|key,value| key == :headers})
@@ -0,0 +1,45 @@
1
+ module Excon
2
+ module Middleware
3
+ class RedirectFollower < Excon::Middleware::Base
4
+ def response_call(datum)
5
+ if datum.has_key?(:response) && [:get, :head].include?(datum[:method].to_s.downcase.to_sym)
6
+ case datum[:response][:status]
7
+ when 301, 302, 303, 307
8
+ uri_parser = datum[:uri_parser] || Excon.defaults[:uri_parser]
9
+ _, location = datum[:response][:headers].detect do |key, value|
10
+ key.casecmp('Location') == 0
11
+ end
12
+ uri = uri_parser.parse(location)
13
+
14
+ port_string = if datum[:omit_default_port] && ((uri.scheme.casecmp('http') == 0 && uri.port.to_i == 80) || (uri.scheme.casecmp('https') == 0 && uri.port.to_i == 443))
15
+ ''
16
+ else
17
+ ':' << uri.port.to_s
18
+ end
19
+
20
+ # delete old/redirect response
21
+ datum.delete(:response)
22
+
23
+ response = datum[:connection].request(
24
+ datum.merge!(
25
+ :headers => (datum[:headers] || {}).merge({'Host' => '' << uri.host << port_string}),
26
+ :host => uri.host,
27
+ :path => uri.path,
28
+ :port => uri.port,
29
+ :query => uri.query,
30
+ :scheme => uri.scheme,
31
+ :user => (URI.decode(uri.user) if uri.user),
32
+ :password => (URI.decode(uri.password) if uri.password)
33
+ )
34
+ )
35
+ datum.merge!({:response => response.data})
36
+ else
37
+ @stack.response_call(datum)
38
+ end
39
+ else
40
+ @stack.response_call(datum)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -84,47 +84,35 @@ module Excon
84
84
 
85
85
  def write(data)
86
86
  if @data[:nonblock]
87
- # We normally return from the return in the else block below, but
88
- # we guard that data is still something in case we get weird
89
- # values and String#[] returns nil. (This behavior has been observed
90
- # in the wild, so this is a simple defensive mechanism)
91
- while data
87
+ if FORCE_ENC
88
+ data.force_encoding('BINARY')
89
+ end
90
+ while true
91
+ written = nil
92
92
  begin
93
93
  # I wish that this API accepted a start position, then we wouldn't
94
94
  # have to slice data when there is a short write.
95
95
  written = @socket.write_nonblock(data)
96
- rescue OpenSSL::SSL::SSLError => error
97
- if error.message == 'write would block'
96
+ rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => error
97
+ if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
98
+ raise error
99
+ else
98
100
  if IO.select(nil, [@socket], nil, @data[:write_timeout])
99
101
  retry
100
102
  else
101
- raise(Excon::Errors::Timeout.new("write timeout reached"))
103
+ raise Excon::Errors::Timeout.new('write timeout reached')
102
104
  end
103
- else
104
- raise(error)
105
105
  end
106
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable
107
- if IO.select(nil, [@socket], nil, @data[:write_timeout])
108
- retry
109
- else
110
- raise(Excon::Errors::Timeout.new("write timeout reached"))
111
- end
112
- else
113
- # Fast, common case.
114
- # The >= seems weird, why would it have written MORE than we
115
- # requested. But we're getting some weird behavior when @socket
116
- # is an OpenSSL socket, where it seems like it's saying it wrote
117
- # more (perhaps due to SSL packet overhead?).
118
- #
119
- # Pretty weird, but this is a simple defensive mechanism.
120
- return if written >= data.size
121
-
122
- # This takes advantage of the fact that most ruby implementations
123
- # have Copy-On-Write strings. Thusly why requesting a subrange
124
- # of data, we actually don't copy data because the new string
125
- # simply references a subrange of the original.
126
- data = data[written, data.size]
127
106
  end
107
+
108
+ # Fast, common case.
109
+ break if written == data.size
110
+
111
+ # This takes advantage of the fact that most ruby implementations
112
+ # have Copy-On-Write strings. Thusly why requesting a subrange
113
+ # of data, we actually don't copy data because the new string
114
+ # simply references a subrange of the original.
115
+ data = data[written, data.size]
128
116
  end
129
117
  else
130
118
  begin
@@ -9,7 +9,8 @@ module Excon
9
9
 
10
10
  # create ssl context
11
11
  ssl_context = OpenSSL::SSL::SSLContext.new
12
-
12
+ ssl_context.ciphers = @data[:ciphers]
13
+
13
14
  if @data[:ssl_verify_peer]
14
15
  # turn verification on
15
16
  ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
@@ -21,7 +22,16 @@ module Excon
21
22
  else # attempt default, fallback to bundled
22
23
  ssl_context.cert_store = OpenSSL::X509::Store.new
23
24
  ssl_context.cert_store.set_default_paths
24
- ssl_context.cert_store.add_file(DEFAULT_CA_FILE)
25
+
26
+ # workaround issue #257 (JRUBY-6970)
27
+ ca_file = DEFAULT_CA_FILE
28
+ ca_file.gsub!(/^jar:/, "") if ca_file =~ /^jar:file:\//
29
+
30
+ begin
31
+ ssl_context.cert_store.add_file(ca_file)
32
+ rescue => e
33
+ Excon.display_warning("Excon unable to add file to cert store, ignoring: #{ca_file}\n[#{e.class}] #{e.message}\n#{e.backtrace.join("\n")}")
34
+ end
25
35
  end
26
36
  else
27
37
  # turn verification off
@@ -67,7 +67,8 @@ with_rackup('ssl_verify_peer.ru') do
67
67
 
68
68
  basic_tests('https://127.0.0.1:8443',
69
69
  :client_key => File.join(File.dirname(__FILE__), 'data', 'excon.cert.key'),
70
- :client_cert => File.join(File.dirname(__FILE__), 'data', 'excon.cert.crt')
70
+ :client_cert => File.join(File.dirname(__FILE__), 'data', 'excon.cert.crt'),
71
+ :reset_connection => RUBY_VERSION == '1.9.2'
71
72
  )
72
73
 
73
74
  end
@@ -0,0 +1,24 @@
1
+ Shindo.tests('Excon decompression support') do
2
+ env_init
3
+
4
+ with_rackup('deflater.ru') do
5
+ connection = Excon.new(
6
+ 'http://127.0.0.1:9292/echo',
7
+ :body => 'x' * 100,
8
+ :method => :post,
9
+ :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]
10
+ )
11
+
12
+ tests('deflate').returns('x' * 100) do
13
+ response = connection.request(:headers => { 'Accept-Encoding' => 'deflate' })
14
+ response.body
15
+ end
16
+
17
+ tests('gzip').returns('x' * 100) do
18
+ response = connection.request(:headers => { 'Accept-Encoding' => 'gzip' })
19
+ response.body
20
+ end
21
+ end
22
+
23
+ env_restore
24
+ end
@@ -193,6 +193,32 @@ Shindo.tests('Excon stubs') do
193
193
 
194
194
  end
195
195
 
196
+ tests("stub_for({})") do
197
+ connection = Excon.new('http://127.0.0.1:9292', :mock => true)
198
+ Excon.stub({}, {})
199
+
200
+ tests("stub_for({})").returns([{}, {}]) do
201
+ Excon.stub_for({})
202
+ end
203
+
204
+ Excon.stubs.clear
205
+ end
206
+
207
+ tests("unstub({})") do
208
+ connection = Excon.new('http://127.0.0.1:9292', :mock => true)
209
+ Excon.stub({}, {})
210
+
211
+ tests("unstub({})").returns([{}, {}]) do
212
+ Excon.unstub({})
213
+ end
214
+
215
+ tests("request(:method => :get)").raises(Excon::Errors::StubNotFound) do
216
+ connection.request(:method => :get)
217
+ end
218
+
219
+ Excon.stubs.clear
220
+ end
221
+
196
222
  tests('mock = false') do
197
223
  with_rackup('basic.ru') do
198
224
  basic_tests
@@ -0,0 +1,32 @@
1
+ Shindo.tests('Excon redirector support') do
2
+ env_init
3
+
4
+ connection = Excon.new(
5
+ 'http://127.0.0.1:9292',
6
+ :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower],
7
+ :mock => true
8
+ )
9
+
10
+ Excon.stub(
11
+ { :path => '/old' },
12
+ {
13
+ :headers => { 'Location' => 'http://127.0.0.1:9292/new' },
14
+ :body => 'old',
15
+ :status => 301
16
+ }
17
+ )
18
+
19
+ Excon.stub(
20
+ { :path => '/new' },
21
+ {
22
+ :body => 'new',
23
+ :status => 200
24
+ }
25
+ )
26
+
27
+ tests("request(:method => :get, :path => '/old').body").returns('new') do
28
+ connection.request(:method => :get, :path => '/old').body
29
+ end
30
+
31
+ env_restore
32
+ end
@@ -0,0 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), 'basic')
2
+
3
+ use Rack::Deflater
4
+ run Basic
@@ -34,6 +34,12 @@ Shindo.tests('Excon request methods') do
34
34
  connection.delete.body
35
35
  end
36
36
 
37
+ tests('not modifies path argument').returns('path') do
38
+ path = 'path'
39
+ connection.get(:path => path)
40
+ path
41
+ end
42
+
37
43
  end
38
44
 
39
45
  end
@@ -3,9 +3,8 @@ require 'bundler'
3
3
 
4
4
  Bundler.require(:default, :development)
5
5
 
6
- require 'stringio'
7
-
8
6
  def basic_tests(url = 'http://127.0.0.1:9292', options = {})
7
+ reset_connection = !!options.delete(:reset_connection)
9
8
  [false, true].each do |nonblock|
10
9
  options = options.merge({:ssl_verify_peer => false, :nonblock => nonblock })
11
10
  connection = Excon.new(url, options)
@@ -77,6 +76,9 @@ def basic_tests(url = 'http://127.0.0.1:9292', options = {})
77
76
  tests('POST /body-sink') do
78
77
 
79
78
  tests('response.body').returns("5000000") do
79
+ if reset_connection && !nonblock
80
+ connection.reset
81
+ end
80
82
  response = connection.request(:method => :post, :path => '/body-sink', :headers => { 'Content-Type' => 'text/plain' }, :body => 'x' * 5_000_000)
81
83
  response.body
82
84
  end
@@ -110,6 +112,20 @@ def basic_tests(url = 'http://127.0.0.1:9292', options = {})
110
112
  response.body
111
113
  end
112
114
 
115
+ tests('with multi-byte strings') do
116
+ body = "\xC3\xBC" * 100
117
+ headers = { 'Custom' => body.dup }
118
+ if RUBY_VERSION >= '1.9'
119
+ body.force_encoding('BINARY')
120
+ headers['Custom'].force_encoding('UTF-8')
121
+ end
122
+
123
+ returns(body, 'properly concatenates request+headers and body') do
124
+ response = connection.request(:method => :post, :path => '/echo', :headers => headers, :body => body)
125
+ response.body
126
+ end
127
+ end
128
+
113
129
  end
114
130
 
115
131
  tests('PUT /echo') do
@@ -134,6 +150,20 @@ def basic_tests(url = 'http://127.0.0.1:9292', options = {})
134
150
  response.body
135
151
  end
136
152
 
153
+ tests('with multi-byte strings') do
154
+ body = "\xC3\xBC" * 100
155
+ headers = { 'Custom' => body.dup }
156
+ if RUBY_VERSION >= '1.9'
157
+ body.force_encoding('BINARY')
158
+ headers['Custom'].force_encoding('UTF-8')
159
+ end
160
+
161
+ returns(body, 'properly concatenates request+headers and body') do
162
+ response = connection.request(:method => :put, :path => '/echo', :headers => headers, :body => body)
163
+ response.body
164
+ end
165
+ end
166
+
137
167
  end
138
168
 
139
169
  end
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.25.3
4
+ version: 0.26.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-07-18 00:00:00.000000000 Z
14
+ date: 2013-09-24 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -178,10 +178,12 @@ files:
178
178
  - lib/excon/constants.rb
179
179
  - lib/excon/errors.rb
180
180
  - lib/excon/middlewares/base.rb
181
+ - lib/excon/middlewares/decompress.rb
181
182
  - lib/excon/middlewares/expects.rb
182
183
  - lib/excon/middlewares/idempotent.rb
183
184
  - lib/excon/middlewares/instrumentor.rb
184
185
  - lib/excon/middlewares/mock.rb
186
+ - lib/excon/middlewares/redirect_follower.rb
185
187
  - lib/excon/middlewares/response_parser.rb
186
188
  - lib/excon/response.rb
187
189
  - lib/excon/socket.rb
@@ -195,14 +197,17 @@ files:
195
197
  - tests/data/xs
196
198
  - tests/errors_tests.rb
197
199
  - tests/header_tests.rb
200
+ - tests/middlewares/decompress_tests.rb
198
201
  - tests/middlewares/idempotent_tests.rb
199
202
  - tests/middlewares/instrumentation_tests.rb
200
203
  - tests/middlewares/mock_tests.rb
204
+ - tests/middlewares/redirect_follower.rb
201
205
  - tests/proxy_tests.rb
202
206
  - tests/query_string_tests.rb
203
207
  - tests/rackups/basic.rb
204
208
  - tests/rackups/basic.ru
205
209
  - tests/rackups/basic_auth.ru
210
+ - tests/rackups/deflater.ru
206
211
  - tests/rackups/proxy.ru
207
212
  - tests/rackups/query_string.ru
208
213
  - tests/rackups/request_headers.ru
@@ -237,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
237
242
  version: '0'
238
243
  segments:
239
244
  - 0
240
- hash: -1555854800829677870
245
+ hash: -2550839664096284654
241
246
  required_rubygems_version: !ruby/object:Gem::Requirement
242
247
  none: false
243
248
  requirements: