rest-client 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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