excon 0.15.5 → 0.16.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/README.md +6 -0
- data/changelog.txt +6 -0
- data/excon.gemspec +2 -2
- data/lib/excon.rb +2 -0
- data/lib/excon/constants.rb +1 -1
- data/lib/excon/socket.rb +88 -57
- data/tests/test_helper.rb +93 -87
- data/tests/timeout_tests.rb +7 -4
- metadata +17 -17
data/README.md
CHANGED
@@ -54,6 +54,12 @@ Both one-off and persistent connections support many other options. Here are a f
|
|
54
54
|
connection.request(:method => :get)
|
55
55
|
connection.request(:method => 'GET')
|
56
56
|
|
57
|
+
# this request can be repeated safely, so retry on errors up to 3 times
|
58
|
+
connection.request(:idempotent => true)
|
59
|
+
|
60
|
+
# opt out of nonblocking operations for performance and/or as a workaround
|
61
|
+
connection.request(:nonblock => false)
|
62
|
+
|
57
63
|
These options can be combined to make pretty much any request you might need.
|
58
64
|
|
59
65
|
Excon can also expect one or more HTTP status code in response, raising an exception if the response does not meet the criteria.
|
data/changelog.txt
CHANGED
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 = '2012-08-
|
16
|
+
s.version = '0.16.0'
|
17
|
+
s.date = '2012-08-14'
|
18
18
|
s.rubyforge_project = 'excon'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
data/lib/excon.rb
CHANGED
@@ -6,6 +6,7 @@ require 'forwardable'
|
|
6
6
|
require 'openssl'
|
7
7
|
require 'rbconfig'
|
8
8
|
require 'socket'
|
9
|
+
require 'timeout'
|
9
10
|
require 'uri'
|
10
11
|
|
11
12
|
require 'excon/constants'
|
@@ -26,6 +27,7 @@ module Excon
|
|
26
27
|
:headers => {},
|
27
28
|
:instrumentor_name => 'excon',
|
28
29
|
:mock => false,
|
30
|
+
:nonblock => true,
|
29
31
|
:read_timeout => 60,
|
30
32
|
:retry_limit => DEFAULT_RETRY_LIMIT,
|
31
33
|
:ssl_ca_file => DEFAULT_CA_FILE,
|
data/lib/excon/constants.rb
CHANGED
data/lib/excon/socket.rb
CHANGED
@@ -33,7 +33,17 @@ module Excon
|
|
33
33
|
|
34
34
|
socket = ::Socket.new(a_family, s_type, 0)
|
35
35
|
|
36
|
-
|
36
|
+
if @params[:nonblock]
|
37
|
+
socket.connect_nonblock(sockaddr)
|
38
|
+
else
|
39
|
+
begin
|
40
|
+
Timeout.timeout(@params[:connect_timeout]) do
|
41
|
+
socket.connect(sockaddr)
|
42
|
+
end
|
43
|
+
rescue Timeout::Error
|
44
|
+
raise Excon::Errors::Timeout.new('connect timeout reached')
|
45
|
+
end
|
46
|
+
end
|
37
47
|
|
38
48
|
@socket = socket
|
39
49
|
break
|
@@ -67,85 +77,106 @@ module Excon
|
|
67
77
|
|
68
78
|
def read(max_length=nil)
|
69
79
|
return nil if @eof
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
80
|
+
if @eof
|
81
|
+
nil
|
82
|
+
elsif @params[:nonblock]
|
83
|
+
begin
|
84
|
+
if max_length
|
85
|
+
until @read_buffer.length >= max_length
|
86
|
+
@read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
while true
|
90
|
+
@read_buffer << @socket.read_nonblock(CHUNK_SIZE)
|
91
|
+
end
|
75
92
|
end
|
76
|
-
|
77
|
-
|
78
|
-
@
|
93
|
+
rescue OpenSSL::SSL::SSLError => error
|
94
|
+
if error.message == 'read would block'
|
95
|
+
if IO.select([@socket], nil, nil, @params[:read_timeout])
|
96
|
+
retry
|
97
|
+
else
|
98
|
+
raise(Excon::Errors::Timeout.new("read timeout reached"))
|
99
|
+
end
|
100
|
+
else
|
101
|
+
raise(error)
|
79
102
|
end
|
80
|
-
|
81
|
-
rescue OpenSSL::SSL::SSLError => error
|
82
|
-
if error.message == 'read would block'
|
103
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
|
83
104
|
if IO.select([@socket], nil, nil, @params[:read_timeout])
|
84
105
|
retry
|
85
106
|
else
|
86
107
|
raise(Excon::Errors::Timeout.new("read timeout reached"))
|
87
108
|
end
|
88
|
-
|
89
|
-
|
109
|
+
rescue EOFError
|
110
|
+
@eof = true
|
90
111
|
end
|
91
|
-
|
92
|
-
|
93
|
-
retry
|
112
|
+
if max_length
|
113
|
+
@read_buffer.slice!(0, max_length)
|
94
114
|
else
|
95
|
-
|
115
|
+
# read until EOFError, so return everything
|
116
|
+
@read_buffer.slice!(0, @read_buffer.length)
|
96
117
|
end
|
97
|
-
rescue EOFError
|
98
|
-
@eof = true
|
99
|
-
end
|
100
|
-
if max_length
|
101
|
-
@read_buffer.slice!(0, max_length)
|
102
118
|
else
|
103
|
-
|
104
|
-
|
119
|
+
begin
|
120
|
+
Timeout.timeout(@params[:read_timeout]) do
|
121
|
+
@socket.read(max_length)
|
122
|
+
end
|
123
|
+
rescue Timeout::Error
|
124
|
+
raise Excon::Errors::Timeout.new('read timeout reached')
|
125
|
+
end
|
105
126
|
end
|
106
127
|
end
|
107
128
|
|
108
129
|
def write(data)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
130
|
+
if @params[:nonblock]
|
131
|
+
# We normally return from the return in the else block below, but
|
132
|
+
# we guard that data is still something in case we get weird
|
133
|
+
# values and String#[] returns nil. (This behavior has been observed
|
134
|
+
# in the wild, so this is a simple defensive mechanism)
|
135
|
+
while data
|
136
|
+
begin
|
137
|
+
# I wish that this API accepted a start position, then we wouldn't
|
138
|
+
# have to slice data when there is a short write.
|
139
|
+
written = @socket.write_nonblock(data)
|
140
|
+
rescue OpenSSL::SSL::SSLError => error
|
141
|
+
if error.message == 'write would block'
|
142
|
+
if IO.select(nil, [@socket], nil, @params[:write_timeout])
|
143
|
+
retry
|
144
|
+
else
|
145
|
+
raise(Excon::Errors::Timeout.new("write timeout reached"))
|
146
|
+
end
|
147
|
+
else
|
148
|
+
raise(error)
|
149
|
+
end
|
150
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable
|
120
151
|
if IO.select(nil, [@socket], nil, @params[:write_timeout])
|
121
152
|
retry
|
122
153
|
else
|
123
154
|
raise(Excon::Errors::Timeout.new("write timeout reached"))
|
124
155
|
end
|
125
156
|
else
|
126
|
-
|
157
|
+
# Fast, common case.
|
158
|
+
# The >= seems weird, why would it have written MORE than we
|
159
|
+
# requested. But we're getting some weird behavior when @socket
|
160
|
+
# is an OpenSSL socket, where it seems like it's saying it wrote
|
161
|
+
# more (perhaps due to SSL packet overhead?).
|
162
|
+
#
|
163
|
+
# Pretty weird, but this is a simple defensive mechanism.
|
164
|
+
return if written >= data.size
|
165
|
+
|
166
|
+
# This takes advantage of the fact that most ruby implementations
|
167
|
+
# have Copy-On-Write strings. Thusly why requesting a subrange
|
168
|
+
# of data, we actually don't copy data because the new string
|
169
|
+
# simply references a subrange of the original.
|
170
|
+
data = data[written, data.size]
|
127
171
|
end
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
172
|
+
end
|
173
|
+
else
|
174
|
+
begin
|
175
|
+
Timeout.timeout(@params[:write_timeout]) do
|
176
|
+
@socket.write(data)
|
133
177
|
end
|
134
|
-
|
135
|
-
|
136
|
-
# The >= seems weird, why would it have written MORE than we
|
137
|
-
# requested. But we're getting some weird behavior when @socket
|
138
|
-
# is an OpenSSL socket, where it seems like it's saying it wrote
|
139
|
-
# more (perhaps due to SSL packet overhead?).
|
140
|
-
#
|
141
|
-
# Pretty weird, but this is a simple defensive mechanism.
|
142
|
-
return if written >= data.size
|
143
|
-
|
144
|
-
# This takes advantage of the fact that most ruby implementations
|
145
|
-
# have Copy-On-Write strings. Thusly why requesting a subrange
|
146
|
-
# of data, we actually don't copy data because the new string
|
147
|
-
# simply references a subrange of the original.
|
148
|
-
data = data[written, data.size]
|
178
|
+
rescue Timeout::Error
|
179
|
+
Excon::Errors::Timeout.new('write timeout reached')
|
149
180
|
end
|
150
181
|
end
|
151
182
|
end
|
data/tests/test_helper.rb
CHANGED
@@ -6,123 +6,129 @@ Bundler.require(:default, :development)
|
|
6
6
|
require 'stringio'
|
7
7
|
|
8
8
|
def basic_tests(url = 'http://127.0.0.1:9292')
|
9
|
+
[false, true].each do |nonblock|
|
9
10
|
|
10
|
-
|
11
|
+
connection = Excon.new(url, :nonblock => nonblock, :ssl_verify_peer => false)
|
11
12
|
|
12
|
-
|
13
|
-
response = connection.request(:method => :get, :path => '/content-length/100')
|
13
|
+
tests("nonblock => #{nonblock}") do
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
end
|
15
|
+
tests('GET /content-length/100') do
|
16
|
+
response = connection.request(:method => :get, :path => '/content-length/100')
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
tests('response.status').returns(200) do
|
19
|
+
response.status
|
20
|
+
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
tests("response.headers['Connection']").returns('Keep-Alive') do
|
23
|
+
response.headers['Connection']
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
tests("response.headers['Content-Length']").returns('100') do
|
27
|
+
response.headers['Content-Length']
|
28
|
+
end
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
tests("response.headers['Content-Type']").returns('text/html;charset=utf-8') do
|
31
|
+
response.headers['Content-Type']
|
32
|
+
end
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
test("Time.parse(response.headers['Date']).is_a?(Time)") do
|
35
|
+
Time.parse(response.headers['Date']).is_a?(Time)
|
36
|
+
end
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
test("!!(response.headers['Server'] =~ /^WEBrick/)") do
|
39
|
+
!!(response.headers['Server'] =~ /^WEBrick/)
|
40
|
+
end
|
42
41
|
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
tests("response.headers['Custom']").returns("Foo: bar") do
|
43
|
+
response.headers['Custom']
|
44
|
+
end
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
46
|
+
tests("response.body").returns('x' * 100) do
|
47
|
+
response.body
|
48
|
+
end
|
49
|
+
|
50
|
+
tests("deprecated block usage").returns(['x' * 100, 0, 100]) do
|
51
|
+
data = []
|
52
|
+
connection.request(:method => :get, :path => '/content-length/100') do |chunk, remaining_length, total_length|
|
53
|
+
data = [chunk, remaining_length, total_length]
|
54
|
+
end
|
55
|
+
data
|
56
|
+
end
|
57
|
+
|
58
|
+
tests("response_block usage").returns(['x' * 100, 0, 100]) do
|
59
|
+
data = []
|
60
|
+
response_block = lambda do |chunk, remaining_length, total_length|
|
61
|
+
data = [chunk, remaining_length, total_length]
|
62
|
+
end
|
63
|
+
connection.request(:method => :get, :path => '/content-length/100', :response_block => response_block)
|
64
|
+
data
|
65
|
+
end
|
54
66
|
|
55
|
-
tests("response_block usage").returns(['x' * 100, 0, 100]) do
|
56
|
-
data = []
|
57
|
-
response_block = lambda do |chunk, remaining_length, total_length|
|
58
|
-
data = [chunk, remaining_length, total_length]
|
59
67
|
end
|
60
|
-
connection.request(:method => :get, :path => '/content-length/100', :response_block => response_block)
|
61
|
-
data
|
62
|
-
end
|
63
68
|
|
64
|
-
|
69
|
+
tests('POST /body-sink') do
|
65
70
|
|
66
|
-
|
71
|
+
tests('response.body').returns("5000000") do
|
72
|
+
response = connection.request(:method => :post, :path => '/body-sink', :headers => { 'Content-Type' => 'text/plain' }, :body => 'x' * 5_000_000)
|
73
|
+
response.body
|
74
|
+
end
|
67
75
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
76
|
+
tests('empty body').returns('0') do
|
77
|
+
response = connection.request(:method => :post, :path => '/body-sink', :headers => { 'Content-Type' => 'text/plain' }, :body => '')
|
78
|
+
response.body
|
79
|
+
end
|
72
80
|
|
73
|
-
|
74
|
-
response = connection.request(:method => :post, :path => '/body-sink', :headers => { 'Content-Type' => 'text/plain' }, :body => '')
|
75
|
-
response.body
|
76
|
-
end
|
81
|
+
end
|
77
82
|
|
78
|
-
|
83
|
+
tests('POST /echo') do
|
79
84
|
|
80
|
-
|
85
|
+
tests('with file').returns('x' * 100 + "\n") do
|
86
|
+
file_path = File.join(File.dirname(__FILE__), "data", "xs")
|
87
|
+
response = connection.request(:method => :post, :path => '/echo', :body => File.open(file_path))
|
88
|
+
response.body
|
89
|
+
end
|
81
90
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
91
|
+
tests('without request_block').returns('x' * 100) do
|
92
|
+
response = connection.request(:method => :post, :path => '/echo', :body => 'x' * 100)
|
93
|
+
response.body
|
94
|
+
end
|
87
95
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
96
|
+
tests('with request_block').returns('x' * 100) do
|
97
|
+
data = ['x'] * 100
|
98
|
+
request_block = lambda do
|
99
|
+
data.shift.to_s
|
100
|
+
end
|
101
|
+
response = connection.request(:method => :post, :path => '/echo', :request_block => request_block)
|
102
|
+
response.body
|
103
|
+
end
|
92
104
|
|
93
|
-
tests('with request_block').returns('x' * 100) do
|
94
|
-
data = ['x'] * 100
|
95
|
-
request_block = lambda do
|
96
|
-
data.shift.to_s
|
97
105
|
end
|
98
|
-
response = connection.request(:method => :post, :path => '/echo', :request_block => request_block)
|
99
|
-
response.body
|
100
|
-
end
|
101
106
|
|
102
|
-
|
107
|
+
tests('PUT /echo') do
|
103
108
|
|
104
|
-
|
109
|
+
tests('with file').returns('x' * 100 + "\n") do
|
110
|
+
file_path = File.join(File.dirname(__FILE__), "data", "xs")
|
111
|
+
response = connection.request(:method => :put, :path => '/echo', :body => File.open(file_path))
|
112
|
+
response.body
|
113
|
+
end
|
105
114
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
115
|
+
tests('without request_block').returns('x' * 100) do
|
116
|
+
response = connection.request(:method => :put, :path => '/echo', :body => 'x' * 100)
|
117
|
+
response.body
|
118
|
+
end
|
111
119
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
120
|
+
tests('request_block usage').returns('x' * 100) do
|
121
|
+
data = ['x'] * 100
|
122
|
+
request_block = lambda do
|
123
|
+
data.shift.to_s
|
124
|
+
end
|
125
|
+
response = connection.request(:method => :put, :path => '/echo', :request_block => request_block)
|
126
|
+
response.body
|
127
|
+
end
|
116
128
|
|
117
|
-
tests('request_block usage').returns('x' * 100) do
|
118
|
-
data = ['x'] * 100
|
119
|
-
request_block = lambda do
|
120
|
-
data.shift.to_s
|
121
129
|
end
|
122
|
-
response = connection.request(:method => :put, :path => '/echo', :request_block => request_block)
|
123
|
-
response.body
|
124
|
-
end
|
125
130
|
|
131
|
+
end
|
126
132
|
end
|
127
133
|
end
|
128
134
|
|
data/tests/timeout_tests.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
with_rackup('timeout.ru') do
|
2
2
|
Shindo.tests('read should timeout') do
|
3
|
-
|
3
|
+
[false, true].each do |nonblock|
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
connection = Excon.new('http://127.0.0.1:9292', :nonblock => nonblock)
|
6
|
+
|
7
|
+
tests("nonblock => #{nonblock} hits read_timeout").raises(Excon::Errors::Timeout) do
|
8
|
+
connection.request(:method => :get, :path => '/timeout', :read_timeout => 1)
|
9
|
+
end
|
8
10
|
|
11
|
+
end
|
9
12
|
end
|
10
13
|
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.16.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,11 +11,11 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2012-08-
|
14
|
+
date: 2012-08-14 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activesupport
|
18
|
-
requirement: &
|
18
|
+
requirement: &70312939290540 !ruby/object:Gem::Requirement
|
19
19
|
none: false
|
20
20
|
requirements:
|
21
21
|
- - ! '>='
|
@@ -23,10 +23,10 @@ dependencies:
|
|
23
23
|
version: '0'
|
24
24
|
type: :development
|
25
25
|
prerelease: false
|
26
|
-
version_requirements: *
|
26
|
+
version_requirements: *70312939290540
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: delorean
|
29
|
-
requirement: &
|
29
|
+
requirement: &70312939288380 !ruby/object:Gem::Requirement
|
30
30
|
none: false
|
31
31
|
requirements:
|
32
32
|
- - ! '>='
|
@@ -34,10 +34,10 @@ dependencies:
|
|
34
34
|
version: '0'
|
35
35
|
type: :development
|
36
36
|
prerelease: false
|
37
|
-
version_requirements: *
|
37
|
+
version_requirements: *70312939288380
|
38
38
|
- !ruby/object:Gem::Dependency
|
39
39
|
name: open4
|
40
|
-
requirement: &
|
40
|
+
requirement: &70312939286840 !ruby/object:Gem::Requirement
|
41
41
|
none: false
|
42
42
|
requirements:
|
43
43
|
- - ! '>='
|
@@ -45,10 +45,10 @@ dependencies:
|
|
45
45
|
version: '0'
|
46
46
|
type: :development
|
47
47
|
prerelease: false
|
48
|
-
version_requirements: *
|
48
|
+
version_requirements: *70312939286840
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
50
|
name: rake
|
51
|
-
requirement: &
|
51
|
+
requirement: &70312939351900 !ruby/object:Gem::Requirement
|
52
52
|
none: false
|
53
53
|
requirements:
|
54
54
|
- - ! '>='
|
@@ -56,10 +56,10 @@ dependencies:
|
|
56
56
|
version: '0'
|
57
57
|
type: :development
|
58
58
|
prerelease: false
|
59
|
-
version_requirements: *
|
59
|
+
version_requirements: *70312939351900
|
60
60
|
- !ruby/object:Gem::Dependency
|
61
61
|
name: rdoc
|
62
|
-
requirement: &
|
62
|
+
requirement: &70312939349280 !ruby/object:Gem::Requirement
|
63
63
|
none: false
|
64
64
|
requirements:
|
65
65
|
- - ! '>='
|
@@ -67,10 +67,10 @@ dependencies:
|
|
67
67
|
version: '0'
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
|
-
version_requirements: *
|
70
|
+
version_requirements: *70312939349280
|
71
71
|
- !ruby/object:Gem::Dependency
|
72
72
|
name: shindo
|
73
|
-
requirement: &
|
73
|
+
requirement: &70312939363220 !ruby/object:Gem::Requirement
|
74
74
|
none: false
|
75
75
|
requirements:
|
76
76
|
- - ! '>='
|
@@ -78,10 +78,10 @@ dependencies:
|
|
78
78
|
version: '0'
|
79
79
|
type: :development
|
80
80
|
prerelease: false
|
81
|
-
version_requirements: *
|
81
|
+
version_requirements: *70312939363220
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
83
|
name: sinatra
|
84
|
-
requirement: &
|
84
|
+
requirement: &70312939360540 !ruby/object:Gem::Requirement
|
85
85
|
none: false
|
86
86
|
requirements:
|
87
87
|
- - ! '>='
|
@@ -89,7 +89,7 @@ dependencies:
|
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
|
-
version_requirements: *
|
92
|
+
version_requirements: *70312939360540
|
93
93
|
description: EXtended http(s) CONnections
|
94
94
|
email: geemus@gmail.com
|
95
95
|
executables: []
|
@@ -168,7 +168,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
168
|
version: '0'
|
169
169
|
segments:
|
170
170
|
- 0
|
171
|
-
hash:
|
171
|
+
hash: -2915309492516457660
|
172
172
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
173
|
none: false
|
174
174
|
requirements:
|