rest-client 1.8.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,7 +14,7 @@ module RestClient
14
14
  #
15
15
  # With a timeout (seconds):
16
16
  #
17
- # RestClient::Resource.new('http://slow', :timeout => 10)
17
+ # RestClient::Resource.new('http://slow', :read_timeout => 10)
18
18
  #
19
19
  # With an open timeout (seconds):
20
20
  #
@@ -113,8 +113,8 @@ module RestClient
113
113
  options[:headers] || {}
114
114
  end
115
115
 
116
- def timeout
117
- options[:timeout]
116
+ def read_timeout
117
+ options[:read_timeout]
118
118
  end
119
119
 
120
120
  def open_timeout
@@ -2,20 +2,76 @@ module RestClient
2
2
 
3
3
  # A Response from RestClient, you can access the response body, the code or the headers.
4
4
  #
5
- module Response
5
+ class Response < String
6
6
 
7
7
  include AbstractResponse
8
8
 
9
+ # Return the HTTP response body.
10
+ #
11
+ # Future versions of RestClient will deprecate treating response objects
12
+ # directly as strings, so it will be necessary to call `.body`.
13
+ #
14
+ # @return [String]
15
+ #
9
16
  def body
10
- self
17
+ # Benchmarking suggests that "#{self}" is fastest, and that caching the
18
+ # body string in an instance variable doesn't make it enough faster to be
19
+ # worth the extra memory storage.
20
+ String.new(self)
11
21
  end
12
22
 
13
- def self.create body, net_http_res, args, request
14
- result = body || ''
15
- result.extend Response
23
+ # Convert the HTTP response body to a pure String object.
24
+ #
25
+ # @return [String]
26
+ def to_s
27
+ body
28
+ end
29
+
30
+ # Convert the HTTP response body to a pure String object.
31
+ #
32
+ # @return [String]
33
+ def to_str
34
+ body
35
+ end
36
+
37
+ def inspect
38
+ "<RestClient::Response #{code.inspect} #{body_truncated(10).inspect}>"
39
+ end
40
+
41
+ def self.create(body, net_http_res, args, request)
42
+ result = self.new(body || '')
43
+
16
44
  result.response_set_vars(net_http_res, args, request)
45
+ fix_encoding(result)
46
+
17
47
  result
18
48
  end
19
49
 
50
+ private
51
+
52
+ def self.fix_encoding(response)
53
+ charset = RestClient::Utils.get_encoding_from_headers(response.headers)
54
+ encoding = nil
55
+
56
+ begin
57
+ encoding = Encoding.find(charset) if charset
58
+ rescue ArgumentError
59
+ RestClient.log << "No such encoding: #{charset.inspect}"
60
+ end
61
+
62
+ return unless encoding
63
+
64
+ response.force_encoding(encoding)
65
+
66
+ response
67
+ end
68
+
69
+ def body_truncated(length)
70
+ if body.length > length
71
+ body[0..length] + '...'
72
+ else
73
+ body
74
+ end
75
+ end
20
76
  end
21
77
  end
