excon 0.17.0 → 0.18.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.
- data/Gemfile.lock +2 -1
- data/README.md +8 -0
- data/changelog.txt +17 -0
- data/excon.gemspec +6 -3
- data/lib/excon.rb +8 -1
- data/lib/excon/connection.rb +129 -98
- data/lib/excon/constants.rb +1 -1
- data/lib/excon/errors.rb +5 -3
- data/lib/excon/middlewares/base.rb +19 -0
- data/lib/excon/middlewares/expects.rb +10 -11
- data/lib/excon/middlewares/instrumentor.rb +12 -11
- data/lib/excon/middlewares/mock.rb +84 -0
- data/lib/excon/response.rb +14 -77
- data/tests/{stub_tests.rb → middlewares/mock_tests.rb} +1 -1
- data/tests/proxy_tests.rb +8 -8
- data/tests/requests_tests.rb +31 -0
- data/tests/test_helper.rb +4 -0
- metadata +8 -5
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
excon (0.
|
4
|
+
excon (0.18.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
@@ -14,6 +14,7 @@ GEM
|
|
14
14
|
delorean (2.0.0)
|
15
15
|
chronic
|
16
16
|
eventmachine (1.0.0)
|
17
|
+
eventmachine (1.0.0-java)
|
17
18
|
formatador (0.2.3)
|
18
19
|
i18n (0.6.0)
|
19
20
|
jruby-openssl (0.7.7)
|
data/README.md
CHANGED
@@ -100,6 +100,14 @@ You can make `Transfer-Encoding: chunked` requests by passing a block that will
|
|
100
100
|
|
101
101
|
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.
|
102
102
|
|
103
|
+
Pipelining Requests
|
104
|
+
------------------
|
105
|
+
|
106
|
+
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.
|
107
|
+
|
108
|
+
connection = Excon.new('http://geemus.com/')
|
109
|
+
connection.requests([{:method => :get, :method => :get}])
|
110
|
+
|
103
111
|
Streaming Responses
|
104
112
|
-------------------
|
105
113
|
|
data/changelog.txt
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
0.18.0 02/21/2013
|
2
|
+
=================
|
3
|
+
|
4
|
+
more refactoring around middlewares
|
5
|
+
add pipelining capabilities
|
6
|
+
allow [] style access to response attributes
|
7
|
+
|
8
|
+
|
9
|
+
0.17.0 02/01/2013
|
10
|
+
=================
|
11
|
+
|
12
|
+
add patch method
|
13
|
+
allow passing family for addresses to params/proxy
|
14
|
+
more consistent empty header passing
|
15
|
+
nicer debug output
|
16
|
+
internal refactoring toward middleware pattern
|
17
|
+
|
1
18
|
0.16.10 11/16/2012
|
2
19
|
==================
|
3
20
|
|
data/excon.gemspec
CHANGED
@@ -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.
|
17
|
-
s.date = '2013-02-
|
16
|
+
s.version = '0.18.0'
|
17
|
+
s.date = '2013-02-21'
|
18
18
|
s.rubyforge_project = 'excon'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -96,8 +96,10 @@ Gem::Specification.new do |s|
|
|
96
96
|
lib/excon/connection.rb
|
97
97
|
lib/excon/constants.rb
|
98
98
|
lib/excon/errors.rb
|
99
|
+
lib/excon/middlewares/base.rb
|
99
100
|
lib/excon/middlewares/expects.rb
|
100
101
|
lib/excon/middlewares/instrumentor.rb
|
102
|
+
lib/excon/middlewares/mock.rb
|
101
103
|
lib/excon/response.rb
|
102
104
|
lib/excon/socket.rb
|
103
105
|
lib/excon/ssl_socket.rb
|
@@ -109,6 +111,7 @@ Gem::Specification.new do |s|
|
|
109
111
|
tests/header_tests.rb
|
110
112
|
tests/idempotent_tests.rb
|
111
113
|
tests/middlewares/instrumentation_tests.rb
|
114
|
+
tests/middlewares/mock_tests.rb
|
112
115
|
tests/proxy_tests.rb
|
113
116
|
tests/query_string_tests.rb
|
114
117
|
tests/rackups/basic.rb
|
@@ -124,9 +127,9 @@ Gem::Specification.new do |s|
|
|
124
127
|
tests/rackups/timeout.ru
|
125
128
|
tests/request_headers_tests.rb
|
126
129
|
tests/request_method_tests.rb
|
130
|
+
tests/requests_tests.rb
|
127
131
|
tests/servers/bad.rb
|
128
132
|
tests/servers/eof.rb
|
129
|
-
tests/stub_tests.rb
|
130
133
|
tests/test_helper.rb
|
131
134
|
tests/thread_safety_tests.rb
|
132
135
|
tests/timeout_tests.rb
|
data/lib/excon.rb
CHANGED
@@ -21,6 +21,11 @@ module Excon
|
|
21
21
|
:headers => {},
|
22
22
|
:idempotent => false,
|
23
23
|
:instrumentor_name => 'excon',
|
24
|
+
:middlewares => [
|
25
|
+
Excon::Middleware::Instrumentor,
|
26
|
+
Excon::Middleware::Expects,
|
27
|
+
Excon::Middleware::Mock
|
28
|
+
],
|
24
29
|
:mock => false,
|
25
30
|
:nonblock => DEFAULT_NONBLOCK,
|
26
31
|
:read_timeout => 60,
|
@@ -43,8 +48,10 @@ end
|
|
43
48
|
require 'excon/constants'
|
44
49
|
require 'excon/connection'
|
45
50
|
require 'excon/errors'
|
51
|
+
require 'excon/middlewares/base'
|
46
52
|
require 'excon/middlewares/expects'
|
47
53
|
require 'excon/middlewares/instrumentor'
|
54
|
+
require 'excon/middlewares/mock'
|
48
55
|
require 'excon/response'
|
49
56
|
require 'excon/socket'
|
50
57
|
require 'excon/ssl_socket'
|
@@ -111,7 +118,7 @@ module Excon
|
|
111
118
|
request_params.update(
|
112
119
|
:host => uri.host,
|
113
120
|
:path => uri.path,
|
114
|
-
:port => uri.port,
|
121
|
+
:port => uri.port.to_s,
|
115
122
|
:query => uri.query,
|
116
123
|
:scheme => uri.scheme
|
117
124
|
)
|
data/lib/excon/connection.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
module Excon
|
2
2
|
class Connection
|
3
|
-
|
3
|
+
VALID_CONNECTION_KEYS = [:body, :headers, :host, :host_port, :path, :port, :query, :scheme, :user, :password,
|
4
|
+
:instrumentor, :instrumentor_name, :ssl_ca_file, :ssl_verify_peer, :chunk_size,
|
5
|
+
:nonblock, :retry_limit, :connect_timeout, :read_timeout, :write_timeout, :captures,
|
6
|
+
:exception, :expects, :mock, :proxy, :method, :idempotent, :request_block, :response_block,
|
7
|
+
:middlewares, :retries_remaining, :connection, :stack, :response, :pipeline]
|
4
8
|
attr_reader :data
|
5
9
|
|
6
10
|
def params
|
@@ -21,6 +25,13 @@ module Excon
|
|
21
25
|
@data[:proxy] = new_proxy
|
22
26
|
end
|
23
27
|
|
28
|
+
def assert_valid_keys_for_argument!(argument, valid_keys)
|
29
|
+
invalid_keys = argument.keys - valid_keys
|
30
|
+
return true if invalid_keys.empty?
|
31
|
+
raise ArgumentError, "The following keys are invalid: #{invalid_keys.map(&:inspect).join(', ')}"
|
32
|
+
end
|
33
|
+
private :assert_valid_keys_for_argument!
|
34
|
+
|
24
35
|
# Initializes a new Connection instance
|
25
36
|
# @param [String] url The destination URL
|
26
37
|
# @param [Hash<Symbol, >] params One or more optional params
|
@@ -36,14 +47,16 @@ module Excon
|
|
36
47
|
# @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
|
37
48
|
# @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
|
38
49
|
def initialize(url, params = {})
|
50
|
+
assert_valid_keys_for_argument!(params, VALID_CONNECTION_KEYS)
|
39
51
|
uri = URI.parse(url)
|
40
52
|
@data = Excon.defaults.merge({
|
41
53
|
:host => uri.host,
|
42
|
-
:host_port => '' << uri.host << ':' << uri.port.to_s,
|
43
54
|
:path => uri.path,
|
44
|
-
:port => uri.port,
|
55
|
+
:port => uri.port.to_s,
|
45
56
|
:query => uri.query,
|
46
57
|
:scheme => uri.scheme,
|
58
|
+
:user => (URI.decode(uri.user) if uri.user),
|
59
|
+
:password => (URI.decode(uri.password) if uri.password),
|
47
60
|
}).merge!(params)
|
48
61
|
# merge does not deep-dup, so make sure headers is not the original
|
49
62
|
@data[:headers] = @data[:headers].dup
|
@@ -74,20 +87,21 @@ module Excon
|
|
74
87
|
@data[:headers]['Authorization'] ||= 'Basic ' << ['' << uri.user.to_s << ':' << uri.password.to_s].pack('m').delete(Excon::CR_NL)
|
75
88
|
end
|
76
89
|
|
77
|
-
@socket_key = '' << @data[:
|
90
|
+
@socket_key = '' << @data[:host] << ':' << @data[:port]
|
78
91
|
reset
|
79
92
|
end
|
80
93
|
|
81
|
-
def
|
94
|
+
def request_call(datum)
|
82
95
|
begin
|
83
|
-
|
84
|
-
|
96
|
+
if datum.has_key?(:response)
|
97
|
+
# we already have data from a middleware, so bail
|
98
|
+
return datum
|
85
99
|
else
|
86
100
|
socket.data = datum
|
87
101
|
# start with "METHOD /path"
|
88
102
|
request = datum[:method].to_s.upcase << ' '
|
89
103
|
if @data[:proxy]
|
90
|
-
request << datum[:scheme] << '://' <<
|
104
|
+
request << datum[:scheme] << '://' << @data[:host] << ':' << @data[:port].to_s
|
91
105
|
end
|
92
106
|
request << datum[:path]
|
93
107
|
|
@@ -164,24 +178,21 @@ module Excon
|
|
164
178
|
end
|
165
179
|
end
|
166
180
|
end
|
167
|
-
|
168
|
-
# read the response
|
169
|
-
response_datum = Excon::Response.parse(socket, datum)
|
170
|
-
|
171
|
-
if response_datum[:headers]['Connection'] == 'close'
|
172
|
-
reset
|
173
|
-
end
|
174
|
-
|
175
|
-
response_datum
|
176
181
|
end
|
177
|
-
rescue
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
+
rescue => error
|
183
|
+
case error
|
184
|
+
when Excon::Errors::StubNotFound, Excon::Errors::Timeout
|
185
|
+
raise(error)
|
186
|
+
else
|
187
|
+
raise(Excon::Errors::SocketError.new(error))
|
188
|
+
end
|
182
189
|
end
|
183
190
|
|
184
|
-
|
191
|
+
datum
|
192
|
+
end
|
193
|
+
|
194
|
+
def response_call(datum)
|
195
|
+
datum
|
185
196
|
end
|
186
197
|
|
187
198
|
# Sends the supplied request to the destination host.
|
@@ -197,9 +208,9 @@ module Excon
|
|
197
208
|
def request(params, &block)
|
198
209
|
# @data has defaults, merge in new params to override
|
199
210
|
datum = @data.merge(params)
|
200
|
-
|
211
|
+
assert_valid_keys_for_argument!(params, VALID_CONNECTION_KEYS)
|
201
212
|
datum[:headers] = @data[:headers].merge(datum[:headers] || {})
|
202
|
-
datum[:headers]['Host']
|
213
|
+
datum[:headers]['Host'] ||= '' << datum[:host] << ':' << datum[:port]
|
203
214
|
datum[:retries_remaining] ||= datum[:retry_limit]
|
204
215
|
|
205
216
|
# if path is empty or doesn't start with '/', insert one
|
@@ -212,37 +223,50 @@ module Excon
|
|
212
223
|
datum[:response_block] = Proc.new
|
213
224
|
end
|
214
225
|
|
215
|
-
datum[:
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
226
|
+
datum[:connection] = self
|
227
|
+
|
228
|
+
datum[:stack] = datum[:middlewares].map do |middleware|
|
229
|
+
lambda {|stack| middleware.new(stack)}
|
230
|
+
end.reverse.inject(self) do |middlewares, middleware|
|
220
231
|
middleware.call(middlewares)
|
221
232
|
end
|
222
|
-
|
233
|
+
datum = datum[:stack].request_call(datum)
|
234
|
+
|
235
|
+
unless datum[:pipeline]
|
236
|
+
datum = response(datum)
|
237
|
+
|
238
|
+
if datum[:response][:headers]['Connection'] == 'close'
|
239
|
+
reset
|
240
|
+
end
|
223
241
|
|
224
|
-
|
242
|
+
Excon::Response.new(datum[:response])
|
243
|
+
else
|
244
|
+
datum
|
245
|
+
end
|
225
246
|
rescue => request_error
|
247
|
+
reset
|
226
248
|
if datum[:idempotent] && [Excon::Errors::Timeout, Excon::Errors::SocketError,
|
227
|
-
Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex }
|
249
|
+
Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex } && datum[:retries_remaining] > 1
|
228
250
|
datum[:retries_remaining] -= 1
|
229
|
-
|
230
|
-
request(datum, &block)
|
231
|
-
else
|
232
|
-
if datum.has_key?(:instrumentor)
|
233
|
-
datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => request_error)
|
234
|
-
end
|
235
|
-
raise(request_error)
|
236
|
-
end
|
251
|
+
request(datum, &block)
|
237
252
|
else
|
238
253
|
if datum.has_key?(:instrumentor)
|
239
254
|
datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => request_error)
|
240
255
|
end
|
241
|
-
reset
|
242
256
|
raise(request_error)
|
243
257
|
end
|
244
258
|
end
|
245
259
|
|
260
|
+
# Sends the supplied requests to the destination host using pipelining.
|
261
|
+
# @pipeline_params [Array<Hash>] pipeline_params An array of one or more optional params, override defaults set in Connection.new, see #request for details
|
262
|
+
def requests(pipeline_params)
|
263
|
+
pipeline_params.map do |params|
|
264
|
+
request(params.merge!(:pipeline => true))
|
265
|
+
end.map do |datum|
|
266
|
+
Excon::Response.new(response(datum)[:response])
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
246
270
|
def reset
|
247
271
|
(old_socket = sockets.delete(@socket_key)) && old_socket.close
|
248
272
|
end
|
@@ -304,67 +328,75 @@ module Excon
|
|
304
328
|
end
|
305
329
|
end
|
306
330
|
|
307
|
-
def
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
if match = value.match(datum[:headers][key])
|
325
|
-
datum[:captures][:headers][key] = match.captures
|
326
|
-
end
|
327
|
-
match
|
328
|
-
else
|
329
|
-
value == datum[:headers][key]
|
330
|
-
end
|
331
|
-
end
|
332
|
-
non_headers_match = (stub.keys - [:headers]).all? do |key|
|
333
|
-
case value = stub[key]
|
334
|
-
when Regexp
|
335
|
-
if match = value.match(datum[key])
|
336
|
-
datum[:captures][key] = match.captures
|
337
|
-
end
|
338
|
-
match
|
339
|
-
else
|
340
|
-
value == datum[key]
|
331
|
+
def response(datum={})
|
332
|
+
unless datum.has_key?(:response)
|
333
|
+
datum[:response] = {
|
334
|
+
:body => '',
|
335
|
+
:headers => {},
|
336
|
+
:status => socket.read(12)[9, 11].to_i,
|
337
|
+
:remote_ip => socket.remote_ip
|
338
|
+
}
|
339
|
+
socket.readline # read the rest of the status line and CRLF
|
340
|
+
|
341
|
+
until ((data = socket.readline).chop!).empty?
|
342
|
+
key, value = data.split(/:\s*/, 2)
|
343
|
+
datum[:response][:headers][key] = ([*datum[:response][:headers][key]] << value).compact.join(', ')
|
344
|
+
if key.casecmp('Content-Length') == 0
|
345
|
+
content_length = value.to_i
|
346
|
+
elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
|
347
|
+
transfer_encoding_chunked = true
|
341
348
|
end
|
342
349
|
end
|
343
|
-
if headers_match && non_headers_match
|
344
|
-
response_datum = case response
|
345
|
-
when Proc
|
346
|
-
response.call(datum)
|
347
|
-
else
|
348
|
-
response
|
349
|
-
end
|
350
350
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
351
|
+
unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])
|
352
|
+
|
353
|
+
# check to see if expects was set and matched
|
354
|
+
expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(datum[:response][:status])
|
355
|
+
|
356
|
+
# if expects matched and there is a block, use it
|
357
|
+
if expected_status && datum.has_key?(:response_block)
|
358
|
+
if transfer_encoding_chunked
|
359
|
+
# 2 == "/r/n".length
|
360
|
+
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
361
|
+
datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
|
362
|
+
end
|
363
|
+
socket.read(2)
|
364
|
+
elsif remaining = content_length
|
365
|
+
while remaining > 0
|
366
|
+
datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
|
367
|
+
remaining -= datum[:chunk_size]
|
368
|
+
end
|
369
|
+
else
|
370
|
+
while remaining = socket.read(datum[:chunk_size])
|
371
|
+
datum[:response_block].call(remaining, remaining.length, content_length)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
else # no block or unexpected status
|
375
|
+
if transfer_encoding_chunked
|
376
|
+
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
377
|
+
datum[:response][:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length
|
378
|
+
end
|
379
|
+
socket.read(2) # 2 == "/r/n".length
|
380
|
+
elsif remaining = content_length
|
381
|
+
while remaining > 0
|
382
|
+
datum[:response][:body] << socket.read([datum[:chunk_size], remaining].min)
|
383
|
+
remaining -= datum[:chunk_size]
|
384
|
+
end
|
385
|
+
else
|
386
|
+
datum[:response][:body] << socket.read
|
361
387
|
end
|
362
388
|
end
|
363
|
-
return response_datum
|
364
389
|
end
|
365
390
|
end
|
366
|
-
|
367
|
-
|
391
|
+
|
392
|
+
datum[:stack].response_call(datum)
|
393
|
+
rescue => error
|
394
|
+
case error
|
395
|
+
when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout
|
396
|
+
raise(error)
|
397
|
+
else
|
398
|
+
raise(Excon::Errors::SocketError.new(error))
|
399
|
+
end
|
368
400
|
end
|
369
401
|
|
370
402
|
def socket
|
@@ -388,9 +420,8 @@ module Excon
|
|
388
420
|
end
|
389
421
|
{
|
390
422
|
:host => uri.host,
|
391
|
-
:host_port => '' << uri.host << ':' << uri.port.to_s,
|
392
423
|
:password => uri.password,
|
393
|
-
:port => uri.port,
|
424
|
+
:port => uri.port.to_s,
|
394
425
|
:scheme => uri.scheme,
|
395
426
|
:user => uri.user
|
396
427
|
}
|
data/lib/excon/constants.rb
CHANGED
data/lib/excon/errors.rb
CHANGED
@@ -120,15 +120,17 @@ module Excon
|
|
120
120
|
503 => [Excon::Errors::ServiceUnavailable, 'Service Unavailable'],
|
121
121
|
504 => [Excon::Errors::GatewayTimeout, 'Gateway Timeout']
|
122
122
|
}
|
123
|
-
|
123
|
+
|
124
|
+
error, message = @errors[response[:status]] || [Excon::Errors::HTTPStatusError, 'Unknown']
|
124
125
|
|
125
126
|
# scrub authorization
|
127
|
+
request = request.dup
|
128
|
+
request.reject! {|key, value| [:connection, :stack].include?(key)}
|
126
129
|
if request[:headers].has_key?('Authorization')
|
127
|
-
request = request.dup
|
128
130
|
request[:headers] = request[:headers].dup
|
129
131
|
request[:headers]['Authorization'] = REDACTED
|
130
132
|
end
|
131
|
-
error.new("Expected(#{request[:expects].inspect}) <=> Actual(#{response
|
133
|
+
error.new("Expected(#{request[:expects].inspect}) <=> Actual(#{response[:status]} #{message})\n request => #{request.inspect}\n response => #{response.inspect}", request, response)
|
132
134
|
end
|
133
135
|
|
134
136
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Excon
|
2
|
+
module Middleware
|
3
|
+
class Base
|
4
|
+
def initialize(stack)
|
5
|
+
@stack = stack
|
6
|
+
end
|
7
|
+
|
8
|
+
def request_call(datum)
|
9
|
+
# do stuff
|
10
|
+
@stack.request_call(datum)
|
11
|
+
end
|
12
|
+
|
13
|
+
def response_call(datum)
|
14
|
+
@stack.response_call(datum)
|
15
|
+
# do stuff
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
module Excon
|
2
2
|
module Middleware
|
3
|
-
class Expects
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
raise(Excon::Errors.status_error(datum, Excon::Response.new(response_datum)))
|
3
|
+
class Expects < Excon::Middleware::Base
|
4
|
+
def response_call(datum)
|
5
|
+
if datum.has_key?(:expects) && ![*datum[:expects]].include?(datum[:response][:status])
|
6
|
+
raise(
|
7
|
+
Excon::Errors.status_error(
|
8
|
+
datum.reject {|key,value| key == :response},
|
9
|
+
datum[:response]
|
10
|
+
)
|
11
|
+
)
|
13
12
|
else
|
14
|
-
|
13
|
+
@stack.response_call(datum)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
@@ -1,25 +1,26 @@
|
|
1
1
|
module Excon
|
2
2
|
module Middleware
|
3
|
-
class Instrumentor
|
4
|
-
def
|
5
|
-
@app = app
|
6
|
-
end
|
7
|
-
|
8
|
-
def call(datum)
|
3
|
+
class Instrumentor < Excon::Middleware::Base
|
4
|
+
def request_call(datum)
|
9
5
|
if datum.has_key?(:instrumentor)
|
10
6
|
if datum[:retries_remaining] < datum[:retry_limit]
|
11
7
|
event_name = "#{datum[:instrumentor_name]}.retry"
|
12
8
|
else
|
13
9
|
event_name = "#{datum[:instrumentor_name]}.request"
|
14
10
|
end
|
15
|
-
|
16
|
-
@
|
11
|
+
datum[:instrumentor].instrument(event_name, datum) do
|
12
|
+
@stack.request_call(datum)
|
17
13
|
end
|
18
|
-
datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", response_datum)
|
19
|
-
response_datum
|
20
14
|
else
|
21
|
-
@
|
15
|
+
@stack.request_call(datum)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def response_call(datum)
|
20
|
+
if datum.has_key?(:instrumentor)
|
21
|
+
datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", datum[:response])
|
22
22
|
end
|
23
|
+
@stack.response_call(datum)
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Excon
|
2
|
+
module Middleware
|
3
|
+
class Mock < Excon::Middleware::Base
|
4
|
+
def request_call(datum)
|
5
|
+
if datum[:mock]
|
6
|
+
# convert File/Tempfile body to string before matching:
|
7
|
+
unless datum[:body].nil? || datum[:body].is_a?(String)
|
8
|
+
if datum[:body].respond_to?(:binmode)
|
9
|
+
datum[:body].binmode
|
10
|
+
end
|
11
|
+
if datum[:body].respond_to?(:rewind)
|
12
|
+
datum[:body].rewind
|
13
|
+
end
|
14
|
+
datum[:body] = datum[:body].read
|
15
|
+
end
|
16
|
+
|
17
|
+
datum[:captures] = {:headers => {}} # setup data to hold captures
|
18
|
+
Excon.stubs.each do |stub, response|
|
19
|
+
headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
|
20
|
+
case value = stub[:headers][key]
|
21
|
+
when Regexp
|
22
|
+
if match = value.match(datum[:headers][key])
|
23
|
+
datum[:captures][:headers][key] = match.captures
|
24
|
+
end
|
25
|
+
match
|
26
|
+
else
|
27
|
+
value == datum[:headers][key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
non_headers_match = (stub.keys - [:headers]).all? do |key|
|
31
|
+
case value = stub[key]
|
32
|
+
when Regexp
|
33
|
+
if match = value.match(datum[key])
|
34
|
+
datum[:captures][key] = match.captures
|
35
|
+
end
|
36
|
+
match
|
37
|
+
else
|
38
|
+
value == datum[key]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if headers_match && non_headers_match
|
42
|
+
datum[:response] = {
|
43
|
+
:body => '',
|
44
|
+
:headers => {},
|
45
|
+
:status => 200,
|
46
|
+
:remote_ip => '127.0.0.1'
|
47
|
+
}
|
48
|
+
|
49
|
+
stub_datum = case response
|
50
|
+
when Proc
|
51
|
+
response.call(datum)
|
52
|
+
else
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
datum[:response].merge!(stub_datum.reject {|key,value| key == :headers})
|
57
|
+
if stub_datum.has_key?(:headers)
|
58
|
+
datum[:response][:headers].merge!(stub_datum[:headers])
|
59
|
+
end
|
60
|
+
|
61
|
+
if datum[:expects] && ![*datum[:expects]].include?(datum[:response][:status])
|
62
|
+
# don't pass stuff into a block if there was an error
|
63
|
+
elsif datum.has_key?(:response_block) && datum[:response].has_key?(:body)
|
64
|
+
body = datum[:response].delete(:body)
|
65
|
+
content_length = remaining = body.bytesize
|
66
|
+
i = 0
|
67
|
+
while i < body.length
|
68
|
+
datum[:response_block].call(body[i, datum[:chunk_size]], [remaining - datum[:chunk_size], 0].max, content_length)
|
69
|
+
remaining -= datum[:chunk_size]
|
70
|
+
i += datum[:chunk_size]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
return @stack.request_call(datum)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# if we reach here no stubs matched
|
77
|
+
raise(Excon::Errors::StubNotFound.new('no stubs matched ' << datum.inspect))
|
78
|
+
else
|
79
|
+
@stack.request_call(datum)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/excon/response.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
1
|
module Excon
|
2
2
|
class Response
|
3
3
|
|
4
|
-
attr_accessor :body, :headers, :status, :remote_ip
|
4
|
+
attr_accessor :body, :data, :headers, :status, :remote_ip
|
5
5
|
|
6
|
-
def
|
7
|
-
{
|
8
|
-
:body
|
9
|
-
:headers
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
def initialize(params={})
|
7
|
+
@data = {
|
8
|
+
:body => '',
|
9
|
+
:headers => {}
|
10
|
+
}.merge(params)
|
11
|
+
@body = @data[:body]
|
12
|
+
@headers = @data[:headers]
|
13
|
+
@status = @data[:status]
|
14
|
+
@remote_ip = @data[:remote_ip]
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
@data[key]
|
13
19
|
end
|
14
20
|
|
15
21
|
def params
|
@@ -17,75 +23,6 @@ module Excon
|
|
17
23
|
data
|
18
24
|
end
|
19
25
|
|
20
|
-
def initialize(params={})
|
21
|
-
@body = params[:body] || ''
|
22
|
-
@headers = params[:headers] || {}
|
23
|
-
@status = params[:status]
|
24
|
-
@remote_ip = params[:remote_ip]
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.parse(socket, datum={})
|
28
|
-
response_datum = {
|
29
|
-
:body => '',
|
30
|
-
:headers => {},
|
31
|
-
:status => socket.read(12)[9, 11].to_i,
|
32
|
-
:remote_ip => socket.remote_ip
|
33
|
-
}
|
34
|
-
socket.readline # read the rest of the status line and CRLF
|
35
|
-
|
36
|
-
until ((data = socket.readline).chop!).empty?
|
37
|
-
key, value = data.split(/:\s*/, 2)
|
38
|
-
response_datum[:headers][key] = ([*response_datum[:headers][key]] << value).compact.join(', ')
|
39
|
-
if key.casecmp('Content-Length') == 0
|
40
|
-
content_length = value.to_i
|
41
|
-
elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
|
42
|
-
transfer_encoding_chunked = true
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(response_datum[:status])
|
47
|
-
|
48
|
-
# check to see if expects was set and matched
|
49
|
-
expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(response_datum[:status])
|
50
|
-
|
51
|
-
# if expects matched and there is a block, use it
|
52
|
-
if expected_status && datum.has_key?(:response_block)
|
53
|
-
if transfer_encoding_chunked
|
54
|
-
# 2 == "/r/n".length
|
55
|
-
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
56
|
-
datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
|
57
|
-
end
|
58
|
-
socket.read(2)
|
59
|
-
elsif remaining = content_length
|
60
|
-
while remaining > 0
|
61
|
-
datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
|
62
|
-
remaining -= datum[:chunk_size]
|
63
|
-
end
|
64
|
-
else
|
65
|
-
while remaining = socket.read(datum[:chunk_size])
|
66
|
-
datum[:response_block].call(remaining, remaining.length, content_length)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
else # no block or unexpected status
|
70
|
-
if transfer_encoding_chunked
|
71
|
-
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
72
|
-
response_datum[:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length
|
73
|
-
end
|
74
|
-
socket.read(2) # 2 == "/r/n".length
|
75
|
-
elsif remaining = content_length
|
76
|
-
while remaining > 0
|
77
|
-
response_datum[:body] << socket.read([datum[:chunk_size], remaining].min)
|
78
|
-
remaining -= datum[:chunk_size]
|
79
|
-
end
|
80
|
-
else
|
81
|
-
response_datum[:body] << socket.read
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
response_datum
|
87
|
-
end
|
88
|
-
|
89
26
|
# Retrieve a specific header value. Header names are treated case-insensitively.
|
90
27
|
# @param [String] name Header name
|
91
28
|
def get_header(name)
|
@@ -107,7 +107,7 @@ Shindo.tests('Excon stubs') do
|
|
107
107
|
|
108
108
|
tests("stub({:body => File.open(...), :method => :get}, { :status => 200 })") do
|
109
109
|
|
110
|
-
file_path = File.join(File.dirname(__FILE__), 'data', 'xs')
|
110
|
+
file_path = File.join(File.dirname(__FILE__), '..', 'data', 'xs')
|
111
111
|
|
112
112
|
Excon.stub(
|
113
113
|
{ :body => File.read(file_path), :method => :get },
|
data/tests/proxy_tests.rb
CHANGED
@@ -17,7 +17,7 @@ Shindo.tests('Excon proxy support') do
|
|
17
17
|
connection.data[:proxy][:host]
|
18
18
|
end
|
19
19
|
|
20
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
20
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
21
21
|
connection.data[:proxy][:port]
|
22
22
|
end
|
23
23
|
|
@@ -37,7 +37,7 @@ Shindo.tests('Excon proxy support') do
|
|
37
37
|
connection.data[:proxy][:host]
|
38
38
|
end
|
39
39
|
|
40
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
40
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
41
41
|
connection.data[:proxy][:port]
|
42
42
|
end
|
43
43
|
|
@@ -53,7 +53,7 @@ Shindo.tests('Excon proxy support') do
|
|
53
53
|
connection.data[:proxy][:host]
|
54
54
|
end
|
55
55
|
|
56
|
-
tests('connection.data[:proxy][:port]').returns(8081) do
|
56
|
+
tests('connection.data[:proxy][:port]').returns('8081') do
|
57
57
|
connection.data[:proxy][:port]
|
58
58
|
end
|
59
59
|
|
@@ -69,7 +69,7 @@ Shindo.tests('Excon proxy support') do
|
|
69
69
|
connection.data[:proxy][:host]
|
70
70
|
end
|
71
71
|
|
72
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
72
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
73
73
|
connection.data[:proxy][:port]
|
74
74
|
end
|
75
75
|
end
|
@@ -89,7 +89,7 @@ Shindo.tests('Excon proxy support') do
|
|
89
89
|
connection.data[:proxy][:host]
|
90
90
|
end
|
91
91
|
|
92
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
92
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
93
93
|
connection.data[:proxy][:port]
|
94
94
|
end
|
95
95
|
|
@@ -105,7 +105,7 @@ Shindo.tests('Excon proxy support') do
|
|
105
105
|
connection.data[:proxy][:host]
|
106
106
|
end
|
107
107
|
|
108
|
-
tests('connection.data[:proxy][:port]').returns(8081) do
|
108
|
+
tests('connection.data[:proxy][:port]').returns('8081') do
|
109
109
|
connection.data[:proxy][:port]
|
110
110
|
end
|
111
111
|
|
@@ -121,7 +121,7 @@ Shindo.tests('Excon proxy support') do
|
|
121
121
|
connection.data[:proxy][:host]
|
122
122
|
end
|
123
123
|
|
124
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
124
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
125
125
|
connection.data[:proxy][:port]
|
126
126
|
end
|
127
127
|
end
|
@@ -141,7 +141,7 @@ Shindo.tests('Excon proxy support') do
|
|
141
141
|
connection.data[:proxy][:host]
|
142
142
|
end
|
143
143
|
|
144
|
-
tests('connection.data[:proxy][:port]').returns(8080) do
|
144
|
+
tests('connection.data[:proxy][:port]').returns('8080') do
|
145
145
|
connection.data[:proxy][:port]
|
146
146
|
end
|
147
147
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
with_rackup('basic.ru') do
|
2
|
+
Shindo.tests('requests should succeed') do
|
3
|
+
|
4
|
+
connection = Excon.new('http://127.0.0.1:9292')
|
5
|
+
|
6
|
+
tests('HEAD /content-length/100, GET /content-length/100') do
|
7
|
+
responses = connection.requests([
|
8
|
+
{:method => :head, :path => '/content-length/100'},
|
9
|
+
{:method => :get, :path => '/content-length/100'}
|
10
|
+
])
|
11
|
+
|
12
|
+
tests('head body is empty').returns('') do
|
13
|
+
responses.first.body
|
14
|
+
end
|
15
|
+
|
16
|
+
tests('head content length is 100').returns('100') do
|
17
|
+
responses.first.headers['Content-Length']
|
18
|
+
end
|
19
|
+
|
20
|
+
tests('get body is non-empty').returns('x' * 100) do
|
21
|
+
responses.last.body
|
22
|
+
end
|
23
|
+
|
24
|
+
tests('get content length is 100').returns('100') do
|
25
|
+
responses.last.headers['Content-Length']
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/tests/test_helper.rb
CHANGED
@@ -19,6 +19,10 @@ def basic_tests(url = 'http://127.0.0.1:9292')
|
|
19
19
|
response.status
|
20
20
|
end
|
21
21
|
|
22
|
+
tests('response[:status]').returns(200) do
|
23
|
+
response[:status]
|
24
|
+
end
|
25
|
+
|
22
26
|
tests("response.headers['Connection']").returns('Keep-Alive') do
|
23
27
|
response.headers['Connection']
|
24
28
|
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.
|
4
|
+
version: 0.18.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-02-
|
14
|
+
date: 2013-02-21 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activesupport
|
@@ -177,8 +177,10 @@ files:
|
|
177
177
|
- lib/excon/connection.rb
|
178
178
|
- lib/excon/constants.rb
|
179
179
|
- lib/excon/errors.rb
|
180
|
+
- lib/excon/middlewares/base.rb
|
180
181
|
- lib/excon/middlewares/expects.rb
|
181
182
|
- lib/excon/middlewares/instrumentor.rb
|
183
|
+
- lib/excon/middlewares/mock.rb
|
182
184
|
- lib/excon/response.rb
|
183
185
|
- lib/excon/socket.rb
|
184
186
|
- lib/excon/ssl_socket.rb
|
@@ -190,6 +192,7 @@ files:
|
|
190
192
|
- tests/header_tests.rb
|
191
193
|
- tests/idempotent_tests.rb
|
192
194
|
- tests/middlewares/instrumentation_tests.rb
|
195
|
+
- tests/middlewares/mock_tests.rb
|
193
196
|
- tests/proxy_tests.rb
|
194
197
|
- tests/query_string_tests.rb
|
195
198
|
- tests/rackups/basic.rb
|
@@ -205,9 +208,9 @@ files:
|
|
205
208
|
- tests/rackups/timeout.ru
|
206
209
|
- tests/request_headers_tests.rb
|
207
210
|
- tests/request_method_tests.rb
|
211
|
+
- tests/requests_tests.rb
|
208
212
|
- tests/servers/bad.rb
|
209
213
|
- tests/servers/eof.rb
|
210
|
-
- tests/stub_tests.rb
|
211
214
|
- tests/test_helper.rb
|
212
215
|
- tests/thread_safety_tests.rb
|
213
216
|
- tests/timeout_tests.rb
|
@@ -226,7 +229,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
226
229
|
version: '0'
|
227
230
|
segments:
|
228
231
|
- 0
|
229
|
-
hash:
|
232
|
+
hash: 4265625222715917717
|
230
233
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
231
234
|
none: false
|
232
235
|
requirements:
|
@@ -235,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
238
|
version: '0'
|
236
239
|
requirements: []
|
237
240
|
rubyforge_project: excon
|
238
|
-
rubygems_version: 1.8.
|
241
|
+
rubygems_version: 1.8.23
|
239
242
|
signing_key:
|
240
243
|
specification_version: 2
|
241
244
|
summary: speed, persistence, http(s)
|