rest-client 2.0.0.rc2 → 2.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -127,6 +127,6 @@ Rake::RDocTask.new do |t|
127
127
  t.title = "rest-client, fetch RESTful resources effortlessly"
128
128
  t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
129
129
  t.options << '--charset' << 'utf-8'
130
- t.rdoc_files.include('README.rdoc')
130
+ t.rdoc_files.include('README.md')
131
131
  t.rdoc_files.include('lib/*.rb')
132
132
  end
data/history.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  This release is largely API compatible, but makes several breaking changes.
4
4
 
5
- - Drop support for Ruby 1.9.2
5
+ - Drop support for Ruby 1.9
6
+ - Allow mime-types as new as 3.x (requires ruby 2.0)
6
7
  - Respect Content-Type charset header provided by server. Previously,
7
8
  rest-client would not override the string encoding chosen by Net::HTTP. Now
8
9
  responses that specify a charset will yield a body string in that encoding.
@@ -20,8 +21,8 @@ This release is largely API compatible, but makes several breaking changes.
20
21
  inherits from `ExceptionWithResponse`. Previously, HTTP 304, 401, and 404
21
22
  inherited directly from `ExceptionWithResponse` rather than from
22
23
  `RequestFailed`. Now _all_ HTTP status code exceptions inherit from both.
23
- - Rename `:timeout` to `:read_timeout`. When `:timeout` is passed, now set both
24
- `:read_timeout` and `:open_timeout`.
24
+ - Rename the `:timeout` request option to `:read_timeout`. When `:timeout` is
25
+ passed, now set both `:read_timeout` and `:open_timeout`.
25
26
  - Change default HTTP Accept header to `*/*`
26
27
  - Use a more descriptive User-Agent header by default
27
28
  - Drop RC4-MD5 from default cipher list
@@ -37,12 +38,30 @@ This release is largely API compatible, but makes several breaking changes.
37
38
  - `Response#to_i` will now behave like `String#to_i` instead of returning the
38
39
  HTTP response code, which was very surprising behavior.
39
40
  - `Response#body` and `#to_s` will now return a true `String` object rather
40
- than self. Previously there was no easy way to get the true `String` response
41
- instead of the Frankenstein response string object with AbstractResponse
42
- mixed in.
41
+ than self. Previously there was no easy way to get the true `String`
42
+ response instead of the Frankenstein response string object with
43
+ AbstractResponse mixed in.
44
+ - Response objects no longer accept an extra request args hash, but instead
45
+ access request args directly from the request object, which reduces
46
+ confusion and duplication.
43
47
  - Handle multiple HTTP response headers with the same name (except for
44
48
  Set-Cookie, which is special) by joining the values with a comma space,
45
49
  compliant with RFC 7230
50
+ - Rewrite cookie support to be much smarter and to use cookie jars consistently
51
+ for requests, responses, and redirection (resolving long-standing complaints
52
+ about the previously broken behavior):
53
+ - The `:cookies` option may now be a Hash of Strings, an Array of
54
+ HTTP::Cookie objects, or a full HTTP::CookieJar.
55
+ - Add `RestClient::Request#cookie_jar` and reimplement `Request#cookies` to
56
+ be a wrapper around the cookie jar.
57
+ - Still support passing the `:cookies` option in the headers hash, but now
58
+ raise ArgumentError if that option is also passed to `Request#initialize`.
59
+ - Warn if both `:cookies` and a `Cookie` header are supplied.
60
+ - Use the `Request#cookie_jar` as the basis for `Response#cookie_jar`,
61
+ creating a copy of the jar and adding any newly received cookies.
62
+ - When following redirection, also use this same strategy so that cookies
63
+ from the original request are carried through in a standards-compliant way
64
+ by the cookie jar.
46
65
  - Don't set basic auth header if explicit `Authorization` header is specified
47
66
  - Add `:proxy` option to requests, which can be used for thread-safe
48
67
  per-request proxy configuration, overriding `RestClient.proxy`
@@ -54,16 +73,29 @@ This release is largely API compatible, but makes several breaking changes.
54
73
  treat any object that responds to `.read` as a streaming payload and pass it
