rest-client 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rest-client might be problematic. Click here for more details.
- data/README.rdoc +119 -7
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/history.md +28 -0
- data/lib/restclient.rb +63 -20
- data/lib/restclient/exceptions.rb +77 -46
- data/lib/restclient/mixin/response.rb +23 -7
- data/lib/restclient/payload.rb +2 -2
- data/lib/restclient/request.rb +60 -71
- data/lib/restclient/resource.rb +11 -10
- data/spec/exceptions_spec.rb +3 -5
- data/spec/integration_spec.rb +38 -0
- data/spec/mixin/response_spec.rb +1 -1
- data/spec/payload_spec.rb +6 -6
- data/spec/request_spec.rb +357 -332
- data/spec/resource_spec.rb +24 -0
- data/spec/response_spec.rb +51 -0
- data/spec/restclient_spec.rb +21 -11
- metadata +8 -4
@@ -3,8 +3,7 @@ module RestClient
|
|
3
3
|
module Response
|
4
4
|
attr_reader :net_http_res
|
5
5
|
|
6
|
-
# HTTP status code
|
7
|
-
# other codes.
|
6
|
+
# HTTP status code
|
8
7
|
def code
|
9
8
|
@code ||= @net_http_res.code.to_i
|
10
9
|
end
|
@@ -22,15 +21,32 @@ module RestClient
|
|
22
21
|
|
23
22
|
# Hash of cookies extracted from response headers
|
24
23
|
def cookies
|
25
|
-
@cookies ||= (self.headers[:set_cookie] ||
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
@cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content|
|
25
|
+
# correctly parse comma-separated cookies containing HTTP dates (which also contain a comma)
|
26
|
+
cookie_content.split(/,\s*/).inject([""]) { |array, blob|
|
27
|
+
blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob)
|
28
|
+
array
|
29
|
+
}.each do |cookie|
|
30
|
+
next if cookie.empty?
|
31
|
+
key, *val = cookie.split(";").first.split("=")
|
32
|
+
out[key] = val.join("=")
|
29
33
|
end
|
30
34
|
out
|
31
35
|
end
|
32
36
|
end
|
33
37
|
|
38
|
+
# Return the default behavior corresponding to the response code:
|
39
|
+
# the response itself for code in 200..206 and an exception in other cases
|
40
|
+
def return!
|
41
|
+
if (200..206).include? code
|
42
|
+
self
|
43
|
+
elsif Exceptions::EXCEPTIONS_MAP[code]
|
44
|
+
raise Exceptions::EXCEPTIONS_MAP[code], self
|
45
|
+
else
|
46
|
+
raise RequestFailed, self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
34
50
|
def self.included(receiver)
|
35
51
|
receiver.extend(RestClient::Mixin::Response::ClassMethods)
|
36
52
|
end
|
@@ -38,7 +54,7 @@ module RestClient
|
|
38
54
|
module ClassMethods
|
39
55
|
def beautify_headers(headers)
|
40
56
|
headers.inject({}) do |out, (key, value)|
|
41
|
-
out[key.gsub(/-/, '_').downcase.to_sym] = value.first
|
57
|
+
out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first
|
42
58
|
out
|
43
59
|
end
|
44
60
|
end
|
data/lib/restclient/payload.rb
CHANGED
@@ -138,7 +138,7 @@ module RestClient
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def create_regular_field(s, k, v)
|
141
|
-
s.write("Content-Disposition:
|
141
|
+
s.write("Content-Disposition: form-data; name=\"#{k}\"")
|
142
142
|
s.write(EOL)
|
143
143
|
s.write(EOL)
|
144
144
|
s.write(v)
|
@@ -146,7 +146,7 @@ module RestClient
|
|
146
146
|
|
147
147
|
def create_file_field(s, k, v)
|
148
148
|
begin
|
149
|
-
s.write("Content-Disposition:
|
149
|
+
s.write("Content-Disposition: form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
|
150
150
|
s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
|
151
151
|
s.write(EOL)
|
152
152
|
while data = v.read(8124)
|
data/lib/restclient/request.rb
CHANGED
@@ -20,16 +20,18 @@ module RestClient
|
|
20
20
|
# * :timeout and :open_timeout
|
21
21
|
# * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
|
22
22
|
class Request
|
23
|
+
|
23
24
|
attr_reader :method, :url, :payload, :headers, :processed_headers,
|
24
25
|
:cookies, :user, :password, :timeout, :open_timeout,
|
25
26
|
:verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
|
26
27
|
:raw_response
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
|
30
|
+
def self.execute(args, &block)
|
31
|
+
new(args).execute &block
|
30
32
|
end
|
31
33
|
|
32
|
-
def initialize
|
34
|
+
def initialize args
|
33
35
|
@method = args[:method] or raise ArgumentError, "must pass :method"
|
34
36
|
@url = args[:url] or raise ArgumentError, "must pass :url"
|
35
37
|
@headers = args[:headers] || {}
|
@@ -48,23 +50,25 @@ module RestClient
|
|
48
50
|
@processed_headers = make_headers headers
|
49
51
|
end
|
50
52
|
|
51
|
-
def execute
|
52
|
-
execute_inner
|
53
|
+
def execute &block
|
54
|
+
execute_inner &block
|
53
55
|
rescue Redirect => e
|
56
|
+
@processed_headers.delete "Content-Length"
|
57
|
+
@processed_headers.delete "Content-Type"
|
54
58
|
@url = e.url
|
55
59
|
@method = :get
|
56
60
|
@payload = nil
|
57
|
-
execute
|
61
|
+
execute &block
|
58
62
|
end
|
59
63
|
|
60
|
-
def execute_inner
|
64
|
+
def execute_inner &block
|
61
65
|
uri = parse_url_with_auth(url)
|
62
|
-
transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload
|
66
|
+
transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, &block
|
63
67
|
end
|
64
68
|
|
65
69
|
def make_headers user_headers
|
66
70
|
unless @cookies.empty?
|
67
|
-
user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join(
|
71
|
+
user_headers[:cookie] = @cookies.map {|(key, val)| "#{key.to_s}=#{val}" }.sort.join(",")
|
68
72
|
end
|
69
73
|
|
70
74
|
headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
|
@@ -73,13 +77,13 @@ module RestClient
|
|
73
77
|
target_value = value.to_s
|
74
78
|
final[target_key] = MIME::Types.type_for_extension target_value
|
75
79
|
elsif 'ACCEPT' == target_key.upcase
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
# Accept can be composed of several comma-separated values
|
81
|
+
if value.is_a? Array
|
82
|
+
target_values = value
|
83
|
+
else
|
84
|
+
target_values = value.to_s.split ','
|
85
|
+
end
|
86
|
+
final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ')
|
83
87
|
else
|
84
88
|
final[target_key] = value.to_s
|
85
89
|
end
|
@@ -132,8 +136,8 @@ module RestClient
|
|
132
136
|
end
|
133
137
|
end
|
134
138
|
|
135
|
-
def transmit
|
136
|
-
setup_credentials
|
139
|
+
def transmit uri, req, payload, &block
|
140
|
+
setup_credentials req
|
137
141
|
|
138
142
|
net = net_http_class.new(uri.host, uri.port)
|
139
143
|
net.use_ssl = uri.is_a?(URI::HTTPS)
|
@@ -148,20 +152,12 @@ module RestClient
|
|
148
152
|
net.read_timeout = @timeout if @timeout
|
149
153
|
net.open_timeout = @open_timeout if @open_timeout
|
150
154
|
|
151
|
-
|
155
|
+
log_request
|
152
156
|
|
153
157
|
net.start do |http|
|
154
158
|
res = http.request(req, payload) { |http_response| fetch_body(http_response) }
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
if result.kind_of?(String) or @method == :head
|
159
|
-
Response.new(result, res)
|
160
|
-
elsif @raw_response
|
161
|
-
RawResponse.new(@tf, res)
|
162
|
-
else
|
163
|
-
Response.new(nil, res)
|
164
|
-
end
|
159
|
+
log_response res
|
160
|
+
process_result res, &block
|
165
161
|
end
|
166
162
|
rescue EOFError
|
167
163
|
raise RestClient::ServerBrokeConnection
|
@@ -181,14 +177,16 @@ module RestClient
|
|
181
177
|
@tf = Tempfile.new("rest-client")
|
182
178
|
size, total = 0, http_response.header['Content-Length'].to_i
|
183
179
|
http_response.read_body do |chunk|
|
184
|
-
@tf.write
|
180
|
+
@tf.write chunk
|
185
181
|
size += chunk.size
|
186
|
-
if
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
182
|
+
if RestClient.log
|
183
|
+
if size == 0
|
184
|
+
RestClient.log << "#{@method} #{@url} done (0 length file\n)"
|
185
|
+
elsif total == 0
|
186
|
+
RestClient.log << "#{@method} #{@url} (zero content length)\n"
|
187
|
+
else
|
188
|
+
RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
|
189
|
+
end
|
192
190
|
end
|
193
191
|
end
|
194
192
|
@tf.close
|
@@ -199,13 +197,17 @@ module RestClient
|
|
199
197
|
http_response
|
200
198
|
end
|
201
199
|
|
202
|
-
def process_result
|
203
|
-
if
|
200
|
+
def process_result res
|
201
|
+
if @raw_response
|
204
202
|
# We don't decode raw requests
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
203
|
+
response = RawResponse.new(@tf, res)
|
204
|
+
else
|
205
|
+
response = Response.new(Request.decode(res['content-encoding'], res.body), res)
|
206
|
+
end
|
207
|
+
|
208
|
+
code = res.code.to_i
|
209
|
+
|
210
|
+
if (301..303).include? code
|
209
211
|
url = res.header['Location']
|
210
212
|
|
211
213
|
if url !~ /^http/
|
@@ -213,53 +215,40 @@ module RestClient
|
|
213
215
|
uri.path = "/#{url}".squeeze('/')
|
214
216
|
url = uri.to_s
|
215
217
|
end
|
216
|
-
|
217
218
|
raise Redirect, url
|
218
|
-
elsif res.code == "304"
|
219
|
-
raise NotModified, res
|
220
|
-
elsif res.code == "401"
|
221
|
-
raise Unauthorized, res
|
222
|
-
elsif res.code == "404"
|
223
|
-
raise ResourceNotFound, res
|
224
219
|
else
|
225
|
-
|
220
|
+
if block_given?
|
221
|
+
yield response
|
222
|
+
else
|
223
|
+
response.return!
|
224
|
+
end
|
226
225
|
end
|
227
226
|
end
|
228
227
|
|
229
|
-
def self.decode
|
228
|
+
def self.decode content_encoding, body
|
230
229
|
if content_encoding == 'gzip' and not body.empty?
|
231
230
|
Zlib::GzipReader.new(StringIO.new(body)).read
|
232
231
|
elsif content_encoding == 'deflate'
|
233
|
-
Zlib::Inflate.new.inflate
|
232
|
+
Zlib::Inflate.new.inflate body
|
234
233
|
else
|
235
234
|
body
|
236
235
|
end
|
237
236
|
end
|
238
237
|
|
239
|
-
def
|
238
|
+
def log_request
|
240
239
|
if RestClient.log
|
241
240
|
out = []
|
242
241
|
out << "RestClient.#{method} #{url.inspect}"
|
243
|
-
out <<
|
244
|
-
out <<
|
245
|
-
out.join(', ')
|
242
|
+
out << payload.short_inspect if payload
|
243
|
+
out << processed_headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '')
|
244
|
+
RestClient.log << out.join(', ') + "\n"
|
246
245
|
end
|
247
246
|
end
|
248
247
|
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
def display_log(msg)
|
255
|
-
return unless log_to = RestClient.log
|
256
|
-
|
257
|
-
if log_to == 'stdout'
|
258
|
-
STDOUT.puts msg
|
259
|
-
elsif log_to == 'stderr'
|
260
|
-
STDERR.puts msg
|
261
|
-
else
|
262
|
-
File.open(log_to, 'a') { |f| f.puts msg }
|
248
|
+
def log_response res
|
249
|
+
if RestClient.log
|
250
|
+
size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
|
251
|
+
RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
|
263
252
|
end
|
264
253
|
end
|
265
254
|
|
@@ -274,7 +263,7 @@ module MIME
|
|
274
263
|
|
275
264
|
# Return the first found content-type for a value considered as an extension or the value itself
|
276
265
|
def type_for_extension ext
|
277
|
-
candidates =
|
266
|
+
candidates = @extension_index[ext]
|
278
267
|
candidates.empty? ? ext : candidates[0].content_type
|
279
268
|
end
|
280
269
|
|
data/lib/restclient/resource.rb
CHANGED
@@ -34,10 +34,11 @@ module RestClient
|
|
34
34
|
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
35
35
|
#
|
36
36
|
class Resource
|
37
|
-
attr_reader :url, :options
|
37
|
+
attr_reader :url, :options, :block
|
38
38
|
|
39
|
-
def initialize(url, options={}, backwards_compatibility=nil)
|
39
|
+
def initialize(url, options={}, backwards_compatibility=nil, &block)
|
40
40
|
@url = url
|
41
|
+
@block = block
|
41
42
|
if options.class == Hash
|
42
43
|
@options = options
|
43
44
|
else # compatibility with previous versions
|
@@ -45,38 +46,38 @@ module RestClient
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
def get(additional_headers={}, &
|
49
|
+
def get(additional_headers={}, &block)
|
49
50
|
headers = (options[:headers] || {}).merge(additional_headers)
|
50
51
|
Request.execute(options.merge(
|
51
52
|
:method => :get,
|
52
53
|
:url => url,
|
53
|
-
:headers => headers), &
|
54
|
+
:headers => headers), &(block || @block))
|
54
55
|
end
|
55
56
|
|
56
|
-
def post(payload, additional_headers={}, &
|
57
|
+
def post(payload, additional_headers={}, &block)
|
57
58
|
headers = (options[:headers] || {}).merge(additional_headers)
|
58
59
|
Request.execute(options.merge(
|
59
60
|
:method => :post,
|
60
61
|
:url => url,
|
61
62
|
:payload => payload,
|
62
|
-
:headers => headers), &
|
63
|
+
:headers => headers), &(block || @block))
|
63
64
|
end
|
64
65
|
|
65
|
-
def put(payload, additional_headers={}, &
|
66
|
+
def put(payload, additional_headers={}, &block)
|
66
67
|
headers = (options[:headers] || {}).merge(additional_headers)
|
67
68
|
Request.execute(options.merge(
|
68
69
|
:method => :put,
|
69
70
|
:url => url,
|
70
71
|
:payload => payload,
|
71
|
-
:headers => headers), &
|
72
|
+
:headers => headers), &(block || @block))
|
72
73
|
end
|
73
74
|
|
74
|
-
def delete(additional_headers={}, &
|
75
|
+
def delete(additional_headers={}, &block)
|
75
76
|
headers = (options[:headers] || {}).merge(additional_headers)
|
76
77
|
Request.execute(options.merge(
|
77
78
|
:method => :delete,
|
78
79
|
:url => url,
|
79
|
-
:headers => headers), &
|
80
|
+
:headers => headers), &(block || @block))
|
80
81
|
end
|
81
82
|
|
82
83
|
def to_s
|
data/spec/exceptions_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/base'
|
|
2
2
|
|
3
3
|
describe RestClient::Exception do
|
4
4
|
it "sets the exception message to ErrorMessage" do
|
5
|
-
RestClient::ResourceNotFound.new.message.should == 'Resource
|
5
|
+
RestClient::ResourceNotFound.new.message.should == 'Resource Not Found'
|
6
6
|
end
|
7
7
|
|
8
8
|
it "contains exceptions in RestClient" do
|
@@ -29,10 +29,8 @@ describe RestClient::RequestFailed do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it "http_body convenience method for fetching the body (decoding when necessary)" do
|
32
|
-
@response
|
33
|
-
@response
|
34
|
-
RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body')
|
35
|
-
RestClient::RequestFailed.new(@response).http_body.should == 'plain body'
|
32
|
+
RestClient::RequestFailed.new(@response).http_code.should == 502
|
33
|
+
RestClient::RequestFailed.new(@response).message.should == 'HTTP status code 502'
|
36
34
|
end
|
37
35
|
|
38
36
|
it "shows the status code in the message" do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
require 'webmock/rspec'
|
4
|
+
include WebMock
|
5
|
+
|
6
|
+
describe RestClient do
|
7
|
+
|
8
|
+
it "a simple request" do
|
9
|
+
body = 'abc'
|
10
|
+
stub_request(:get, "www.example.com").to_return(:body => body, :status => 200)
|
11
|
+
response = RestClient.get "www.example.com"
|
12
|
+
response.code.should == 200
|
13
|
+
response.should == body
|
14
|
+
end
|
15
|
+
|
16
|
+
it "a simple request with gzipped content" do
|
17
|
+
stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } )
|
18
|
+
response = RestClient.get "www.example.com"
|
19
|
+
response.code.should == 200
|
20
|
+
response.should == "i'm gziped\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "a 404" do
|
24
|
+
body = "Ho hai ! I'm not here !"
|
25
|
+
stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
|
26
|
+
begin
|
27
|
+
RestClient.get "www.example.com"
|
28
|
+
raise
|
29
|
+
rescue RestClient::ResourceNotFound => e
|
30
|
+
e.http_code.should == 404
|
31
|
+
e.response.code.should == 404
|
32
|
+
e.response.should == body
|
33
|
+
e.http_body.should == body
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
end
|
data/spec/mixin/response_spec.rb
CHANGED
data/spec/payload_spec.rb
CHANGED
@@ -39,11 +39,11 @@ describe RestClient::Payload do
|
|
39
39
|
m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]])
|
40
40
|
m.to_s.should == <<-EOS
|
41
41
|
--#{m.boundary}\r
|
42
|
-
Content-Disposition:
|
42
|
+
Content-Disposition: form-data; name="bar"\r
|
43
43
|
\r
|
44
44
|
baz\r
|
45
45
|
--#{m.boundary}\r
|
46
|
-
Content-Disposition:
|
46
|
+
Content-Disposition: form-data; name="foo"\r
|
47
47
|
\r
|
48
48
|
bar\r
|
49
49
|
--#{m.boundary}--\r
|
@@ -55,7 +55,7 @@ bar\r
|
|
55
55
|
m = RestClient::Payload::Multipart.new({:foo => f})
|
56
56
|
m.to_s.should == <<-EOS
|
57
57
|
--#{m.boundary}\r
|
58
|
-
Content-Disposition:
|
58
|
+
Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r
|
59
59
|
Content-Type: image/jpeg\r
|
60
60
|
\r
|
61
61
|
#{IO.read(f.path)}\r
|
@@ -70,7 +70,7 @@ Content-Type: image/jpeg\r
|
|
70
70
|
m = RestClient::Payload::Multipart.new({:foo => f})
|
71
71
|
m.to_s.should == <<-EOS
|
72
72
|
--#{m.boundary}\r
|
73
|
-
Content-Disposition:
|
73
|
+
Content-Disposition: form-data; name="foo"; filename="foo.txt"\r
|
74
74
|
Content-Type: text/plain\r
|
75
75
|
\r
|
76
76
|
#{IO.read(f.path)}\r
|
@@ -82,7 +82,7 @@ Content-Type: text/plain\r
|
|
82
82
|
m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}})
|
83
83
|
m.to_s.should == <<-EOS
|
84
84
|
--#{m.boundary}\r
|
85
|
-
Content-Disposition:
|
85
|
+
Content-Disposition: form-data; name="bar[baz]"\r
|
86
86
|
\r
|
87
87
|
foo\r
|
88
88
|
--#{m.boundary}--\r
|
@@ -94,7 +94,7 @@ foo\r
|
|
94
94
|
m = RestClient::Payload::Multipart.new({:foo => {:bar => f}})
|
95
95
|
m.to_s.should == <<-EOS
|
96
96
|
--#{m.boundary}\r
|
97
|
-
Content-Disposition:
|
97
|
+
Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r
|
98
98
|
Content-Type: text/plain\r
|
99
99
|
\r
|
100
100
|
#{IO.read(f.path)}\r
|