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 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.
@@ -1,3 +1,9 @@
1
+ 0.16.0 09/14/2012
2
+ =================
3
+
4
+ add nonblock => false to use blocking requests with Timeout.timeout
5
+ update readme to describe nonblock and idempotent options
6
+
1
7
  0.15.5 08/01/2012
2
8
  =================
3
9
 
@@ -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.15.5'
17
- s.date = '2012-08-01'
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
@@ -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,
@@ -1,6 +1,6 @@
1
1
  module Excon
2
2
  unless const_defined?(:VERSION)
3
- VERSION = '0.15.5'
3
+ VERSION = '0.16.0'
4
4
  end
5
5
 
6
6
  unless const_defined?(:CHUNK_SIZE)
@@ -33,7 +33,17 @@ module Excon
33
33
 
34
34
  socket = ::Socket.new(a_family, s_type, 0)
35
35
 
36
- socket.connect_nonblock(sockaddr)
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
- begin
72
- if max_length
73
- until @read_buffer.length >= max_length
74
- @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
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
- else
77
- while true
78
- @read_buffer << @socket.read_nonblock(CHUNK_SIZE)
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
- end
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
- else
89
- raise(error)
109
+ rescue EOFError
110
+ @eof = true
90
111
  end
91
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
92
- if IO.select([@socket], nil, nil, @params[:read_timeout])
93
- retry
112
+ if max_length
113
+ @read_buffer.slice!(0, max_length)
94
114
  else
95
- raise(Excon::Errors::Timeout.new("read timeout reached"))
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
- # read until EOFError, so return everything
104
- @read_buffer.slice!(0, @read_buffer.length)
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
- # We normally return from the return in the else block below, but
110
- # we guard that data is still something in case we get weird
111
- # values and String#[] returns nil. (This behavior has been observed
112
- # in the wild, so this is a simple defensive mechanism)
113
- while data
114
- begin
115
- # I wish that this API accepted a start position, then we wouldn't
116
- # have to slice data when there is a short write.
117
- written = @socket.write_nonblock(data)
118
- rescue OpenSSL::SSL::SSLError => error
119
- if error.message == 'write would block'
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
- raise(error)
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
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable
129
- if IO.select(nil, [@socket], nil, @params[:write_timeout])
130
- retry
131
- else
132
- raise(Excon::Errors::Timeout.new("write timeout reached"))
172
+ end
173
+ else
174
+ begin
175
+ Timeout.timeout(@params[:write_timeout]) do
176
+ @socket.write(data)
133
177
  end
134
- else
135
- # Fast, common case.
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
@@ -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
- connection = Excon.new(url, :ssl_verify_peer => false)
11
+ connection = Excon.new(url, :nonblock => nonblock, :ssl_verify_peer => false)
11
12
 
12
- tests('GET /content-length/100') do
13
- response = connection.request(:method => :get, :path => '/content-length/100')
13
+ tests("nonblock => #{nonblock}") do
14
14
 
15
- tests('response.status').returns(200) do
16
- response.status
17
- end
15
+ tests('GET /content-length/100') do
16
+ response = connection.request(:method => :get, :path => '/content-length/100')
18
17
 
19
- tests("response.headers['Connection']").returns('Keep-Alive') do
20
- response.headers['Connection']
21
- end
18
+ tests('response.status').returns(200) do
19
+ response.status
20
+ end
22
21
 
23
- tests("response.headers['Content-Length']").returns('100') do
24
- response.headers['Content-Length']
25
- end
22
+ tests("response.headers['Connection']").returns('Keep-Alive') do
23
+ response.headers['Connection']
24
+ end
26
25
 
27
- tests("response.headers['Content-Type']").returns('text/html;charset=utf-8') do
28
- response.headers['Content-Type']
29
- end
26
+ tests("response.headers['Content-Length']").returns('100') do
27
+ response.headers['Content-Length']
28
+ end
30
29
 
31
- test("Time.parse(response.headers['Date']).is_a?(Time)") do
32
- Time.parse(response.headers['Date']).is_a?(Time)
33
- end
30
+ tests("response.headers['Content-Type']").returns('text/html;charset=utf-8') do
31
+ response.headers['Content-Type']
32
+ end
34
33
 