55
74
  through to `.body_stream=` on the Net:HTTP object. This massively reduces the
56
75
  memory required for large file uploads.
57
- - Remove `RestClient::MaxRedirectsReached` in favor of the normal
58
- `ExceptionWithResponse` subclasses. This makes the response accessible on the
59
- exception object as `.response`, making it possible for callers to tell what
60
- has actually happened when the redirect limit is reached.
61
- - When following HTTP redirection, store a list of each previous response on
62
- the response object as `.history`. This makes it possible to access the
63
- original response headers and body before the redirection was followed.
76
+ - Changes to redirection behavior:
77
+ - Remove `RestClient::MaxRedirectsReached` in favor of the normal
78
+ `ExceptionWithResponse` subclasses. This makes the response accessible on
79
+ the exception object as `.response`, making it possible for callers to tell
80
+ what has actually happened when the redirect limit is reached.
81
+ - When following HTTP redirection, store a list of each previous response on
82
+ the response object as `.history`. This makes it possible to access the
83
+ original response headers and body before the redirection was followed.
84
+ - Follow redirection consistently, regardless of whether the HTTP method was
85
+ passed as a symbol or string. Under the hood rest-client now normalizes the
86
+ HTTP request method to a lowercase string.
64
87
  - Add `:before_execution_proc` option to `RestClient::Request`. This makes it
65
88
  possible to add procs like `RestClient.add_before_execution_proc` to a single
66
89
  request without global state.
90
+ - Run tests on Travis's beta OS X support.
91
+ - Make `Request#transmit` a private method, along with a few others.
92
+ - Refactor URI parsing to happen earlier, in Request initialization.
93
+ - When adding URL params, handle URLs that already contain params.
94
+ - Add a few more exception classes for obscure HTTP status codes.
95
+ - Multipart: use a much more robust multipart boundary with greater entropy.
96
+ - Make `RestClient::Payload::Base#inspect` stop pretending to be a String.
97
+ - Add `Request#redacted_uri` and `Request#redacted_url` to display the URI
98
+ with any password redacted.
67
99
 
68
100
  # 2.0.0.rc1
69
101
 
@@ -13,6 +13,7 @@ require File.dirname(__FILE__) + '/restclient/abstract_response'
13
13
  require File.dirname(__FILE__) + '/restclient/response'
14
14
  require File.dirname(__FILE__) + '/restclient/raw_response'
15
15
  require File.dirname(__FILE__) + '/restclient/resource'
16
+ require File.dirname(__FILE__) + '/restclient/params_array'
16
17
  require File.dirname(__FILE__) + '/restclient/payload'
17
18
  require File.dirname(__FILE__) + '/restclient/windows'
18
19
 
@@ -93,8 +94,9 @@ module RestClient
93
94
  # A global proxy URL to use for all requests. This can be overridden on a
94
95
  # per-request basis by passing `:proxy` to RestClient::Request.
95
96
  def self.proxy
96
- @proxy
97
+ @proxy ||= nil
97
98
  end
99
+
98
100
  def self.proxy=(value)
99
101
  @proxy = value
100
102
  @proxy_set = true
@@ -106,7 +108,7 @@ module RestClient
106
108
  # @return [Boolean]
107
109
  #
108
110
  def self.proxy_set?
109
- !!@proxy_set
111
+ @proxy_set ||= false
110
112
  end
111
113
 
112
114
  # Setup the log for RestClient calls.
@@ -5,7 +5,7 @@ module RestClient
5
5
 
6
6
  module AbstractResponse
7
7
 
8
- attr_reader :net_http_res, :args, :request
8
+ attr_reader :net_http_res, :request
9
9
 
10
10
  def inspect
11
11
  raise NotImplementedError.new('must override in subclass')
@@ -31,20 +31,28 @@ module RestClient
31
31
  @raw_headers ||= @net_http_res.to_hash
32
32
  end
33
33
 
34
- def response_set_vars(net_http_res, args, request)
34
+ def response_set_vars(net_http_res, request)
35
35
  @net_http_res = net_http_res
36
- @args = args
37
36
  @request = request
38
37
 