@@ -0,0 +1,93 @@
1
+ module RestClient
2
+ # Various utility methods
3
+ module Utils
4
+
5
+ # Return encoding from an HTTP header hash.
6
+ #
7
+ # We use the RFC 7231 specification and do not impose a default encoding on
8
+ # text. This differs from the older RFC 2616 behavior, which specifies
9
+ # using ISO-8859-1 for text/* content types without a charset.
10
+ #
11
+ # Strings will effectively end up using `Encoding.default_external` when
12
+ # this method returns nil.
13
+ #
14
+ # @param headers [Hash]
15
+ #
16
+ # @return [String, nil] encoding Return the string encoding or nil if no
17
+ # header is found.
18
+ #
19
+ def self.get_encoding_from_headers(headers)
20
+ type_header = headers[:content_type]
21
+ return nil unless type_header
22
+
23
+ _content_type, params = cgi_parse_header(type_header)
24
+
25
+ if params.include?('charset')
26
+ return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
27
+ end
28
+
29
+ nil
30
+ end
31
+
32
+ # Parse semi-colon separated, potentially quoted header string iteratively.
33
+ #
34
+ # @private
35
+ #
36
+ def self._cgi_parseparam(s)
37
+ return enum_for(__method__, s) unless block_given?
38
+
39
+ while s[0] == ';'
40
+ s = s[1..-1]
41
+ ends = s.index(';')
42
+ while ends && ends > 0 \
43
+ && (s[0...ends].count('"') -
44
+ s[0...ends].scan('\"').count) % 2 != 0
45
+ ends = s.index(';', ends + 1)
46
+ end
47
+ if ends.nil?
48
+ ends = s.length
49
+ end
50
+ f = s[0...ends]
51
+ yield f.strip
52
+ s = s[ends..-1]
53
+ end
54
+ nil
55
+ end
56
+
57
+ # Parse a Content-type like header.
58
+ #
59
+ # Return the main content-type and a hash of options.
60
+ #
61
+ # This method was ported directly from Python's cgi.parse_header(). It
62
+ # probably doesn't read or perform particularly well in ruby.
63
+ # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331
64
+ #
65
+ #
66
+ # @param [String] line
67
+ # @return [Array(String, Hash)]
68
+ #
69
+ def self.cgi_parse_header(line)
70
+ parts = _cgi_parseparam(';' + line)
71
+ key = parts.next
72
+ pdict = {}
73
+
74
+ begin
75
+ while (p = parts.next)
76
+ i = p.index('=')
77
+ if i
78
+ name = p[0...i].strip.downcase
79
+ value = p[i+1..-1].strip
80
+ if value.length >= 2 && value[0] == '"' && value[-1] == '"'
81
+ value = value[1...-1]
82
+ value = value.gsub('\\\\', '\\').gsub('\\"', '"')
83
+ end
84
+ pdict[name] = value
85
+ end
86
+ end
87
+ rescue StopIteration
88
+ end
89
+
90
+ [key, pdict]
91
+ end
92
+ end
93
+ end
@@ -1,5 +1,6 @@
1
1
  module RestClient
2
- VERSION = '1.8.0' unless defined?(self::VERSION)
2
+ VERSION_INFO = [2, 0, 0, 'rc1'] unless defined?(self::VERSION_INFO)
3
+ VERSION = VERSION_INFO.map(&:to_s).join('.') unless defined?(self::VERSION)
3
4
 
4
5
  def self.version
5
6
  VERSION
@@ -17,14 +17,15 @@ Gem::Specification.new do |s|
17
17
  s.summary = 'Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.'
18
18
 
19
19
  s.add_development_dependency('webmock', '~> 1.4')
20
- s.add_development_dependency('rspec', '~> 2.4')
21
- s.add_development_dependency('pry')
22
- s.add_development_dependency('pry-doc')
20
+ s.add_development_dependency('rspec', '~> 2.99')
21
+ s.add_development_dependency('pry', '~> 0')
22
+ s.add_development_dependency('pry-doc', '~> 0')
23
23
  s.add_development_dependency('rdoc', '>= 2.4.2', '< 5.0')
24
+ s.add_development_dependency('rubocop', '~> 0')
24
25
 
25
26
  s.add_dependency('http-cookie', '>= 1.0.2', '< 2.0')
26
27
  s.add_dependency('mime-types', '>= 1.16', '< 3.0')
27
- s.add_dependency('netrc', '~> 0.7')
28
+ s.add_dependency('netrc', '~> 0.8')
28
29
 
29
- s.required_ruby_version = '>= 1.9.2'
30
+ s.required_ruby_version = '>= 1.9.3'
30
31
  end
@@ -0,0 +1,14 @@
1
+ module Helpers
2
+ def response_double(opts={})
3
+ double('response', {:to_hash => {}}.merge(opts))
4
+ end
5
+
6
+ def fake_stderr
7
+ original_stderr = $stderr
8
+ $stderr = StringIO.new
9
+ yield
10
+ $stderr.string
11
+ ensure
12
+ $stderr = original_stderr
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+ require_relative '../spec_helper'
@@ -0,0 +1,72 @@
1
+ require_relative '_lib'
2
+ require 'json'
3
+
4
+ describe RestClient::Request do
5
+ before(:all) do
6
+ WebMock.disable!
7
+ end
8
+
9
+ after(:all) do
10
+ WebMock.enable!
11
+ end
12
+
13
+ def default_httpbin_url
14
+ # add a hack to work around java/jruby bug
15
+ # java.lang.RuntimeException: Could not generate DH keypair with backtrace
16
+ if ENV['TRAVIS_RUBY_VERSION'] == 'jruby-19mode'
17
+ 'http://httpbin.org/'
18
+ else
19
+ 'https://httpbin.org/'
20
+ end
21
+ end
22
+
23
+ def httpbin(suffix='')
24
+ url = ENV.fetch('HTTPBIN_URL', default_httpbin_url)
25
+ unless url.end_with?('/')
26
+ url += '/'
27
+ end
28
+
29
+ url + suffix
30
+ end
31
+
32
+ def execute_httpbin(suffix, opts={})
33
+ opts = {url: httpbin(suffix)}.merge(opts)
34
+ RestClient::Request.execute(opts)
35
+ end
36
+
37
+ def execute_httpbin_json(suffix, opts={})
38
+ JSON.parse(execute_httpbin(suffix, opts))
39
+ end
40
+
41
+ describe '.execute' do
42
+ it 'sends a user agent' do
43
+ data = execute_httpbin_json('user-agent', method: :get)
44
+ data['user-agent'].should match(/rest-client/)
45
+ end
46
+
47
+ it 'receives cookies on 302' do
48
+ expect {
49
+ execute_httpbin('cookies/set?foo=bar', method: :get, max_redirects: 0)
50
+ }.to raise_error(RestClient::Found) { |ex|
51
+ ex.http_code.should eq 302
52
+ ex.response.cookies['foo'].should eq 'bar'
53
+ }
54
+ end
55
+
56
+ it 'passes along cookies through 302' do
57
+ data = execute_httpbin_json('cookies/set?foo=bar', method: :get)
58
+ data.should have_key('cookies')
59
+ data['cookies']['foo'].should eq 'bar'
60
+ end
61
+
62
+ it 'handles quote wrapped cookies' do
63
+ expect {
64
+ execute_httpbin('cookies/set?foo=' + CGI.escape('"bar:baz"'),
65
+ method: :get, max_redirects: 0)
66
+ }.to raise_error(RestClient::Found) { |ex|
67
+ ex.http_code.should eq 302
68
+ ex.response.cookies['foo'].should eq '"bar:baz"'
69
+ }
70
+ end
71
+ end
72
+ end
@@ -1,4 +1,6 @@
1
- require 'spec_helper'
1
+ # -*- coding: utf-8 -*-
2
+ require_relative '_lib'
3
+ require 'base64'
2
4
 
3
5
  describe RestClient do
4
6
 
@@ -31,5 +33,81 @@ describe RestClient do
31
33
  end
32
34
  end
33
35
 
36
+ describe 'charset parsing' do
37
+ it 'handles utf-8' do
38
+ body = "λ".force_encoding('ASCII-8BIT')
39
+ stub_request(:get, "www.example.com").to_return(
40
+ :body => body, :status => 200, :headers => {
41
+ 'Content-Type' => 'text/plain; charset=UTF-8'
42
+ })
43
+ response = RestClient.get "www.example.com"
44
+ response.encoding.should eq Encoding::UTF_8
45
+ response.valid_encoding?.should eq true
46
+ end
47
+
48
+ it 'handles windows-1252' do
49
+ body = "\xff".force_encoding('ASCII-8BIT')
50
+ stub_request(:get, "www.example.com").to_return(
51
+ :body => body, :status => 200, :headers => {
52
+ 'Content-Type' => 'text/plain; charset=windows-1252'
53
+ })
54
+ response = RestClient.get "www.example.com"
55
+ response.encoding.should eq Encoding::WINDOWS_1252
56
+ response.encode('utf-8').should eq "ÿ"
57
+ response.valid_encoding?.should eq true
58
+ end
59
+
60
+ it 'handles binary' do
61
+ body = "\xfe".force_encoding('ASCII-8BIT')
62
+ stub_request(:get, "www.example.com").to_return(
63
+ :body => body, :status => 200, :headers => {
64
+ 'Content-Type' => 'application/octet-stream; charset=binary'
65
+ })
66
+ response = RestClient.get "www.example.com"
67
+ response.encoding.should eq Encoding::BINARY
68
+ lambda {
69
+ response.encode('utf-8')
70
+ }.should raise_error(Encoding::UndefinedConversionError)
71
+ response.valid_encoding?.should eq true
72
+ end
73
+
74
+ it 'handles euc-jp' do
75
+ body = "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA".
76
+ force_encoding(Encoding::BINARY)
77
+ body_utf8 = 'あいうえお'
78
+ body_utf8.encoding.should eq Encoding::UTF_8
79
+
80
+ stub_request(:get, 'www.example.com').to_return(
81
+ :body => body, :status => 200, :headers => {
82
+ 'Content-Type' => 'text/plain; charset=EUC-JP'
83
+ })
84
+ response = RestClient.get 'www.example.com'
85
+ response.encoding.should eq Encoding::EUC_JP
86
+ response.valid_encoding?.should eq true
87
+ response.length.should eq 5
88
+ response.encode('utf-8').should eq body_utf8
89
+ end
34
90
 
91
+ it 'defaults to Encoding.default_external' do
92
+ stub_request(:get, 'www.example.com').to_return(
93
+ body: 'abc', status: 200, headers: {
94
+ 'Content-Type' => 'text/plain'
95
+ })
96
+
97
+ response = RestClient.get 'www.example.com'
98
+ response.encoding.should eq Encoding.default_external
99
+ end
100
+
101
+ it 'leaves images as binary' do
102
+ gif = Base64.strict_decode64('R0lGODlhAQABAAAAADs=')
103
+
104
+ stub_request(:get, 'www.example.com').to_return(
105
+ body: gif, status: 200, headers: {
106
+ 'Content-Type' => 'image/gif'
107
+ })
108
+
109
+ response = RestClient.get 'www.example.com'
110
+ response.encoding.should eq Encoding::BINARY
111
+ end
112
+ end
35
113
  end
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ require_relative '_lib'
2
2
 
3
3
  describe RestClient::Request do
4
4
  before(:all) do
@@ -101,4 +101,27 @@ describe RestClient::Request do
101
101
  expect { request.execute }.to_not raise_error
102
102
  end
103
103
  end
104
+
105
+ describe "timeouts" do
106
+ it "raises OpenTimeout when it hits an open timeout" do
107
+ request = RestClient::Request.new(
108
+ :method => :get,
109
+ :url => 'http://www.mozilla.org',
110
+ :open_timeout => 1e-10,
111
+ )
112
+ expect { request.execute }.to(
113
+ raise_error(RestClient::Exceptions::OpenTimeout))
114
+ end
115
+
116
+ it "raises ReadTimeout when it hits a read timeout via :read_timeout" do
117
+ request = RestClient::Request.new(
118
+ :method => :get,
119
+ :url => 'https://www.mozilla.org',
120
+ :read_timeout => 1e-10,
121
+ )
122
+ expect { request.execute }.to(
123
+ raise_error(RestClient::Exceptions::ReadTimeout))
124
+ end
125
+ end
126
+
104
127
  end
@@ -1,2 +1,22 @@
1
1
  require 'webmock/rspec'
2
- require 'restclient'
2
+ require 'rest-client'
3
+
4
+ require_relative './helpers'
5
+
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+
10
+ # Run specs in random order to surface order dependencies. If you find an
11
+ # order dependency and want to debug it, you can fix the order by providing
12
+ # the seed, which is printed after each run.
13
+ # --seed 1234
14
+ config.order = 'random'
15
+
16
+ # add helpers
17
+ config.include Helpers, :include_helpers
18
+
19
+ config.mock_with :rspec do |mocks|
20
+ mocks.yield_receiver_to_any_instance_implementation_blocks = true
21
+ end
22
+ end