35
- test("!!(response.headers['Server'] =~ /^WEBrick/)") do
36
- !!(response.headers['Server'] =~ /^WEBrick/)
37
- end
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
- tests("response.headers['Custom']").returns("Foo: bar") do
40
- response.headers['Custom']
41
- end
38
+ test("!!(response.headers['Server'] =~ /^WEBrick/)") do
39
+ !!(response.headers['Server'] =~ /^WEBrick/)
40
+ end
42
41
 
43
- tests("response.body").returns('x' * 100) do
44
- response.body
45
- end
42
+ tests("response.headers['Custom']").returns("Foo: bar") do
43
+ response.headers['Custom']
44
+ end
46
45
 
47
- tests("deprecated block usage").returns(['x' * 100, 0, 100]) do
48
- data = []
49
- connection.request(:method => :get, :path => '/content-length/100') do |chunk, remaining_length, total_length|
50
- data = [chunk, remaining_length, total_length]
51
- end
52
- data
53
- end
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
- end
69
+ tests('POST /body-sink') do
65
70
 
66
- tests('POST /body-sink') do
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
- tests('response.body').returns("5000000") do
69
- response = connection.request(:method => :post, :path => '/body-sink', :headers => { 'Content-Type' => 'text/plain' }, :body => 'x' * 5_000_000)
70
- response.body
71
- end
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
- tests('empty body').returns('0') do
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
- end
83
+ tests('POST /echo') do
79
84
 
80
- tests('POST /echo') do
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
- tests('with file').returns('x' * 100 + "\n") do
83
- file_path = File.join(File.dirname(__FILE__), "data", "xs")
84
- response = connection.request(:method => :post, :path => '/echo', :body => File.open(file_path))
85
- response.body
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
- tests('without request_block').returns('x' * 100) do
89
- response = connection.request(:method => :post, :path => '/echo', :body => 'x' * 100)
90
- response.body
91
- end
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
- end
107
+ tests('PUT /echo') do
103
108
 
104
- tests('PUT /echo') do
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
- tests('with file').returns('x' * 100 + "\n") do
107
- file_path = File.join(File.dirname(__FILE__), "data", "xs")
108
- response = connection.request(:method => :put, :path => '/echo', :body => File.open(file_path))
109
- response.body
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
- tests('without request_block').returns('x' * 100) do
113
- response = connection.request(:method => :put, :path => '/echo', :body => 'x' * 100)
114
- response.body
115
- end
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
 
@@ -1,10 +1,13 @@
1
1
  with_rackup('timeout.ru') do
2
2
  Shindo.tests('read should timeout') do
3
- connection = Excon.new('http://127.0.0.1:9292')
3
+ [false, true].each do |nonblock|
4
4
 
5
- tests('hits read_timeout').raises(Excon::Errors::Timeout) do
6
- connection.request(:method => :get, :path => '/timeout', :read_timeout => 1)
7
- end
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.15.5
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-01 00:00:00.000000000 Z
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: &70157967898360 !ruby/object:Gem::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: *70157967898360
26
+ version_requirements: *70312939290540
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: delorean
29
- requirement: &70157967894540 !ruby/object:Gem::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: *70157967894540
37
+ version_requirements: *70312939288380
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: open4
40
- requirement: &70157967906920 !ruby/object:Gem::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: *70157967906920
48
+ version_requirements: *70312939286840
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: rake
51
- requirement: &70157967905100 !ruby/object:Gem::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: *70157967905100
59
+ version_requirements: *70312939351900
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: rdoc
62
- requirement: &70157967902880 !ruby/object:Gem::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: *70157967902880
70
+ version_requirements: *70312939349280
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: shindo
73
- requirement: &70157967902120 !ruby/object:Gem::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: *70157967902120
81
+ version_requirements: *70312939363220
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: sinatra
84
- requirement: &70157967917600 !ruby/object:Gem::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: *70157967917600
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: 3311072793664556069
171
+ hash: -2915309492516457660
172
172
  required_rubygems_version: !ruby/object:Gem::Requirement
173
173
  none: false
174
174
  requirements: