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.

@@ -3,8 +3,7 @@ module RestClient
3
3
  module Response
4
4
  attr_reader :net_http_res
5
5
 
6
- # HTTP status code, always 200 since RestClient throws exceptions for
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] || "").split('; ').inject({}) do |out, raw_c|
26
- key, val = raw_c.split('=')
27
- unless %w(expires domain path secure).member?(key)
28
- out[key] = val
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
@@ -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: multipart/form-data; name=\"#{k}\"")
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: multipart/form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
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)
@@ -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
- def self.execute(args)
29
- new(args).execute
29
+
30
+ def self.execute(args, &block)
31
+ new(args).execute &block
30
32
  end
31
33
 
32
- def initialize(args)
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
- # Accept can be composed of several comma-separated values
77
- if value.is_a? Array
78
- target_values = value
79
- else
80
- target_values = value.to_s.split ','
81
- end
82
- final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ')
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(uri, req, payload)
136
- setup_credentials(req)
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
- display_log request_log
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
- result = process_result(res)
156
- display_log response_log(res)
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(chunk)
180
+ @tf.write chunk
185
181
  size += chunk.size
186
- if size == 0
187
- display_log("#{@method} #{@url} done (0 length file)")
188
- elsif total == 0
189
- display_log("#{@method} #{@url} (zero content length)")
190
- else
191
- display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
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(res)
203
- if res.code =~ /\A2\d{2}\z/
200
+ def process_result res
201
+ if @raw_response
204
202
  # We don't decode raw requests
205
- unless @raw_response
206
- self.class.decode res['content-encoding'], res.body if res.body
207
- end
208
- elsif %w(301 302 303).include? res.code
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
- raise RequestFailed, res
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(content_encoding, body)
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(body)
232
+ Zlib::Inflate.new.inflate body
234
233
  else
235
234
  body
236
235
  end
237
236
  end
238
237
 
239
- def request_log
238
+ def log_request
240
239
  if RestClient.log
241
240
  out = []
242
241
  out << "RestClient.#{method} #{url.inspect}"
243
- out << "headers: #{processed_headers.inspect}"
244
- out << "paylod: #{payload.short_inspect}" if payload
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 response_log(res)
250
- size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
251
- "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
252
- end
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 = @extension_index[ext]
266
+ candidates = @extension_index[ext]
278
267
  candidates.empty? ? ext : candidates[0].content_type
279
268
  end
280
269
 
@@ -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={}, &b)
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), &b)
54
+ :headers => headers), &(block || @block))
54
55
  end
55
56
 
56
- def post(payload, additional_headers={}, &b)
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), &b)
63
+ :headers => headers), &(block || @block))
63
64
  end
64
65
 
65
- def put(payload, additional_headers={}, &b)
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), &b)
72
+ :headers => headers), &(block || @block))
72
73
  end
73
74
 
74
- def delete(additional_headers={}, &b)
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), &b)
80
+ :headers => headers), &(block || @block))
80
81
  end
81
82
 
82
83
  def to_s
@@ -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 not found'
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.stub!(:[]).with('content-encoding').and_return('gzip')
33
- @response.stub!(:body).and_return('compressed body')
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
@@ -5,7 +5,7 @@ class MockResponse
5
5
 
6
6
  def initialize(body, res)
7
7
  @net_http_res = res
8
- @body = @body
8
+ @body = body
9
9
  end
10
10
  end
11
11
 
@@ -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: multipart/form-data; name="bar"\r
42
+ Content-Disposition: form-data; name="bar"\r
43
43
  \r
44
44
  baz\r
45
45
  --#{m.boundary}\r
46
- Content-Disposition: multipart/form-data; name="foo"\r
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: multipart/form-data; name="foo"; filename="master_shake.jpg"\r
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: multipart/form-data; name="foo"; filename="foo.txt"\r
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: multipart/form-data; name="bar[baz]"\r
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: multipart/form-data; name="foo[bar]"; filename="foo.txt"\r
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