39
38
  # prime redirection history
40
39
  history
41
40
  end
42
41
 
43
- # Hash of cookies extracted from response headers
42
+ # Hash of cookies extracted from response headers.
43
+ #
44
+ # NB: This will return only cookies whose domain matches this request, and
45
+ # may not even return all of those cookies if there are duplicate names.
46
+ # Use the full cookie_jar for more nuanced access.
47
+ #
48
+ # @see #cookie_jar
49
+ #
50
+ # @return [Hash]
51
+ #
44
52
  def cookies
45
53
  hash = {}
46
54
 
47
- cookie_jar.cookies.each do |cookie|
55
+ cookie_jar.cookies(@request.uri).each do |cookie|
48
56
  hash[cookie.name] = cookie.value
49
57
  end
50
58
 
@@ -56,28 +64,40 @@ module RestClient
56
64
  # @return [HTTP::CookieJar]
57
65
  #
58
66
  def cookie_jar
59
- return @cookie_jar if @cookie_jar
67
+ return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
60
68
 
61
- jar = HTTP::CookieJar.new
69
+ jar = @request.cookie_jar.dup
62
70
  headers.fetch(:set_cookie, []).each do |cookie|
63
- jar.parse(cookie, @request.url)
71
+ jar.parse(cookie, @request.uri)
64
72
  end
65
73
 
66
74
  @cookie_jar = jar
67
75
  end
68
76
 
69
77
  # Return the default behavior corresponding to the response code:
70
- # the response itself for code in 200..206, redirection for 301, 302 and 307 in get and head cases, redirection for 303 and an exception in other cases
78
+ #
79
+ # For 20x status codes: return the response itself
80
+ #
81
+ # For 30x status codes:
82
+ # 301, 302, 307: redirect GET / HEAD if there is a Location header
83
+ # 303: redirect, changing method to GET, if there is a Location header
84
+ #
85
+ # For all other responses, raise a response exception
86
+ #
71
87
  def return!(&block)
72
- if (200..207).include? code
88
+ case code
89
+ when 200..207
73
90
  self
74
- elsif [301, 302, 307].include? code
75
- unless [:get, :head].include? args[:method]
76
- raise exception_with_response
77
- else
91
+ when 301, 302, 307
92
+ case request.method
93
+ when 'get', 'head'
94
+ check_max_redirects
78
95
  follow_redirection(&block)
96
+ else
97
+ raise exception_with_response
79
98
  end
80
- elsif code == 303
99
+ when 303
100
+ check_max_redirects
81
101
  follow_get_redirection(&block)
82
102
  else
83
103
  raise exception_with_response
@@ -96,13 +116,13 @@ module RestClient
96
116
  # Follow a redirection response by making a new HTTP request to the
97
117
  # redirection target.
98
118
  def follow_redirection(&block)
99
- _follow_redirection(@args.dup, &block)
119
+ _follow_redirection(request.args.dup, &block)
100
120
  end
101
121
 
102
122
  # Follow a redirection response, but change the HTTP method to GET and drop
103
123
  # the payload from the original request.
104
124
  def follow_get_redirection(&block)
105
- new_args = @args.dup
125
+ new_args = request.args.dup
106
126
  new_args[:method] = :get
107
127
  new_args.delete(:payload)
108
128
 
@@ -132,7 +152,7 @@ module RestClient
132
152
  #
133
153
  def self.beautify_headers(headers)
134
154
  headers.inject({}) do |out, (key, value)|
135
- key_sym = key.gsub(/-/, '_').downcase.to_sym
155
+ key_sym = key.tr('-', '_').downcase.to_sym
136
156
 
137
157
  # Handle Set-Cookie specially since it cannot be joined by comma.
138
158
  if key.downcase == 'set-cookie'
@@ -158,24 +178,24 @@ module RestClient
158
178
  # parse location header and merge into existing URL
159
179
  url = headers[:location]
160
180
 
181
+ # cannot follow redirection if there is no location header
182
+ unless url
183
+ raise exception_with_response
184
+ end
185
+
161
186
  # handle relative redirects
162
187
  unless url.start_with?('http')
163
188
  url = URI.parse(request.url).merge(url).to_s
164
189
  end
165
190
  new_args[:url] = url
166
191
 
167
- if request.max_redirects <= 0
168
- raise exception_with_response
169
- end
170
192
  new_args[:password] = request.password
171
193
  new_args[:user] = request.user
172
194
  new_args[:headers] = request.headers
173
195
  new_args[:max_redirects] = request.max_redirects - 1
174
196
 
175
- # TODO: figure out what to do with original :cookie, :cookies values
176
- new_args[:headers]['Cookie'] = HTTP::Cookie.cookie_value(
177
- cookie_jar.cookies(new_args.fetch(:url)))
178
-
197
+ # pass through our new cookie jar
198
+ new_args[:cookies] = cookie_jar
179
199
 
180
200
  # prepare new request
181
201
  new_req = Request.new(new_args)
@@ -187,6 +207,12 @@ module RestClient
187
207
  new_req.execute(&block)
188
208
  end
189
209
 
210
+ def check_max_redirects
211
+ if request.max_redirects <= 0
212
+ raise exception_with_response
213
+ end
214
+ end
215
+
190
216
  def exception_with_response
191
217
  begin
192
218
  klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
@@ -1,5 +1,19 @@
1
1
  module RestClient
2
2
 
3
+ # Hash of HTTP status code => message.
4
+ #
5
+ # 1xx: Informational - Request received, continuing process
6
+ # 2xx: Success - The action was successfully received, understood, and
7
+ # accepted
8
+ # 3xx: Redirection - Further action must be taken in order to complete the
9
+ # request
10
+ # 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
11
+ # 5xx: Server Error - The server failed to fulfill an apparently valid
12
+ # request
13
+ #
14
+ # @see
15
+ # http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
16
+ #
3
17
  STATUSES = {100 => 'Continue',
4
18
  101 => 'Switching Protocols',
5
19
  102 => 'Processing', #WebDAV
@@ -12,6 +26,8 @@ module RestClient
12
26
  205 => 'Reset Content',
13
27
  206 => 'Partial Content',
14
28
  207 => 'Multi-Status', #WebDAV
29
+ 208 => 'Already Reported', # RFC5842
30
+ 226 => 'IM Used', # RFC3229
15
31
 
16
32
  300 => 'Multiple Choices',
17
33
  301 => 'Moved Permanently',
@@ -21,6 +37,7 @@ module RestClient
21
37
  305 => 'Use Proxy', # http/1.1
22
38
  306 => 'Switch Proxy', # no longer used
23
39
  307 => 'Temporary Redirect', # http/1.1
40
+ 308 => 'Permanent Redirect', # RFC7538
24
41
 
25
42
  400 => 'Bad Request',
26
43
  401 => 'Unauthorized',
@@ -35,10 +52,10 @@ module RestClient
35
52
  410 => 'Gone',
36
53
  411 => 'Length Required',
37
54
  412 => 'Precondition Failed',
38
- 413 => 'Request Entity Too Large',
39
- 414 => 'Request-URI Too Long',
55
+ 413 => 'Payload Too Large', # RFC7231 (renamed, see below)
56
+ 414 => 'URI Too Long', # RFC7231 (renamed, see below)
40
57
  415 => 'Unsupported Media Type',
41
- 416 => 'Requested Range Not Satisfiable',
58
+ 416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
42
59
  417 => 'Expectation Failed',
43
60
  418 => 'I\'m A Teapot', #RFC2324
44
61
  421 => 'Too Many Connections From This IP',
@@ -61,11 +78,28 @@ module RestClient
61
78
  505 => 'HTTP Version Not Supported',
62
79
  506 => 'Variant Also Negotiates',
63
80
  507 => 'Insufficient Storage', #WebDAV
81
+ 508 => 'Loop Detected', # RFC5842
64
82
  509 => 'Bandwidth Limit Exceeded', #Apache
65
83
  510 => 'Not Extended',
66
84
  511 => 'Network Authentication Required', # RFC6585
67
85
  }
68
86
 
87
+ STATUSES_COMPATIBILITY = {
88
+ # The RFCs all specify "Not Found", but "Resource Not Found" was used in
89
+ # earlier RestClient releases.
90
+ 404 => ['ResourceNotFound'],
91
+
92
+ # HTTP 413 was renamed to "Payload Too Large" in RFC7231.
93
+ 413 => ['RequestEntityTooLarge'],
94
+
95
+ # HTTP 414 was renamed to "URI Too Long" in RFC7231.
96
+ 414 => ['RequestURITooLong'],
97
+
98
+ # HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233.
99
+ 416 => ['RequestedRangeNotSatisfiable'],
100
+ }
101
+
102
+
69
103
  # This is the base RestClient exception class. Rescue it if you want to
70
104
  # catch any exception that your request might raise
71
105
  # You can get the status code by e.http_code, or see anything about the
@@ -148,8 +182,13 @@ module RestClient
148
182
  Exceptions::EXCEPTIONS_MAP[code] = klass_constant
149
183
  end
150
184
 
151
- # Backwards compatibility. "Not Found" is the actual text in the RFCs.
152
- ResourceNotFound = NotFound
185
+ # Create HTTP status exception classes used for backwards compatibility
186
+ STATUSES_COMPATIBILITY.each_pair do |code, compat_list|
187
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
188
+ compat_list.each do |old_name|
189
+ const_set(old_name, klass)
190
+ end
191
+ end
153
192
 
154
193
  module Exceptions
155
194
  # We have to split the Exceptions module like we do here because the
@@ -197,7 +236,7 @@ module RestClient
197
236
  end
198
237
 
199
238
  class SSLCertificateNotVerified < Exception
200
- def initialize(message)
239
+ def initialize(message = 'SSL certificate not verified')
201
240
  super nil, nil
202
241
  self.message = message
203
242
  end
@@ -0,0 +1,72 @@
1
+ module RestClient
2
+
3
+ # The ParamsArray class is used to represent an ordered list of [key, value]
4
+ # pairs. Use this when you need to include a key multiple times or want
5
+ # explicit control over parameter ordering.
6
+ #
7
+ # Most of the request payload & parameter functions normally accept a Hash of
8
+ # keys => values, which does not allow for duplicated keys.
9
+ #
10
+ # @see RestClient::Utils.encode_query_string
11
+ # @see RestClient::Utils.flatten_params
12
+ #
13
+ class ParamsArray
14
+ include Enumerable
15
+
16
+ # @param array [Array<Array>] An array of parameter key,value pairs. These
17
+ # pairs may be 2 element arrays [key, value] or single element hashes
18
+ # {key => value}. They may also be single element arrays to represent a
19
+ # key with no value.
20
+ #
21
+ # @example
22
+ # >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
23
+ # This will be encoded as "foo=123&foo=456&bar=789"
24
+ #
25
+ # @example
26
+ # >> ParamsArray.new({foo: 123, bar: 456})
27
+ # This is valid, but there's no reason not to just use the Hash directly
28
+ # instead of a ParamsArray.
29
+ #
30
+ #
31
+ def initialize(array)
32
+ @array = process_input(array)
33
+ end
34
+
35
+ def each(*args, &blk)
36
+ @array.each(*args, &blk)
37
+ end
38
+
39
+ def empty?
40
+ @array.empty?
41
+ end
42
+
43
+ private
44
+
45
+ def process_input(array)
46
+ array.map {|v| process_pair(v) }
47
+ end
48
+
49
+ # A pair may be:
50
+ # - A single element hash, e.g. {foo: 'bar'}
51
+ # - A two element array, e.g. ['foo', 'bar']
52
+ # - A one element array, e.g. ['foo']
53
+ #
54
+ def process_pair(pair)
55
+ case pair
56
+ when Hash
57
+ if pair.length != 1
58
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
59
+ end
60
+ pair.to_a.fetch(0)
61
+ when Array
62
+ if pair.length > 2
63
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
64
+ end
65
+ [pair.fetch(0), pair[1]]
66
+ else
67
+ # recurse, converting any non-array to an array
68
+ process_pair(pair.to_a)
69
+ end
70
+ end
71
+ end
72
+ end