[DRE-commits] [ruby-rest-client] 01/12: New upstream version 2.0.2

Lucas Nussbaum lucas at moszumanska.debian.org
Sat Jul 8 08:38:20 UTC 2017


This is an automated email from the git hooks/post-receive script.

lucas pushed a commit to branch master
in repository ruby-rest-client.

commit 97574ac53ec6e155c23ceb82f48f2d396b818e38
Author: Lucas Nussbaum <lucas at debian.org>
Date:   Sat Jul 1 20:35:47 2017 +0200

    New upstream version 2.0.2
---
 .gitignore                           |    1 +
 .rspec                               |    3 +-
 .rubocop-disables.yml                |  384 ++++++++++++
 .rubocop.yml                         |    3 +
 .travis.yml                          |   46 +-
 AUTHORS                              |   19 +-
 README.md                            |  784 +++++++++++++++++++++++
 README.rdoc                          |  324 ----------
 Rakefile                             |   18 +-
 bin/restclient                       |    8 +-
 history.md                           |  128 ++++
 lib/restclient.rb                    |   23 +-
 lib/restclient/abstract_response.rb  |  201 ++++--
 lib/restclient/exceptions.rb         |  145 +++--
 lib/restclient/params_array.rb       |   72 +++
 lib/restclient/payload.rb            |  113 ++--
 lib/restclient/platform.rb           |   19 +
 lib/restclient/raw_response.rb       |    7 +-
 lib/restclient/request.rb            |  742 ++++++++++++++--------
 lib/restclient/resource.rb           |    6 +-
 lib/restclient/response.rb           |   71 ++-
 lib/restclient/utils.rb              |  235 +++++++
 lib/restclient/version.rb            |    3 +-
 metadata.yml                         |  253 --------
 rest-client.gemspec                  |   19 +-
 spec/helpers.rb                      |   22 +
 spec/integration/_lib.rb             |    1 +
 spec/integration/httpbin_spec.rb     |   87 +++
 spec/integration/integration_spec.rb |  108 +++-
 spec/integration/request_spec.rb     |   27 +-
 spec/spec_helper.rb                  |   29 +-
 spec/unit/_lib.rb                    |    1 +
 spec/unit/abstract_response_spec.rb  |  127 ++--
 spec/unit/exceptions_spec.rb         |   69 +-
 spec/unit/params_array_spec.rb       |   36 ++
 spec/unit/payload_spec.rb            |  126 ++--
 spec/unit/raw_response_spec.rb       |    8 +-
 spec/unit/request2_spec.rb           |   42 +-
 spec/unit/request_spec.rb            | 1139 ++++++++++++++++++++++------------
 spec/unit/resource_spec.rb           |   58 +-
 spec/unit/response_spec.rb           |  180 ++++--
 spec/unit/restclient_spec.rb         |   30 +-
 spec/unit/utils_spec.rb              |  147 +++++
 spec/unit/windows/root_certs_spec.rb |    6 +-
 44 files changed, 4175 insertions(+), 1695 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1a68400..0ce7bb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
 /doc
 /pkg
 /rdoc
+/.yardoc
diff --git a/.rspec b/.rspec
index 37bd1a9..5f16476 100644
--- a/.rspec
+++ b/.rspec
@@ -1 +1,2 @@
---colour --format progress --order random
+--color
+--format progress
diff --git a/.rubocop-disables.yml b/.rubocop-disables.yml
new file mode 100644
index 0000000..ef15b6b
--- /dev/null
+++ b/.rubocop-disables.yml
@@ -0,0 +1,384 @@
+# This configuration was generated by `rubocop --auto-gen-config`
+# on 2014-07-08 08:57:44 +0000 using RuboCop version 0.24.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# TODO
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/StringConversionInInterpolation:
+  Enabled: false
+
+# Tests only
+# Offense count: 16
+# Cop supports --auto-correct.
+Lint/UnusedBlockArgument:
+  Enabled: false
+
+Lint/Eval:
+  Exclude:
+  - rest-client.windows.gemspec
+
+Lint/HandleExceptions:
+  Exclude:
+  - lib/restclient/utils.rb
+
+Lint/UselessAccessModifier:
+  Exclude:
+  - lib/restclient/windows/root_certs.rb
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/Alias:
+  Enabled: false
+
+# TODO
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/AndOr:
+  Enabled: false
+
+# TODO
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/BlockDelimiters:
+  Enabled: false
+
+# Offense count: 48
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BracesAroundHashParameters:
+  Enabled: false
+
+# Offense count: 1
+Style/ClassAndModuleCamelCase:
+  Enabled: false
+
+# Offense count: 2
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/ClassAndModuleChildren:
+  Enabled: false
+
+# TODO?
+# Offense count: 14
+Metrics/AbcSize:
+  Max: 75
+
+# TODO?
+Metrics/MethodLength:
+  Max: 66
+
+# TODO?
+# Offense count: 4
+Metrics/PerceivedComplexity:
+  Max: 24
+
+# Offense count: 1
+# Configuration parameters: CountComments.
+Metrics/ClassLength:
+  Max: 411
+
+# TODO
+# Offense count: 5
+Style/ClassVars:
+  Enabled: false
+
+# TODO
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredMethods.
+Style/CollectionMethods:
+  Enabled: false
+
+# TODO
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/ColonMethodCall:
+  Enabled: false
+
+Style/ConditionalAssignment:
+  EnforcedStyle: assign_inside_condition
+
+# Offense count: 2
+Style/ConstantName:
+  Enabled: false
+
+# TODO: eh?
+# Offense count: 4
+Metrics/CyclomaticComplexity:
+  Max: 22
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/DeprecatedHashMethods:
+  Enabled: false
+
+# TODO: docs
+# Offense count: 17
+Style/Documentation:
+  Enabled: false
+
+# Offense count: 9
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/DotPosition:
+  Enabled: false
+
+# Offense count: 1
+Style/DoubleNegation:
+  Enabled: false
+
+# TODO
+# Offense count: 2
+Style/EachWithObject:
+  Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/EmptyLines:
+  Enabled: false
+
+# Offense count: 11
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundClassBody:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/EmptyLinesAroundMethodBody:
+  Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundModuleBody:
+  Enabled: false
+
+# Offense count: 31
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Encoding:
+  Enabled: false
+
+# TODO: exclude
+# Offense count: 1
+# Configuration parameters: Exclude.
+Style/FileName:
+  Enabled: false
+
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/FormatString:
+  Enabled: false
+
+# TODO: enable
+# Cop supports --auto-correct.
+# Configuration parameters: SupportedStyles.
+Style/HashSyntax:
+  Enabled: false
+
+# NOTABUG
+# Offense count: 8
+# Configuration parameters: MaxLineLength.
+Style/IfUnlessModifier:
+  Enabled: false
+
+# TODO: configure
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/IndentHash:
+  Enabled: false
+
+# NOTABUG
+# Offense count: 19
+Style/Lambda:
+  Enabled: false
+
+# TODO
+# Offense count: 14
+# Cop supports --auto-correct.
+Style/LeadingCommentSpace:
+  Enabled: false
+
+# TODO
+# Offense count: 218
+# Configuration parameters: AllowURI.
+Metrics/LineLength:
+  Max: 340
+
+# TODO
+# Offense count: 28
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/MethodDefParentheses:
+  Enabled: false
+
+# TODO
+# Offense count: 1
+Style/ModuleFunction:
+  Enabled: false
+
+# Offense count: 4
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Next:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: IncludeSemanticChanges.
+Style/NonNilCheck:
+  Enabled: false
+
+# TODO: exclude
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/Not:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/NumericLiterals:
+  MinDigits: 11
+
+# TODO?
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowSafeAssignment.
+Style/ParenthesesAroundCondition:
+  Enabled: false
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredDelimiters.
+Style/PercentLiteralDelimiters:
+  PreferredDelimiters:
+    '%w': '{}'
+    '%W': '{}'
+    '%Q': '{}'
+  Exclude:
+    - 'bin/restclient'
+
+# Offense count: 3
+# Configuration parameters: NamePrefixBlacklist.
+Style/PredicateName:
+  Enabled: false
+
+# TODO: configure
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/RaiseArgs:
+  Enabled: false
+
+# TODO
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantBegin:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/RedundantSelf:
+  Enabled: false
+
+# Offense count: 1
+Style/RescueModifier:
+  Enabled: false
+  Exclude:
+    - 'bin/restclient'
+
+# TODO: configure
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SignalException:
+  Enabled: false
+
+# TODO
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/SpaceAfterNot:
+  Enabled: false
+
+# Offense count: 19
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceAroundEqualsInParameterDefault:
+  Enabled: false
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Style/SpaceAroundOperators:
+  Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceBeforeBlockBraces:
+  Enabled: false
+
+# Offense count: 37
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+Style/SpaceInsideBlockBraces:
+  Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/SpaceInsideBrackets:
+  Enabled: false
+
+# Offense count: 181
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles.
+Style/SpaceInsideHashLiteralBraces:
+  Enabled: false
+
+# TODO
+# Offense count: 9
+# Cop supports --auto-correct.
+Style/SpaceInsideParens:
+  Enabled: false
+
+# Offense count: 414
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/StringLiterals:
+  Enabled: false
+
+Style/TrailingCommaInLiteral:
+  EnforcedStyleForMultiline: comma
+Style/TrailingCommaInArguments:
+  Enabled: false
+
+# TODO: configure
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, Whitelist.
+Style/TrivialAccessors:
+  Enabled: false
+  Exclude: ['lib/restclient/payload.rb']
+
+# TODO?
+# Offense count: 3
+Style/UnlessElse:
+  Enabled: false
+
+# TODO?
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/UnneededPercentQ:
+  Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/WordArray:
+  MinSize: 4
+
+# TODO?
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BarePercentLiterals:
+  Enabled: false
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..4a11243
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,3 @@
+---
+inherit_from:
+- .rubocop-disables.yml
diff --git a/.travis.yml b/.travis.yml
index ca00399..182000c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,48 @@
+# Available ruby versions: http://rubies.travis-ci.org/
+
 language: ruby
+
+os:
+  - linux
+  - osx
+
 rvm:
-  - "1.9.2"
-  - "1.9.3"
   - "2.0.0"
-  # Forgo 2.1.0 until Travis has a satisfactory fix for
-  # https://github.com/travis-ci/travis-ci/issues/2220
-  - "2.1"  # always the latest 2.1.x
-  - "jruby-19mode"
+  - "2.1" # latest 2.1.x
+  - "2.2.5"
+  - "2.3.3"
+  - "2.4.0"
+  - "ruby-head"
+  - "jruby-9.0.5.0"
+  - "jruby-9.1.5.0"
+  - "jruby-head"
+
+cache: bundler
+
 script:
   bundle exec rake test
+
 branches:
   except:
     - "readme-edits"
+
+before_install:
+  - gem update --system
+  # bundler installation needed for jruby-head
+  # https://github.com/travis-ci/travis-ci/issues/5861
+  - gem install bundler
+
+# Travis OS X support is pretty janky. These are some hacks to include tests
+# only on versions that actually work.
+# (last tested: 2016-11)
+matrix:
+  # exclude: {}
+  # include: {}
+
+  allow_failures:
+    - rvm: 'ruby-head'
+
+  # return results as soon as mandatory versions are done
+  fast_finish: true
+
+sudo: false
diff --git a/AUTHORS b/AUTHORS
index 9fce0a4..163ce9e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -3,6 +3,8 @@ the following kind souls:
 
 Adam Jacob
 Adam Wiggins
+Adrian Rangel
+Alex Tomlins
 Aman Gupta
 Andy Brody
 Blake Mizerany
@@ -17,12 +19,15 @@ Coda Hale
 Crawford
 Cyril Rohr
 Dan Mayer
+Dario Hamidi
+Darren Coxall
 David Backeus
 David Perkowski
 Dmitri Dolguikh
 Dusty Doris
 Dylan Egan
 El Draper
+Evan Broder
 Evan Smith
 François Beausoleil
 Gabriele Cirulli
@@ -40,11 +45,14 @@ Jari Bakken
 Jeff Remer
 Jeffrey Hardy
 Jeremy Kemper
+Joe Rafaniello
 John Barnette
 Jon Rowe
 Jordi Massaguer Pla
+Joshua J. Campoverde
 Juan Alvarez
 Julien Kirch
+Jun Aruga
 Justin Coyne
 Justin Lambert
 Keith Rarick
@@ -56,26 +64,35 @@ Lars Gierth
 Lawrence Leonard Gilbert
 Lee Jarvis
 Lennon Day-Reynolds
+Lin Jen-Shin
 Marc-André Cournoyer
+Marius Butuc
 Matthew Manning
 Michael Klett
+Michael Rykov
+Michael Westbom
 Mike Fletcher
+Nelson Elhage
 Nicholas Wieland
+Nick Hammond
 Nick Plante
 Niko Dittmann
 Oscar Del Ben
 Pablo Astigarraga
 Paul Dlug
 Pedro Belo
+Pedro Chambino
 Philip Corliss
 Pierre-Louis Gottfrois
 Rafael Ssouza
-Rick "technoweenie"
+Rick Olson
 Robert Eanes
 Rodrigo Panachi
+Samuel Cochran
 Syl Turner
 T. Watanabe
 Tekin
 W. Andrew Loe III
 Waynn Lue
+Xavier Shay
 tpresa
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8cebe84
--- /dev/null
+++ b/README.md
@@ -0,0 +1,784 @@
+# REST Client -- simple DSL for accessing HTTP and REST resources
+
+[![Gem Downloads](https://img.shields.io/gem/dt/rest-client.svg)](https://rubygems.org/gems/rest-client)
+[![Build Status](https://travis-ci.org/rest-client/rest-client.svg?branch=master)](https://travis-ci.org/rest-client/rest-client)
+[![Code Climate](https://codeclimate.com/github/rest-client/rest-client.svg)](https://codeclimate.com/github/rest-client/rest-client)
+[![Inline docs](http://inch-ci.org/github/rest-client/rest-client.svg?branch=master)](http://www.rubydoc.info/github/rest-client/rest-client/master)
+
+A simple HTTP and REST client for Ruby, inspired by the Sinatra's microframework style
+of specifying actions: get, put, post, delete.
+
+* Main page: https://github.com/rest-client/rest-client
+* Mailing list: https://groups.io/g/rest-client
+
+### New mailing list
+
+We have a new email list for announcements, hosted by Groups.io.
+
+* Subscribe on the web: https://groups.io/g/rest-client
+
+* Subscribe by sending an email: mailto:rest-client+subscribe at groups.io
+
+* Open discussion subgroup: https://groups.io/g/rest-client+discuss
+
+The old Librelist mailing list is *defunct*, as Librelist appears to be broken
+and not accepting new mail. The old archives are still up, but have been
+imported into the new list archives as well.
+http://librelist.com/browser/rest.client
+
+## Requirements
+
+MRI Ruby 2.0 and newer are supported. Alternative interpreters compatible with
+2.0+ should work as well.
+
+Earlier Ruby versions such as 1.8.7, 1.9.2, and 1.9.3 are no longer supported. These
+versions no longer have any official support, and do not receive security
+updates.
+
+The rest-client gem depends on these other gems for usage at runtime:
+
+* [mime-types](http://rubygems.org/gems/mime-types)
+* [netrc](http://rubygems.org/gems/netrc)
+* [http-cookie](https://rubygems.org/gems/http-cookie)
+
+There are also several development dependencies. It's recommended to use
+[bundler](http://bundler.io/) to manage these dependencies for hacking on
+rest-client.
+
+### Upgrading to rest-client 2.0 from 1.x
+
+Users are encouraged to upgrade to rest-client 2.0, which cleans up a number of
+API warts and wrinkles, making rest-client generally more useful. Usage is
+largely compatible, so many applications will be able to upgrade with no
+changes.
+
+Overview of significant changes:
+
+* requires Ruby >= 2.0
+* `RestClient::Response` objects are a subclass of `String` rather than a
+  Frankenstein monster. And `#body` or `#to_s` return a true `String` object.
+* cleanup of exception classes, including new `RestClient::Exceptions::Timeout`
+* improvements to handling of redirects: responses and history are properly
+  exposed
+* major changes to cookie support: cookie jars are used for browser-like
+  behavior throughout
+* encoding: Content-Type charset response headers are used to automatically set
+  the encoding of the response string
+* HTTP params: handling of GET/POST params is more consistent and sophisticated
+  for deeply nested hash objects, and `ParamsArray` can be used to pass ordered
+  params
+* improved proxy support with per-request proxy configuration, plus the ability
+  to disable proxies set by environment variables
+* default request headers: rest-client sets `Accept: */*` and
+  `User-Agent: rest-client/...`
+
+See [history.md](./history.md) for a more complete description of changes.
+
+## Usage: Raw URL
+
+Basic usage:
+
+```ruby
+require 'rest-client'
+
+RestClient.get(url, headers={})
+
+RestClient.post(url, payload, headers={})
+```
+
+In the high level helpers, only POST, PATCH, and PUT take a payload argument.
+To pass a payload with other HTTP verbs or to pass more advanced options, use
+`RestClient::Request.execute` instead.
+
+More detailed examples:
+
+```ruby
+require 'rest-client'
+
+RestClient.get 'http://example.com/resource'
+
+RestClient.get 'http://example.com/resource', {params: {id: 50, 'foo' => 'bar'}}
+
+RestClient.get 'https://user:password@example.com/private/resource', {accept: :json}
+
+RestClient.post 'http://example.com/resource', {param1: 'one', nested: {param2: 'two'}}
+
+RestClient.post "http://example.com/resource", {'x' => 1}.to_json, {content_type: :json, accept: :json}
+
+RestClient.delete 'http://example.com/resource'
+
+>> response = RestClient.get 'http://example.com/resource'
+=> <RestClient::Response 200 "<!doctype h...">
+>> response.code
+=> 200
+>> response.cookies
+=> {"Foo"=>"BAR", "QUUX"=>"QUUUUX"}
+>> response.headers
+=> {:content_type=>"text/html; charset=utf-8", :cache_control=>"private" ... }
+>> response.body
+=> "<!doctype html>\n<html>\n<head>\n    <title>Example Domain</title>\n\n ..."
+
+RestClient.post( url,
+  {
+    :transfer => {
+      :path => '/foo/bar',
+      :owner => 'that_guy',
+      :group => 'those_guys'
+    },
+     :upload => {
+      :file => File.new(path, 'rb')
+    }
+  })
+```
+## Passing advanced options
+
+The top level helper methods like RestClient.get accept a headers hash as
+their last argument and don't allow passing more complex options. But these
+helpers are just thin wrappers around `RestClient::Request.execute`.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+                            timeout: 10)
+
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+                            ssl_ca_file: 'myca.pem',
+                            ssl_ciphers: 'AESGCM:!aNULL')
+```
+You can also use this to pass a payload for HTTP verbs like DELETE, where the
+`RestClient.delete` helper doesn't accept a payload.
+
+```ruby
+RestClient::Request.execute(method: :delete, url: 'http://example.com/resource',
+                            payload: 'foo', headers: {myheader: 'bar'})
+```
+
+Due to unfortunate choices in the original API, the params used to populate the
+query string are actually taken out of the headers hash. So if you want to pass
+both the params hash and more complex options, use the special key
+`:params` in the headers hash. This design may change in a future major
+release.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+                            timeout: 10, headers: {params: {foo: 'bar'}})
+
+➔ GET http://example.com/resource?foo=bar
+```
+
+## Multipart
+
+Yeah, that's right!  This does multipart sends for you!
+
+```ruby
+RestClient.post '/data', :myfile => File.new("/path/to/image.jpg", 'rb')
+```
+
+This does two things for you:
+
+- Auto-detects that you have a File value sends it as multipart
+- Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
+
+If you are sending params that do not contain a File object but the payload needs to be multipart then:
+
+```ruby
+RestClient.post '/data', {:foo => 'bar', :multipart => true}
+```
+
+## Usage: ActiveResource-Style
+
+```ruby
+resource = RestClient::Resource.new 'http://example.com/resource'
+resource.get
+
+private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
+private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
+```
+
+See RestClient::Resource module docs for details.
+
+## Usage: Resource Nesting
+
+```ruby
+site = RestClient::Resource.new('http://example.com')
+site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
+```
+See `RestClient::Resource` docs for details.
+
+## Exceptions (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
+
+- for result codes between `200` and `207`, a `RestClient::Response` will be returned
+- for result codes `301`, `302` or `307`, the redirection will be followed if the request is a `GET` or a `HEAD`
+- for result code `303`, the redirection will be followed and the request transformed into a `GET`
+- for other cases, a `RestClient::ExceptionWithResponse` holding the Response will be raised; a specific exception class will be thrown for known error codes
+- call `.response` on the exception to get the server's response
+
+```ruby
+>> RestClient.get 'http://example.com/nonexistent'
+Exception: RestClient::NotFound: 404 Not Found
+
+>> begin
+     RestClient.get 'http://example.com/nonexistent'
+   rescue RestClient::ExceptionWithResponse => e
+     e.response
+   end
+=> <RestClient::Response 404 "<!doctype h...">
+```
+
+### Other exceptions
+
+While most exceptions have been collected under `RestClient::RequestFailed` aka
+`RestClient::ExceptionWithResponse`, there are a few quirky exceptions that
+have been kept for backwards compatibility.
+
+RestClient will propagate up exceptions like socket errors without modification:
+
+```ruby
+>> RestClient.get 'http://localhost:12345'
+Exception: Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 12345
+```
+
+RestClient handles a few specific error cases separately in order to give
+better error messages. These will hopefully be cleaned up in a future major
+release.
+
+`RestClient::ServerBrokeConnection` is translated from `EOFError` to give a
+better error message.
+
+`RestClient::SSLCertificateNotVerified` is raised when HTTPS validation fails.
+Other `OpenSSL::SSL::SSLError` errors are raised as is.
+
+### Redirection
+
+By default, rest-client will follow HTTP 30x redirection requests.
+
+__New in 2.0:__ `RestClient::Response` exposes a `#history` method that returns
+a list of each response received in a redirection chain.
+
+```ruby
+>> r = RestClient.get('http://httpbin.org/redirect/2')
+=> <RestClient::Response 200 "{\n  \"args\":...">
+
+# see each response in the redirect chain
+>> r.history
+=> [<RestClient::Response 302 "<!DOCTYPE H...">, <RestClient::Response 302 "">]
+
+# see each requested URL
+>> r.request.url
+=> "http://httpbin.org/get"
+>> r.history.map {|x| x.request.url}
+=> ["http://httpbin.org/redirect/2", "http://httpbin.org/relative-redirect/1"]
+```
+
+#### Manually following redirection
+
+To disable automatic redirection, set `:max_redirects => 0`.
+
+__New in 2.0:__ Prior versions of rest-client would raise
+`RestClient::MaxRedirectsReached`, with no easy way to access the server's
+response. In 2.0, rest-client raises the normal
+`RestClient::ExceptionWithResponse` as it would with any other non-HTTP-20x
+response.
+
+```ruby
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1')
+=> RestClient::Response 200 "{\n  "args":..."
+
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+RestClient::Found: 302 Found
+```
+
+To manually follow redirection, you can call `Response#follow_redirection`. Or
+you could of course inspect the result and choose custom behavior.
+
+```ruby
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+RestClient::Found: 302 Found
+>> begin
+       RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+   rescue RestClient::ExceptionWithResponse => err
+   end
+>> err
+=> #<RestClient::Found: 302 Found>
+>> err.response
+=> RestClient::Response 302 "<!DOCTYPE H..."
+>> err.response.headers[:location]
+=> "/get"
+>> err.response.follow_redirection
+=> RestClient::Response 200 "{\n  "args":..."
+```
+
+## Result handling
+
+The result of a `RestClient::Request` is a `RestClient::Response` object.
+
+__New in 2.0:__ `RestClient::Response` objects are now a subclass of `String`.
+Previously, they were a real String object with response functionality mixed
+in, which was very confusing to work with.
+
+Response objects have several useful methods. (See the class rdoc for more details.)
+
+- `Response#code`: The HTTP response code
+- `Response#body`: The response body as a string. (AKA .to_s)
+- `Response#headers`: A hash of HTTP response headers
+- `Response#raw_headers`: A hash of HTTP response headers as unprocessed arrays
+- `Response#cookies`: A hash of HTTP cookies set by the server
+- `Response#cookie_jar`: <em>New in 1.8</em> An HTTP::CookieJar of cookies
+- `Response#request`: The RestClient::Request object used to make the request
+- `Response#history`: <em>New in 2.0</em> If redirection was followed, a list of prior Response objects
+
+```ruby
+RestClient.get('http://example.com')
+➔ <RestClient::Response 200 "<!doctype h...">
+
+begin
+ RestClient.get('http://example.com/notfound')
+rescue RestClient::ExceptionWithResponse => err
+  err.response
+end
+➔ <RestClient::Response 404 "<!doctype h...">
+```
+
+### Response callbacks, error handling
+
+A block can be passed to the RestClient method. This block will then be called with the Response.
+Response.return! can be called to invoke the default response's behavior.
+
+```ruby
+# Don't raise exceptions but return the response
+>> RestClient.get('http://example.com/nonexistent') {|response, request, result| response }
+=> <RestClient::Response 404 "<!doctype h...">
+```
+
+```ruby
+# Manage a specific error code
+RestClient.get('http://example.com/resource') { |response, request, result, &block|
+  case response.code
+  when 200
+    p "It worked !"
+    response
+  when 423
+    raise SomeCustomExceptionIfYouWant
+  else
+    response.return!(request, result, &block)
+  end
+}
+```
+
+But note that it may be more straightforward to use exceptions to handle
+different HTTP error response cases:
+
+```ruby
+begin
+  resp = RestClient.get('http://example.com/resource')
+rescue RestClient::Unauthorized, RestClient::Forbidden => err
+  puts 'Access denied'
+  return err.response
+rescue RestClient::ImATeapot => err
+  puts 'The server is a teapot! # RFC 2324'
+  return err.response
+else
+  puts 'It worked!'
+  return resp
+end
+```
+
+For GET and HEAD requests, rest-client automatically follows redirection. For
+other HTTP verbs, call `.follow_redirection` on the response object (works both
+in block form and in exception form).
+
+```ruby
+# Follow redirections for all request types and not only for get and head
+# RFC : "If the 301, 302 or 307 status code is received in response to a request other than GET or HEAD,
+#        the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user,
+#        since this might change the conditions under which the request was issued."
+
+# block style
+RestClient.post('http://example.com/redirect', 'body') { |response, request, result|
+  case response.code
+  when 301, 302, 307
+    response.follow_redirection
+  else
+    response.return!
+  end
+}
+
+# exception style by explicit classes
+begin
+  RestClient.post('http://example.com/redirect', 'body')
+rescue RestClient::MovedPermanently,
+       RestClient::Found,
+       RestClient::TemporaryRedirect => err
+  err.response.follow_redirection
+end
+
+# exception style by response code
+begin
+  RestClient.post('http://example.com/redirect', 'body')
+rescue RestClient::ExceptionWithResponse => err
+  case err.http_code
+  when 301, 302, 307
+    err.response.follow_redirection
+  else
+    raise
+  end
+end
+```
+
+## Non-normalized URIs
+
+If you need to normalize URIs, e.g. to work with International Resource Identifiers (IRIs),
+use the addressable gem (http://addressable.rubyforge.org/api/) in your code:
+
+```ruby
+  require 'addressable/uri'
+  RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str)
+```
+
+## Lower-level access
+
+For cases not covered by the general API, you can use the `RestClient::Request` class, which provides a lower-level API.
+
+You can:
+
+- specify ssl parameters
+- override cookies
+- manually handle the response (e.g. to operate on it as a stream rather than reading it all into memory)
+
+See `RestClient::Request`'s documentation for more information.
+
+## Shell
+
+The restclient shell command gives an IRB session with RestClient already loaded:
+
+```ruby
+$ restclient
+>> RestClient.get 'http://example.com'
+```
+
+Specify a URL argument for get/post/put/delete on that resource:
+
+```ruby
+$ restclient http://example.com
+>> put '/resource', 'data'
+```
+
+Add a user and password for authenticated resources:
+
+```ruby
+$ restclient https://example.com user pass
+>> delete '/private/resource'
+```
+
+Create ~/.restclient for named sessions:
+
+```ruby
+  sinatra:
+    url: http://localhost:4567
+  rack:
+    url: http://localhost:9292
+  private_site:
+    url: http://example.com
+    username: user
+    password: pass
+```
+
+Then invoke:
+
+```ruby
+$ restclient private_site
+```
+
+Use as a one-off, curl-style:
+
+```ruby
+$ restclient get http://example.com/resource > output_body
+
+$ restclient put http://example.com/resource < input_body
+```
+
+## Logging
+
+To enable logging you can:
+
+- set RestClient.log with a Ruby Logger, or
+- set an environment variable to avoid modifying the code (in this case you can use a file name, "stdout" or "stderr"):
+
+```ruby
+$ RESTCLIENT_LOG=stdout path/to/my/program
+```
+Either produces logs like this:
+
+```ruby
+RestClient.get "http://some/resource"
+# => 200 OK | text/html 250 bytes
+RestClient.put "http://some/resource", "payload"
+# => 401 Unauthorized | application/xml 340 bytes
+```
+
+Note that these logs are valid Ruby, so you can paste them into the `restclient`
+shell or a script to replay your sequence of rest calls.
+
+## Proxy
+
+All calls to RestClient, including Resources, will use the proxy specified by
+`RestClient.proxy`:
+
+```ruby
+RestClient.proxy = "http://proxy.example.com/"
+RestClient.get "http://some/resource"
+# => response from some/resource as proxied through proxy.example.com
+```
+
+Often the proxy URL is set in an environment variable, so you can do this to
+use whatever proxy the system is configured to use:
+
+```ruby
+  RestClient.proxy = ENV['http_proxy']
+```
+
+__New in 2.0:__ Specify a per-request proxy by passing the :proxy option to
+RestClient::Request. This will override any proxies set by environment variable
+or by the global `RestClient.proxy` value.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com',
+                            proxy: 'http://proxy.example.com')
+# => single request proxied through the proxy
+```
+
+This can be used to disable the use of a proxy for a particular request.
+
+```ruby
+RestClient.proxy = "http://proxy.example.com/"
+RestClient::Request.execute(method: :get, url: 'http://example.com', proxy: nil)
+# => single request sent without a proxy
+```
+
+## Query parameters
+
+Rest-client can render a hash as HTTP query parameters for GET/HEAD/DELETE
+requests or as HTTP post data in `x-www-form-urlencoded` format for POST
+requests.
+
+__New in 2.0:__ Even though there is no standard specifying how this should
+work, rest-client follows a similar convention to the one used by Rack / Rails
+servers for handling arrays, nested hashes, and null values.
+
+The implementation in
+[./lib/rest-client/utils.rb](RestClient::Utils.encode_query_string)
+closely follows
+[Rack::Utils.build_nested_query](http://www.rubydoc.info/gems/rack/Rack/Utils#build_nested_query-class_method),
+but treats empty arrays and hashes as `nil`. (Rack drops them entirely, which
+is confusing behavior.)
+
+If you don't like this behavior and want more control, just serialize params
+yourself (e.g. with `URI.encode_www_form`) and add the query string to the URL
+directly for GET parameters or pass the payload as a string for POST requests.
+
+Basic GET params:
+```ruby
+RestClient.get('https://httpbin.org/get', params: {foo: 'bar', baz: 'qux'})
+# GET "https://httpbin.org/get?foo=bar&baz=qux"
+```
+
+Basic `x-www-form-urlencoded` POST params:
+```ruby
+>> r = RestClient.post('https://httpbin.org/post', {foo: 'bar', baz: 'qux'})
+# POST "https://httpbin.org/post", data: "foo=bar&baz=qux"
+=> <RestClient::Response 200 "{\n  \"args\":...">
+>> JSON.parse(r.body)
+=> {"args"=>{},
+    "data"=>"",
+    "files"=>{},
+    "form"=>{"baz"=>"qux", "foo"=>"bar"},
+    "headers"=>
+    {"Accept"=>"*/*",
+        "Accept-Encoding"=>"gzip, deflate",
+        "Content-Length"=>"15",
+        "Content-Type"=>"application/x-www-form-urlencoded",
+        "Host"=>"httpbin.org"},
+    "json"=>nil,
+    "url"=>"https://httpbin.org/post"}
+```
+
+JSON payload: rest-client does not speak JSON natively, so serialize your
+payload to a string before passing it to rest-client.
+```ruby
+>> payload = {'name' => 'newrepo', 'description': 'A new repo'}
+>> RestClient.post('https://api.github.com/user/repos', payload.to_json, content_type: :json)
+=> <RestClient::Response 201 "{\"id\":75149...">
+```
+
+Advanced GET params (arrays):
+```ruby
+>> r = RestClient.get('https://http-params.herokuapp.com/get', params: {foo: [1,2,3]})
+# GET "https://http-params.herokuapp.com/get?foo[]=1&foo[]=2&foo[]=3"
+=> <RestClient::Response 200 "Method: GET...">
+>> puts r.body
+query_string: "foo[]=1&foo[]=2&foo[]=3"
+decoded:      "foo[]=1&foo[]=2&foo[]=3"
+
+GET:
+  {"foo"=>["1", "2", "3"]}
+```
+
+Advanced GET params (nested hashes):
+```ruby
+>> r = RestClient.get('https://http-params.herokuapp.com/get', params: {outer: {foo: 123, bar: 456}})
+# GET "https://http-params.herokuapp.com/get?outer[foo]=123&outer[bar]=456"
+=> <RestClient::Response 200 "Method: GET...">
+>> puts r.body
+...
+query_string: "outer[foo]=123&outer[bar]=456"
+decoded:      "outer[foo]=123&outer[bar]=456"
+
+GET:
+  {"outer"=>{"foo"=>"123", "bar"=>"456"}}
+```
+
+__New in 2.0:__ The new `RestClient::ParamsArray` class allows callers to
+provide ordering even to structured parameters. This is useful for unusual
+cases where the server treats the order of parameters as significant or you
+want to pass a particular key multiple times.
+
+Multiple fields with the same name using ParamsArray:
+```ruby
+>> RestClient.get('https://httpbin.org/get', params:
+                  RestClient::ParamsArray.new([[:foo, 1], [:foo, 2]]))
+# GET "https://httpbin.org/get?foo=1&foo=2"
+```
+
+Nested ParamsArray:
+```ruby
+>> RestClient.get('https://httpbin.org/get', params:
+                  {foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])})
+# GET "https://httpbin.org/get?foo[a]=1&foo[a]=2"
+```
+
+## Headers
+
+Request headers can be set by passing a ruby hash containing keys and values
+representing header names and values:
+
+```ruby
+# GET request with modified headers
+RestClient.get 'http://example.com/resource', {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+
+# POST request with modified headers
+RestClient.post 'http://example.com/resource', {:foo => 'bar', :baz => 'qux'}, {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+
+# DELETE request with modified headers
+RestClient.delete 'http://example.com/resource', {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+```
+
+## Timeouts
+
+By default the timeout for a request is 60 seconds. Timeouts for your request can
+be adjusted by setting the `timeout:` to the number of seconds that you would like
+the request to wait. Setting `timeout:` will override both `read_timeout:` and `open_timeout:`.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+                            timeout: 120)
+```
+
+Additionally, you can set `read_timeout:` and `open_timeout:` separately.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+                            read_timeout: 120, open_timeout: 240)
+```
+
+## Cookies
+
+Request and Response objects know about HTTP cookies, and will automatically
+extract and set headers for them as needed:
+
+```ruby
+response = RestClient.get 'http://example.com/action_which_sets_session_id'
+response.cookies
+# => {"_applicatioN_session_id" => "1234"}
+
+response2 = RestClient.post(
+  'http://localhost:3000/',
+  {:param1 => "foo"},
+  {:cookies => {:session_id => "1234"}}
+)
+# ...response body
+```
+### Full cookie jar support (new in 1.8)
+
+The original cookie implementation was very naive and ignored most of the
+cookie RFC standards.
+__New in 1.8__:  An HTTP::CookieJar of cookies
+
+Response objects now carry a cookie_jar method that exposes an HTTP::CookieJar
+of cookies, which supports full standards compliant behavior.
+
+## SSL/TLS support
+
+Various options are supported for configuring rest-client's TLS settings. By
+default, rest-client will verify certificates using the system's CA store on
+all platforms. (This is intended to be similar to how browsers behave.) You can
+specify an :ssl_ca_file, :ssl_ca_path, or :ssl_cert_store to customize the
+certificate authorities accepted.
+
+### SSL Client Certificates
+
+```ruby
+RestClient::Resource.new(
+  'https://example.com',
+  :ssl_client_cert  =>  OpenSSL::X509::Certificate.new(File.read("cert.pem")),
+  :ssl_client_key   =>  OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
+  :ssl_ca_file      =>  "ca_certificate.pem",
+  :verify_ssl       =>  OpenSSL::SSL::VERIFY_PEER
+).get
+```
+Self-signed certificates can be generated with the openssl command-line tool.
+
+## Hook
+
+RestClient.add_before_execution_proc add a Proc to be called before each execution.
+It's handy if you need direct access to the HTTP request.
+
+Example:
+
+```ruby
+# Add oauth support using the oauth gem
+require 'oauth'
+access_token = ...
+
+RestClient.add_before_execution_proc do |req, params|
+  access_token.sign! req
+end
+
+RestClient.get 'http://example.com'
+```
+
+## More
+
+Need caching, more advanced logging or any ability provided by Rack middleware?
+
+Have a look at rest-client-components: http://github.com/crohr/rest-client-components
+
+## Credits
+| | |
+|-------------------------|---------------------------------------------------------|
+| **REST Client Team**    | Andy Brody                                              |
+| **Creator**             | Adam Wiggins                                            |
+| **Maintainers Emeriti** | Lawrence Leonard Gilbert, Matthew Manning, Julien Kirch |
+| **Major contributions** | Blake Mizerany, Julien Kirch                            |
+
+A great many generous folks have contributed features and patches.
+See AUTHORS for the full list.
+
+## Legal
+
+Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"Master Shake" photo (http://www.flickr.com/photos/solgrundy/924205581/) by
+"SolGrundy"; used under terms of the Creative Commons Attribution-ShareAlike 2.0
+Generic license (http://creativecommons.org/licenses/by-sa/2.0/)
+
+Code for reading Windows root certificate store derived from work by Puppet;
+used under terms of the Apache License, Version 2.0.
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index 6d330fa..0000000
--- a/README.rdoc
+++ /dev/null
@@ -1,324 +0,0 @@
-= REST Client -- simple DSL for accessing HTTP and REST resources
-
-Build status: {<img src="https://travis-ci.org/rest-client/rest-client.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/rest-client/rest-client]
-
-A simple HTTP and REST client for Ruby, inspired by the Sinatra's microframework style
-of specifying actions: get, put, post, delete.
-
-* Main page: https://github.com/rest-client/rest-client
-* Mailing list: rest.client at librelist.com (send a mail to subscribe).
-
-== Requirements
-
-MRI Ruby 1.9.2 and newer are supported. Alternative interpreters compatible with
-1.9.1+ should work as well.
-
-Ruby 1.8.7 is no longer supported.  That's because the Ruby 1.8.7 interpreter
-itself no longer has official support, _not_ _even_ _security_ _patches!_ If you
-have been putting off upgrading your servers, now is the time.
-({More info is on the Ruby developers'
-blog.}[http://www.ruby-lang.org/en/news/2013/06/30/we-retire-1-8-7/])
-
-The rest-client gem depends on these other gems for installation and usage:
-
-* {mime-types}[http://rubygems.org/gems/mime-types]
-* {netrc}[http://rubygems.org/gems/netrc]
-* {rdoc}[http://rubygems.org/gems/rdoc]
-
-If you want to hack on the code, you should also have {the Bundler
-gem}[http://bundler.io/] installed so it can manage all necessary development
-dependencies for you.
-
-== Usage: Raw URL
-
-  require 'rest_client'
-
-  RestClient.get 'http://example.com/resource'
-
-  RestClient.get 'http://example.com/resource', {:params => {:id => 50, 'foo' => 'bar'}}
-
-  RestClient.get 'https://user:password@example.com/private/resource', {:accept => :json}
-
-  RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
-
-  RestClient.post "http://example.com/resource", { 'x' => 1 }.to_json, :content_type => :json, :accept => :json
-
-  RestClient.delete 'http://example.com/resource'
-
-  response = RestClient.get 'http://example.com/resource'
-  response.code
-  ➔ 200
-  response.cookies
-  ➔ {"Foo"=>"BAR", "QUUX"=>"QUUUUX"}
-  response.headers
-  ➔ {:content_type=>"text/html; charset=utf-8", :cache_control=>"private" ...
-  response.to_str
-  ➔ \n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n   \"http://www.w3.org/TR/html4/strict.dtd\">\n\n<html ....
-
-  RestClient.post( url,
-    {
-      :transfer => {
-        :path => '/foo/bar',
-        :owner => 'that_guy',
-        :group => 'those_guys'
-      },
-       :upload => {
-        :file => File.new(path, 'rb')
-      }
-    })
-
-== Multipart
-
-Yeah, that's right!  This does multipart sends for you!
-
-  RestClient.post '/data', :myfile => File.new("/path/to/image.jpg", 'rb')
-
-This does two things for you:
-
-* Auto-detects that you have a File value sends it as multipart
-* Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
-
-If you are sending params that do not contain a File object but the payload needs to be multipart then:
-
-  RestClient.post '/data', {:foo => 'bar', :multipart => true}
-
-== Usage: ActiveResource-Style
-
-  resource = RestClient::Resource.new 'http://example.com/resource'
-  resource.get
-
-  private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
-  private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
-
-See RestClient::Resource module docs for details.
-
-== Usage: Resource Nesting
-
-  site = RestClient::Resource.new('http://example.com')
-  site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
-
-See RestClient::Resource docs for details.
-
-== Exceptions (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
-
-* for result codes between 200 and 207, a RestClient::Response will be returned
-* for result codes 301, 302 or 307, the redirection will be followed if the request is a GET or a HEAD
-* for result code 303, the redirection will be followed and the request transformed into a GET
-* for other cases, a RestClient::Exception holding the Response will be raised; a specific exception class will be thrown for known error codes
-
-   RestClient.get 'http://example.com/resource'
-   ➔ RestClient::ResourceNotFound: RestClient::ResourceNotFound
-
-   begin
-     RestClient.get 'http://example.com/resource'
-   rescue => e
-     e.response
-   end
-   ➔ 404 Resource Not Found | text/html 282 bytes
-
-== Result handling
-
-A block can be passed to the RestClient method. This block will then be called with the Response.
-Response.return! can be called to invoke the default response's behavior.
-
-  # Don't raise exceptions but return the response
-  RestClient.get('http://example.com/resource'){|response, request, result| response }
-  ➔ 404 Resource Not Found | text/html 282 bytes
-
-  # Manage a specific error code
-  RestClient.get('http://my-rest-service.com/resource'){ |response, request, result, &block|
-    case response.code
-    when 200
-      p "It worked !"
-      response
-    when 423
-      raise SomeCustomExceptionIfYouWant
-    else
-      response.return!(request, result, &block)
-    end
-  }
-
-  # Follow redirections for all request types and not only for get and head
-  # RFC : "If the 301, 302 or 307 status code is received in response to a request other than GET or HEAD,
-  #        the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user,
-  #        since this might change the conditions under which the request was issued."
-  RestClient.get('http://my-rest-service.com/resource'){ |response, request, result, &block|
-    if [301, 302, 307].include? response.code
-      response.follow_redirection(request, result, &block)
-    else
-      response.return!(request, result, &block)
-    end
-  }
-
-== Non-normalized URIs
-
-If you need to normalize URIs, e.g. to work with International Resource Identifiers (IRIs),
-use the addressable gem (http://addressable.rubyforge.org/api/) in your code:
-
-  require 'addressable/uri'
-  RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str)
-
-== Lower-level access
-
-For cases not covered by the general API, you can use the RestClient::Request class, which provides a lower-level API.
-
-You can:
-
-* specify ssl parameters
-* override cookies
-* manually handle the response (e.g. to operate on it as a stream rather than reading it all into memory)
-
-See RestClient::Request's documentation for more information.
-
-== Shell
-
-The restclient shell command gives an IRB session with RestClient already loaded:
-
-  $ restclient
-  >> RestClient.get 'http://example.com'
-
-Specify a URL argument for get/post/put/delete on that resource:
-
-  $ restclient http://example.com
-  >> put '/resource', 'data'
-
-Add a user and password for authenticated resources:
-
-  $ restclient https://example.com user pass
-  >> delete '/private/resource'
-
-Create ~/.restclient for named sessions:
-
-  sinatra:
-    url: http://localhost:4567
-  rack:
-    url: http://localhost:9292
-  private_site:
-    url: http://example.com
-    username: user
-    password: pass
-
-Then invoke:
-
-  $ restclient private_site
-
-Use as a one-off, curl-style:
-
-  $ restclient get http://example.com/resource > output_body
-
-  $ restclient put http://example.com/resource < input_body
-
-== Logging
-
-To enable logging you can:
-
-* set RestClient.log with a Ruby Logger, or
-* set an environment variable to avoid modifying the code (in this case you can use a file name, "stdout" or "stderr"):
-
-   $ RESTCLIENT_LOG=stdout path/to/my/program
-
-Either produces logs like this:
-
-  RestClient.get "http://some/resource"
-  # => 200 OK | text/html 250 bytes
-  RestClient.put "http://some/resource", "payload"
-  # => 401 Unauthorized | application/xml 340 bytes
-
-Note that these logs are valid Ruby, so you can paste them into the restclient
-shell or a script to replay your sequence of rest calls.
-
-== Proxy
-
-All calls to RestClient, including Resources, will use the proxy specified by
-RestClient.proxy:
-
-  RestClient.proxy = "http://proxy.example.com/"
-  RestClient.get "http://some/resource"
-  # => response from some/resource as proxied through proxy.example.com
-
-Often the proxy URL is set in an environment variable, so you can do this to
-use whatever proxy the system is configured to use:
-
-  RestClient.proxy = ENV['http_proxy']
-
-== Query parameters
-
-Request objects know about query parameters and will automatically add them to
-the URL for GET, HEAD and DELETE requests, escaping the keys and values as needed:
-
-  RestClient.get 'http://example.com/resource', :params => {:foo => 'bar', :baz => 'qux'}
-  # will GET http://example.com/resource?foo=bar&baz=qux
-
-== Cookies
-
-Request and Response objects know about HTTP cookies, and will automatically
-extract and set headers for them as needed:
-
-  response = RestClient.get 'http://example.com/action_which_sets_session_id'
-  response.cookies
-  # => {"_applicatioN_session_id" => "1234"}
-
-  response2 = RestClient.post(
-    'http://localhost:3000/',
-    {:param1 => "foo"},
-    {:cookies => {:session_id => "1234"}}
-  )
-  # ...response body
-
-== SSL Client Certificates
-
-  RestClient::Resource.new(
-    'https://example.com',
-    :ssl_client_cert  =>  OpenSSL::X509::Certificate.new(File.read("cert.pem")),
-    :ssl_client_key   =>  OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
-    :ssl_ca_file      =>  "ca_certificate.pem",
-    :verify_ssl       =>  OpenSSL::SSL::VERIFY_PEER
-  ).get
-
-Self-signed certificates can be generated with the openssl command-line tool.
-
-== Hook
-
-RestClient.add_before_execution_proc add a Proc to be called before each execution.
-It's handy if you need direct access to the HTTP request.
-
-Example:
-
-  # Add oauth support using the oauth gem
-  require 'oauth'
-  access_token = ...
-
-  RestClient.add_before_execution_proc do |req, params|
-    access_token.sign! req
-  end
-
-  RestClient.get 'http://example.com'
-
-== More
-
-Need caching, more advanced logging or any ability provided by Rack middleware?
-
-Have a look at rest-client-components: http://github.com/crohr/rest-client-components
-
-== Credits
-
-REST Client Team:: Matthew Manning, Lawrence Leonard Gilbert, Andy Brody
-
-Creator:: Adam Wiggins
-
-Maintainer Emeritus:: Julien Kirch
-
-Major contributions:: Blake Mizerany, Julien Kirch
-
-Patches contributed by many, including Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, François Beausoleil and Nick Plante.
-
-== Legal
-
-Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"Master Shake" photo (http://www.flickr.com/photos/solgrundy/924205581/) by
-"SolGrundy"; used under terms of the Creative Commons Attribution-ShareAlike 2.0
-Generic license (http://creativecommons.org/licenses/by-sa/2.0/)
-
-Code for reading Windows root certificate store derived from work by Puppet;
-used under terms of the Apache License, Version 2.0.
diff --git a/Rakefile b/Rakefile
index c83b1c7..f804fdb 100644
--- a/Rakefile
+++ b/Rakefile
@@ -34,6 +34,22 @@ RSpec::Core::RakeTask.new('rcov') do |t|
   t.rcov_opts = ['--exclude', 'examples']
 end
 
+desc 'Regenerate authors file'
+task :authors do
+  Dir.chdir(File.dirname(__FILE__)) do
+    File.open('AUTHORS', 'w') do |f|
+      f.write( <<-EOM
+The Ruby REST Client would not be what it is today without the help of
+the following kind souls:
+
+      EOM
+      )
+    end
+
+    sh 'git shortlog -s | cut -f 2 >> AUTHORS'
+  end
+end
+
 task :default do
   sh 'rake -T'
 end
@@ -111,6 +127,6 @@ Rake::RDocTask.new do |t|
   t.title    = "rest-client, fetch RESTful resources effortlessly"
   t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
   t.options << '--charset' << 'utf-8'
-  t.rdoc_files.include('README.rdoc')
+  t.rdoc_files.include('README.md')
   t.rdoc_files.include('lib/*.rb')
 end
diff --git a/bin/restclient b/bin/restclient
index 9a585da..7876503 100755
--- a/bin/restclient
+++ b/bin/restclient
@@ -56,11 +56,9 @@ if @verb
 end
 
 POSSIBLE_VERBS.each do |m|
-  eval <<-end_eval
-def  #{m}(path, *args, &b)
-  r[path].#{m}(*args, &b)
-end
-  end_eval
+  define_method(m.to_sym) do |path, *args, &b|
+    r[path].public_send(m.to_sym, *args, &b)
+  end
 end
 
 def method_missing(s, * args, & b)
diff --git a/history.md b/history.md
index 56d9e87..6c00abd 100644
--- a/history.md
+++ b/history.md
@@ -1,3 +1,131 @@
+# 2.0.2
+
+- Suppress the header override warning introduced in 2.0.1 if the value is the
+  same. There's no conflict if the value is unchanged. (#578)
+
+# 2.0.1
+
+- Warn if auto-generated headers from the payload, such as Content-Type,
+  override headers set by the user. This is usually not what the user wants to
+  happen, and can be surprising. (#554)
+- Drop the old check for weak default TLS ciphers, and use the built-in Ruby
+  defaults. Ruby versions from Oct. 2014 onward use sane defaults, so this is
+  no longer needed. (#573)
+
+# 2.0.0
+
+This release is largely API compatible, but makes several breaking changes.
+
+- Drop support for Ruby 1.9
+- Allow mime-types as new as 3.x (requires ruby 2.0)
+- Respect Content-Type charset header provided by server. Previously,
+  rest-client would not override the string encoding chosen by Net::HTTP. Now
+  responses that specify a charset will yield a body string in that encoding.
+  For example, `Content-Type: text/plain; charset=EUC-JP` will return a String
+  encoded with `Encoding::EUC_JP`. (#361)
+- Change exceptions raised on request timeout. Instead of
+  `RestClient::RequestTimeout` (which is still used for HTTP 408), network
+  timeouts will now raise either `RestClient::Exceptions::ReadTimeout` or
+  `RestClient::Exceptions::OpenTimeout`, both of which inherit from
+  `RestClient::Exceptions::Timeout`. For backwards compatibility, this still
+  inherits from `RestClient::RequestTimeout` so existing uses will still work.
+  This may change in a future major release. These new timeout classes also
+  make the original wrapped exception available as `#original_exception`.
+- Unify request exceptions under `RestClient::RequestFailed`, which still
+  inherits from `ExceptionWithResponse`. Previously, HTTP 304, 401, and 404
+  inherited directly from `ExceptionWithResponse` rather than from
+  `RequestFailed`. Now _all_ HTTP status code exceptions inherit from both.
+- Rename the `:timeout` request option to `:read_timeout`. When `:timeout` is
+  passed, now set both `:read_timeout` and `:open_timeout`.
+- Change default HTTP Accept header to `*/*`
+- Use a more descriptive User-Agent header by default
+- Drop RC4-MD5 from default cipher list
+- Only prepend http:// to URIs without a scheme
+- Fix some support for using IPv6 addresses in URLs (still affected by Ruby
+  2.0+ bug https://bugs.ruby-lang.org/issues/9129, with the fix expected to be
+  backported to 2.0 and 2.1)
+- `Response` objects are now a subclass of `String` rather than a `String` that
+  mixes in the response functionality. Most of the methods remain unchanged,
+  but this makes it much easier to understand what is happening when you look
+  at a RestClient response object. There are a few additional changes:
+  - Response objects now implement `.inspect` to make this distinction clearer.
+  - `Response#to_i` will now behave like `String#to_i` instead of returning the
+    HTTP response code, which was very surprising behavior.
+  - `Response#body` and `#to_s` will now return a true `String` object rather
+    than self. Previously there was no easy way to get the true `String`
+    response instead of the Frankenstein response string object with
+    AbstractResponse mixed in.
+  - Response objects no longer accept an extra request args hash, but instead
+    access request args directly from the request object, which reduces
+    confusion and duplication.
+- Handle multiple HTTP response headers with the same name (except for
+  Set-Cookie, which is special) by joining the values with a comma space,
+  compliant with RFC 7230
+- Rewrite cookie support to be much smarter and to use cookie jars consistently
+  for requests, responses, and redirection in order to resolve long-standing
+  complaints about the previously broken behavior: (#498)
+  - The `:cookies` option may now be a Hash of Strings, an Array of
+    HTTP::Cookie objects, or a full HTTP::CookieJar.
+  - Add `RestClient::Request#cookie_jar` and reimplement `Request#cookies` to
+    be a wrapper around the cookie jar.
+  - Still support passing the `:cookies` option in the headers hash, but now
+    raise ArgumentError if that option is also passed to `Request#initialize`.
+  - Warn if both `:cookies` and a `Cookie` header are supplied.
+  - Use the `Request#cookie_jar` as the basis for `Response#cookie_jar`,
+    creating a copy of the jar and adding any newly received cookies.
+  - When following redirection, also use this same strategy so that cookies
+    from the original request are carried through in a standards-compliant way
+    by the cookie jar.
+- Don't set basic auth header if explicit `Authorization` header is specified
+- Add `:proxy` option to requests, which can be used for thread-safe
+  per-request proxy configuration, overriding `RestClient.proxy`
+- Allow overriding `ENV['http_proxy']` to disable proxies by setting
+  `RestClient.proxy` to a falsey value. Previously there was no way in Ruby 2.x
+  to turn off a proxy specified in the environment without changing `ENV`.
+- Add actual support for streaming request payloads. Previously rest-client
+  would call `.to_s` even on RestClient::Payload::Streamed objects. Instead,
+  treat any object that responds to `.read` as a streaming payload and pass it
+  through to `.body_stream=` on the Net:HTTP object. This massively reduces the
+  memory required for large file uploads.
+- Changes to redirection behavior: (#381, #484)
+  - Remove `RestClient::MaxRedirectsReached` in favor of the normal
+    `ExceptionWithResponse` subclasses. This makes the response accessible on
+    the exception object as `.response`, making it possible for callers to tell
+    what has actually happened when the redirect limit is reached.
+  - When following HTTP redirection, store a list of each previous response on
+    the response object as `.history`. This makes it possible to access the
+    original response headers and body before the redirection was followed.
+  - Follow redirection consistently, regardless of whether the HTTP method was
+    passed as a symbol or string. Under the hood rest-client now normalizes the
+    HTTP request method to a lowercase string.
+- Add `:before_execution_proc` option to `RestClient::Request`. This makes it
+  possible to add procs like `RestClient.add_before_execution_proc` to a single
+  request without global state.
+- Run tests on Travis's beta OS X support.
+- Make `Request#transmit` a private method, along with a few others.
+- Refactor URI parsing to happen earlier, in Request initialization.
+- Improve consistency and functionality of complex URL parameter handling:
+  - When adding URL params, handle URLs that already contain params.
+  - Add new convention for handling URL params containing deeply nested arrays
+    and hashes, unify handling of null/empty values, and use the same code for
+    GET and POST params. (#437)
+  - Add the RestClient::ParamsArray class, a simple array-like container that
+    can be used to pass multiple keys with same name or keys where the ordering
+    is significant.
+- Add a few more exception classes for obscure HTTP status codes.
+- Multipart: use a much more robust multipart boundary with greater entropy.
+- Make `RestClient::Payload::Base#inspect` stop pretending to be a String.
+- Add `Request#redacted_uri` and `Request#redacted_url` to display the URI
+  with any password redacted.
+
+# 2.0.0.rc1
+
+Changes in the release candidate that did not persist through the final 2.0.0
+release:
+- RestClient::Exceptions::Timeout was originally going to be a direct subclass
+  of RestClient::Exception in the release candidate. This exception tree was
+  made a subclass of RestClient::RequestTimeout prior to the final release.
+
 # 1.8.0
 
 - Security: implement standards compliant cookie handling by adding a
diff --git a/lib/restclient.rb b/lib/restclient.rb
index 530fc59..bdb53f0 100644
--- a/lib/restclient.rb
+++ b/lib/restclient.rb
@@ -7,11 +7,13 @@ require 'zlib'
 require File.dirname(__FILE__) + '/restclient/version'
 require File.dirname(__FILE__) + '/restclient/platform'
 require File.dirname(__FILE__) + '/restclient/exceptions'
+require File.dirname(__FILE__) + '/restclient/utils'
 require File.dirname(__FILE__) + '/restclient/request'
 require File.dirname(__FILE__) + '/restclient/abstract_response'
 require File.dirname(__FILE__) + '/restclient/response'
 require File.dirname(__FILE__) + '/restclient/raw_response'
 require File.dirname(__FILE__) + '/restclient/resource'
+require File.dirname(__FILE__) + '/restclient/params_array'
 require File.dirname(__FILE__) + '/restclient/payload'
 require File.dirname(__FILE__) + '/restclient/windows'
 
@@ -89,8 +91,24 @@ module RestClient
     Request.execute(:method => :options, :url => url, :headers => headers, &block)
   end
 
-  class << self
-    attr_accessor :proxy
+  # A global proxy URL to use for all requests. This can be overridden on a
+  # per-request basis by passing `:proxy` to RestClient::Request.
+  def self.proxy
+    @proxy ||= nil
+  end
+
+  def self.proxy=(value)
+    @proxy = value
+    @proxy_set = true
+  end
+
+  # Return whether RestClient.proxy was set explicitly. We use this to
+  # differentiate between no value being set and a value explicitly set to nil.
+  #
+  # @return [Boolean]
+  #
+  def self.proxy_set?
+    @proxy_set ||= false
   end
 
   # Setup the log for RestClient calls.
@@ -150,6 +168,7 @@ module RestClient
   # Add a Proc to be called before each request in executed.
   # The proc parameters will be the http request and the request params.
   def self.add_before_execution_proc &proc
+    raise ArgumentError.new('block is required') unless proc
     @@before_execution_procs << proc
   end
 
diff --git a/lib/restclient/abstract_response.rb b/lib/restclient/abstract_response.rb
index 1cc5657..7cd5b82 100644
--- a/lib/restclient/abstract_response.rb
+++ b/lib/restclient/abstract_response.rb
@@ -5,13 +5,21 @@ module RestClient
 
   module AbstractResponse
 
-    attr_reader :net_http_res, :args, :request
+    attr_reader :net_http_res, :request
+
+    def inspect
+      raise NotImplementedError.new('must override in subclass')
+    end
 
     # HTTP status code
     def code
       @code ||= @net_http_res.code.to_i
     end
 
+    def history
+      @history ||= request.redirection_history || []
+    end
+
     # A hash of the headers, beautified with symbols and underscores.
     # e.g. "Content-type" will become :content_type.
     def headers
@@ -23,17 +31,28 @@ module RestClient
       @raw_headers ||= @net_http_res.to_hash
     end
 
-    def response_set_vars(net_http_res, args, request)
+    def response_set_vars(net_http_res, request)
       @net_http_res = net_http_res
-      @args = args
       @request = request
+
+      # prime redirection history
+      history
     end
 
-    # Hash of cookies extracted from response headers
+    # Hash of cookies extracted from response headers.
+    #
+    # NB: This will return only cookies whose domain matches this request, and
+    # may not even return all of those cookies if there are duplicate names.
+    # Use the full cookie_jar for more nuanced access.
+    #
+    # @see #cookie_jar
+    #
+    # @return [Hash]
+    #
     def cookies
       hash = {}
 
-      cookie_jar.cookies.each do |cookie|
+      cookie_jar.cookies(@request.uri).each do |cookie|
         hash[cookie.name] = cookie.value
       end
 
@@ -45,91 +64,163 @@ module RestClient
     # @return [HTTP::CookieJar]
     #
     def cookie_jar
-      return @cookie_jar if @cookie_jar
+      return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
 
-      jar = HTTP::CookieJar.new
+      jar = @request.cookie_jar.dup
       headers.fetch(:set_cookie, []).each do |cookie|
-        jar.parse(cookie, @request.url)
+        jar.parse(cookie, @request.uri)
       end
 
       @cookie_jar = jar
     end
 
     # Return the default behavior corresponding to the response code:
-    # 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
-    def return! request = nil, result = nil, & block
-      if (200..207).include? code
+    #
+    # For 20x status codes: return the response itself
+    #
+    # For 30x status codes:
+    #   301, 302, 307: redirect GET / HEAD if there is a Location header
+    #   303: redirect, changing method to GET, if there is a Location header
+    #
+    # For all other responses, raise a response exception
+    #
+    def return!(&block)
+      case code
+      when 200..207
         self
-      elsif [301, 302, 307].include? code
-        unless [:get, :head].include? args[:method]
-          raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
+      when 301, 302, 307
+        case request.method
+        when 'get', 'head'
+          check_max_redirects
+          follow_redirection(&block)
         else
-          follow_redirection(request, result, & block)
+          raise exception_with_response
         end
-      elsif code == 303
-        args[:method] = :get
-        args.delete :payload
-        follow_redirection(request, result, & block)
-      elsif Exceptions::EXCEPTIONS_MAP[code]
-        raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
+      when 303
+        check_max_redirects
+        follow_get_redirection(&block)
       else
-        raise RequestFailed.new(self, code)
+        raise exception_with_response
       end
     end
 
     def to_i
-      code
+      warn('warning: calling Response#to_i is not recommended')
+      super
     end
 
     def description
       "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
     end
 
-    # Follow a redirection
-    def follow_redirection request = nil, result = nil, & block
-      new_args = @args.dup
+    # Follow a redirection response by making a new HTTP request to the
+    # redirection target.
+    def follow_redirection(&block)
+      _follow_redirection(request.args.dup, &block)
+    end
 
-      url = headers[:location]
-      if url !~ /^http/
-        url = URI.parse(request.url).merge(url).to_s
-      end
-      new_args[:url] = url
-      if request
-        if request.max_redirects == 0
-          raise MaxRedirectsReached
-        end
-        new_args[:password] = request.password
-        new_args[:user] = request.user
-        new_args[:headers] = request.headers
-        new_args[:max_redirects] = request.max_redirects - 1
-
-        # TODO: figure out what to do with original :cookie, :cookies values
-        new_args[:headers]['Cookie'] = HTTP::Cookie.cookie_value(
-          cookie_jar.cookies(new_args.fetch(:url)))
-      end
+    # Follow a redirection response, but change the HTTP method to GET and drop
+    # the payload from the original request.
+    def follow_get_redirection(&block)
+      new_args = request.args.dup
+      new_args[:method] = :get
+      new_args.delete(:payload)
 
-      Request.execute(new_args, &block)
+      _follow_redirection(new_args, &block)
     end
 
+    # Convert headers hash into canonical form.
+    #
+    # Header names will be converted to lowercase symbols with underscores
+    # instead of hyphens.
+    #
+    # Headers specified multiple times will be joined by comma and space,
+    # except for Set-Cookie, which will always be an array.
+    #
+    # Per RFC 2616, if a server sends multiple headers with the same key, they
+    # MUST be able to be joined into a single header by a comma. However,
+    # Set-Cookie (RFC 6265) cannot because commas are valid within cookie
+    # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
+    # handled as a special case.
+    #
+    # http://tools.ietf.org/html/rfc2616#section-4.2
+    # http://tools.ietf.org/html/rfc7230#section-3.2.2
+    # http://tools.ietf.org/html/rfc6265
+    #
+    # @param headers [Hash]
+    # @return [Hash]
+    #
     def self.beautify_headers(headers)
       headers.inject({}) do |out, (key, value)|
-        out[key.gsub(/-/, '_').downcase.to_sym] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
+        key_sym = key.tr('-', '_').downcase.to_sym
+
+        # Handle Set-Cookie specially since it cannot be joined by comma.
+        if key.downcase == 'set-cookie'
+          out[key_sym] = value
+        else
+          out[key_sym] = value.join(', ')
+        end
+
         out
       end
     end
 
     private
 
-    # Parse a cookie value and return its content in an Hash
-    def parse_cookie cookie_content
-      out = {}
-      CGI::Cookie::parse(cookie_content).each do |key, cookie|
-        unless ['expires', 'path'].include? key
-          out[CGI::escape(key)] = cookie.value[0] ? (CGI::escape(cookie.value[0]) || '') : ''
-        end
+    # Follow a redirection
+    #
+    # @param new_args [Hash] Start with this hash of arguments for the
+    #   redirection request. The hash will be mutated, so be sure to dup any
+    #   existing hash that should not be modified.
+    #
+    def _follow_redirection(new_args, &block)
+
+      # parse location header and merge into existing URL
+      url = headers[:location]
+
+      # cannot follow redirection if there is no location header
+      unless url
+        raise exception_with_response
+      end
+
+      # handle relative redirects
+      unless url.start_with?('http')
+        url = URI.parse(request.url).merge(url).to_s
+      end
+      new_args[:url] = url
+
+      new_args[:password] = request.password
+      new_args[:user] = request.user
+      new_args[:headers] = request.headers
+      new_args[:max_redirects] = request.max_redirects - 1
+
+      # pass through our new cookie jar
+      new_args[:cookies] = cookie_jar
+
+      # prepare new request
+      new_req = Request.new(new_args)
+
+      # append self to redirection history
+      new_req.redirection_history = history + [self]
+
+      # execute redirected request
+      new_req.execute(&block)
+    end
+
+    def check_max_redirects
+      if request.max_redirects <= 0
+        raise exception_with_response
       end
-      out
     end
-  end
 
+    def exception_with_response
+      begin
+        klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
+      rescue KeyError
+        raise RequestFailed.new(self, code)
+      end
+
+      raise klass.new(self, code)
+    end
+  end
 end
diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb
index 4632444..5703bff 100644
--- a/lib/restclient/exceptions.rb
+++ b/lib/restclient/exceptions.rb
@@ -1,5 +1,19 @@
 module RestClient
 
+  # Hash of HTTP status code => message.
+  #
+  # 1xx: Informational - Request received, continuing process
+  # 2xx: Success - The action was successfully received, understood, and
+  #      accepted
+  # 3xx: Redirection - Further action must be taken in order to complete the
+  #      request
+  # 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
+  # 5xx: Server Error - The server failed to fulfill an apparently valid
+  #      request
+  #
+  # @see
+  #   http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+  #
   STATUSES = {100 => 'Continue',
               101 => 'Switching Protocols',
               102 => 'Processing', #WebDAV
@@ -12,6 +26,8 @@ module RestClient
               205 => 'Reset Content',
               206 => 'Partial Content',
               207 => 'Multi-Status', #WebDAV
+              208 => 'Already Reported', # RFC5842
+              226 => 'IM Used', # RFC3229
 
               300 => 'Multiple Choices',
               301 => 'Moved Permanently',
@@ -21,12 +37,13 @@ module RestClient
               305 => 'Use Proxy', # http/1.1
               306 => 'Switch Proxy', # no longer used
               307 => 'Temporary Redirect', # http/1.1
+              308 => 'Permanent Redirect', # RFC7538
 
               400 => 'Bad Request',
               401 => 'Unauthorized',
               402 => 'Payment Required',
               403 => 'Forbidden',
-              404 => 'Resource Not Found',
+              404 => 'Not Found',
               405 => 'Method Not Allowed',
               406 => 'Not Acceptable',
               407 => 'Proxy Authentication Required',
@@ -35,10 +52,10 @@ module RestClient
               410 => 'Gone',
               411 => 'Length Required',
               412 => 'Precondition Failed',
-              413 => 'Request Entity Too Large',
-              414 => 'Request-URI Too Long',
+              413 => 'Payload Too Large', # RFC7231 (renamed, see below)
+              414 => 'URI Too Long', # RFC7231 (renamed, see below)
               415 => 'Unsupported Media Type',
-              416 => 'Requested Range Not Satisfiable',
+              416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
               417 => 'Expectation Failed',
               418 => 'I\'m A Teapot', #RFC2324
               421 => 'Too Many Connections From This IP',
@@ -61,22 +78,27 @@ module RestClient
               505 => 'HTTP Version Not Supported',
               506 => 'Variant Also Negotiates',
               507 => 'Insufficient Storage', #WebDAV
+              508 => 'Loop Detected', # RFC5842
               509 => 'Bandwidth Limit Exceeded', #Apache
               510 => 'Not Extended',
               511 => 'Network Authentication Required', # RFC6585
   }
 
-  # Compatibility : make the Response act like a Net::HTTPResponse when needed
-  module ResponseForException
-    def method_missing symbol, *args
-      if net_http_res.respond_to? symbol
-        warn "[warning] The response contained in an RestClient::Exception is now a RestClient::Response instead of a Net::HTTPResponse, please update your code"
-        net_http_res.send symbol, *args
-      else
-        super
-      end
-    end
-  end
+  STATUSES_COMPATIBILITY = {
+    # The RFCs all specify "Not Found", but "Resource Not Found" was used in
+    # earlier RestClient releases.
+    404 => ['ResourceNotFound'],
+
+    # HTTP 413 was renamed to "Payload Too Large" in RFC7231.
+    413 => ['RequestEntityTooLarge'],
+
+    # HTTP 414 was renamed to "URI Too Long" in RFC7231.
+    414 => ['RequestURITooLong'],
+
+    # HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233.
+    416 => ['RequestedRangeNotSatisfiable'],
+  }
+
 
   # This is the base RestClient exception class. Rescue it if you want to
   # catch any exception that your request might raise
@@ -86,15 +108,13 @@ module RestClient
   # probably an HTML error page) is e.response.
   class Exception < RuntimeError
     attr_accessor :response
+    attr_accessor :original_exception
     attr_writer :message
 
     def initialize response = nil, initial_response_code = nil
       @response = response
       @message = nil
       @initial_response_code = initial_response_code
-
-      # compatibility: this make the exception behave like a Net::HTTPResponse
-      response.extend ResponseForException if response
     end
 
     def http_code
@@ -106,22 +126,25 @@ module RestClient
       end
     end
 
-    def http_body
-      @response.body if @response
+    def http_headers
+      @response.headers if @response
     end
 
-    def inspect
-      "#{message}: #{http_body}"
+    def http_body
+      @response.body if @response
     end
 
     def to_s
-      inspect
+      message
     end
 
     def message
-      @message || self.class.name
+      @message || default_message
     end
 
+    def default_message
+      self.class.name
+    end
   end
 
   # Compatibility
@@ -131,7 +154,7 @@ module RestClient
   # The request failed with an error code not managed by the code
   class RequestFailed < ExceptionWithResponse
 
-    def message
+    def default_message
       "HTTP status code #{http_code}"
     end
 
@@ -140,43 +163,68 @@ module RestClient
     end
   end
 
-  # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+  # RestClient exception classes. TODO: move all exceptions into this module.
+  #
+  # We will a create an exception for each status code, see
+  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+  #
   module Exceptions
     # Map http status codes to the corresponding exception class
     EXCEPTIONS_MAP = {}
   end
 
+  # Create HTTP status exception classes
   STATUSES.each_pair do |code, message|
-
-    # Compatibility
-    superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed
-    klass = Class.new(superclass) do
-      send(:define_method, :message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
+    klass = Class.new(RequestFailed) do
+      send(:define_method, :default_message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
     end
-    klass_constant = const_set message.delete(' \-\''), klass
+    klass_constant = const_set(message.delete(' \-\''), klass)
     Exceptions::EXCEPTIONS_MAP[code] = klass_constant
   end
 
-  # A redirect was encountered; caught by execute to retry with the new url.
-  class Redirect < Exception
-
-    def message
-      'Redirect'
+  # Create HTTP status exception classes used for backwards compatibility
+  STATUSES_COMPATIBILITY.each_pair do |code, compat_list|
+    klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
+    compat_list.each do |old_name|
+      const_set(old_name, klass)
     end
+  end
 
-    attr_accessor :url
+  module Exceptions
+    # We have to split the Exceptions module like we do here because the
+    # EXCEPTIONS_MAP is under Exceptions, but we depend on
+    # RestClient::RequestTimeout below.
+
+    # Base class for request timeouts.
+    #
+    # NB: Previous releases of rest-client would raise RequestTimeout both for
+    # HTTP 408 responses and for actual connection timeouts.
+    class Timeout < RestClient::RequestTimeout
+      def initialize(message=nil, original_exception=nil)
+        super(nil, nil)
+        self.message = message if message
+        self.original_exception = original_exception if original_exception
+      end
+    end
 
-    def initialize(url)
-      @url = url
+    # Timeout when connecting to a server. Typically wraps Net::OpenTimeout (in
+    # ruby 2.0 or greater).
+    class OpenTimeout < Timeout
+      def default_message
+        'Timed out connecting to server'
+      end
     end
-  end
 
-  class MaxRedirectsReached < Exception
-    def message
-      'Maximum number of redirect reached'
+    # Timeout when reading from a server. Typically wraps Net::ReadTimeout (in
+    # ruby 2.0 or greater).
+    class ReadTimeout < Timeout
+      def default_message
+        'Timed out reading data from server'
+      end
     end
   end
 
+
   # The server broke the connection prior to the request completing.  Usually
   # this means it crashed, or sometimes that your network connection was
   # severed before it could complete.
@@ -188,16 +236,9 @@ module RestClient
   end
 
   class SSLCertificateNotVerified < Exception
-    def initialize(message)
+    def initialize(message = 'SSL certificate not verified')
       super nil, nil
       self.message = message
     end
   end
 end
-
-class RestClient::Request
-  # backwards compatibility
-  Redirect = RestClient::Redirect
-  Unauthorized = RestClient::Unauthorized
-  RequestFailed = RestClient::RequestFailed
-end
diff --git a/lib/restclient/params_array.rb b/lib/restclient/params_array.rb
new file mode 100644
index 0000000..359b687
--- /dev/null
+++ b/lib/restclient/params_array.rb
@@ -0,0 +1,72 @@
+module RestClient
+
+  # The ParamsArray class is used to represent an ordered list of [key, value]
+  # pairs. Use this when you need to include a key multiple times or want
+  # explicit control over parameter ordering.
+  #
+  # Most of the request payload & parameter functions normally accept a Hash of
+  # keys => values, which does not allow for duplicated keys.
+  #
+  # @see RestClient::Utils.encode_query_string
+  # @see RestClient::Utils.flatten_params
+  #
+  class ParamsArray
+    include Enumerable
+
+    # @param array [Array<Array>] An array of parameter key,value pairs. These
+    #   pairs may be 2 element arrays [key, value] or single element hashes
+    #   {key => value}. They may also be single element arrays to represent a
+    #   key with no value.
+    #
+    # @example
+    #   >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
+    #   This will be encoded as "foo=123&foo=456&bar=789"
+    #
+    # @example
+    #   >> ParamsArray.new({foo: 123, bar: 456})
+    #   This is valid, but there's no reason not to just use the Hash directly
+    #   instead of a ParamsArray.
+    #
+    #
+    def initialize(array)
+      @array = process_input(array)
+    end
+
+    def each(*args, &blk)
+      @array.each(*args, &blk)
+    end
+
+    def empty?
+      @array.empty?
+    end
+
+    private
+
+    def process_input(array)
+      array.map {|v| process_pair(v) }
+    end
+
+    # A pair may be:
+    # - A single element hash, e.g. {foo: 'bar'}
+    # - A two element array, e.g. ['foo', 'bar']
+    # - A one element array, e.g. ['foo']
+    #
+    def process_pair(pair)
+      case pair
+      when Hash
+        if pair.length != 1
+          raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
+        end
+        pair.to_a.fetch(0)
+      when Array
+        if pair.length > 2
+          raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
+        end
+        [pair.fetch(0), pair[1]]
+      else
+        # recurse, converting any non-array to an array
+        process_pair(pair.to_a)
+      end
+    end
+  end
+end
diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb
index b550598..56563fa 100644
--- a/lib/restclient/payload.rb
+++ b/lib/restclient/payload.rb
@@ -1,5 +1,7 @@
 require 'tempfile'
+require 'securerandom'
 require 'stringio'
+
 require 'mime/types'
 
 module RestClient
@@ -23,28 +25,20 @@ module RestClient
     end
 
     def has_file?(params)
-      params.any? do |_, v|
-        case v
-        when Hash
-          has_file?(v)
-        when Array
-          has_file_array?(v)
-        else
-          v.respond_to?(:path) && v.respond_to?(:read)
-        end
+      unless params.is_a?(Hash)
+        raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
       end
+      _has_file?(params)
     end
 
-    def has_file_array?(params)
-      params.any? do |v|
-        case v
-        when Hash
-          has_file?(v)
-        when Array
-          has_file_array?(v)
-        else
-          v.respond_to?(:path) && v.respond_to?(:read)
-        end
+    def _has_file?(obj)
+      case obj
+      when Hash, ParamsArray
+        obj.any? {|_, v| _has_file?(v) }
+      when Array
+        obj.any? {|v| _has_file?(v) }
+      else
+        obj.respond_to?(:path) && obj.respond_to?(:read)
       end
     end
 
@@ -58,40 +52,13 @@ module RestClient
         @stream.seek(0)
       end
 
-      def read(bytes=nil)
-        @stream.read(bytes)
-      end
-
-      alias :to_s :read
-
-      # Flatten parameters by converting hashes of hashes to flat hashes
-      # {keys1 => {keys2 => value}} will be transformed into [keys1[key2], value]
-      def flatten_params(params, parent_key = nil)
-        result = []
-        params.each do |key, value|
-          calculated_key = parent_key ? "#{parent_key}[#{handle_key(key)}]" : handle_key(key)
-          if value.is_a? Hash
-            result += flatten_params(value, calculated_key)
-          elsif value.is_a? Array
-            result += flatten_params_array(value, calculated_key)
-          else
-            result << [calculated_key, value]
-          end
-        end
-        result
+      def read(*args)
+        @stream.read(*args)
       end
 
-      def flatten_params_array value, calculated_key
-        result = []
-        value.each do |elem|
-          if elem.is_a? Hash
-            result += flatten_params(elem, calculated_key)
-          elsif elem.is_a? Array
-            result += flatten_params_array(elem, calculated_key)
-          else
-            result << ["#{calculated_key}[]", elem]
-          end
-        end
+      def to_s
+        result = read
+        @stream.seek(0)
         result
       end
 
@@ -109,14 +76,12 @@ module RestClient
         @stream.close unless @stream.closed?
       end
 
-      def inspect
-        result = to_s.inspect
-        @stream.seek(0)
-        result
+      def to_s_inspect
+        to_s.inspect
       end
 
       def short_inspect
-        (size > 500 ? "#{size} byte(s) length" : inspect)
+        (size > 500 ? "#{size} byte(s) length" : to_s_inspect)
       end
 
     end
@@ -139,37 +104,28 @@ module RestClient
 
     class UrlEncoded < Base
       def build_stream(params = nil)
-        @stream = StringIO.new(flatten_params(params).collect do |entry|
-          "#{entry[0]}=#{handle_key(entry[1])}"
-        end.join("&"))
+        @stream = StringIO.new(Utils.encode_query_string(params))
         @stream.seek(0)
       end
 
-      # for UrlEncoded escape the keys
-      def handle_key key
-        Parser.escape(key.to_s, Escape)
-      end
-
       def headers
         super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
       end
-
-      Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
-      Escape = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
     end
 
     class Multipart < Base
       EOL = "\r\n"
 
       def build_stream(params)
-        b = "--#{boundary}"
+        b = '--' + boundary
 
         @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
         @stream.binmode
         @stream.write(b + EOL)
 
-        if params.is_a? Hash
-          x = flatten_params(params)
+        case params
+        when Hash, ParamsArray
+          x = Utils.flatten_params(params)
         else
           x = params
         end
@@ -218,10 +174,25 @@ module RestClient
       end
 
       def boundary
-        @boundary ||= rand(1_000_000).to_s
+        return @boundary if defined?(@boundary) && @boundary
+
+        # Use the same algorithm used by WebKit: generate 16 random
+        # alphanumeric characters, replacing `+` `/` with `A` `B` (included in
+        # the list twice) to round out the set of 64.
+        s = SecureRandom.base64(12)
+        s.tr!('+/', 'AB')
+
+        @boundary = '----RubyFormBoundary' + s
       end
 
       # for Multipart do not escape the keys
+      #
+      # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
+      # practice no major browser that I'm aware of uses percent encoding.
+      #
+      # Further discussion of multipart encoding:
+      # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
+      #
       def handle_key key
         key
       end
diff --git a/lib/restclient/platform.rb b/lib/restclient/platform.rb
index c3f321b..87df973 100644
--- a/lib/restclient/platform.rb
+++ b/lib/restclient/platform.rb
@@ -1,3 +1,5 @@
+require 'rbconfig'
+
 module RestClient
   module Platform
     # Return true if we are running on a darwin-based Ruby platform. This will
@@ -26,5 +28,22 @@ module RestClient
       # defined on mri >= 1.9
       RUBY_ENGINE == 'jruby'
     end
+
+    def self.architecture
+      "#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}"
+    end
+
+    def self.ruby_agent_version
+      case RUBY_ENGINE
+      when 'jruby'
+        "jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})"
+      else
+        "#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
+      end
+    end
+
+    def self.default_user_agent
+      "rest-client/#{VERSION} (#{architecture}) #{ruby_agent_version}"
+    end
   end
 end
diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb
index 69fc939..59ecc5c 100644
--- a/lib/restclient/raw_response.rb
+++ b/lib/restclient/raw_response.rb
@@ -15,9 +15,12 @@ module RestClient
 
     attr_reader :file, :request
 
-    def initialize(tempfile, net_http_res, args, request)
+    def inspect
+      "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
+    end
+
+    def initialize(tempfile, net_http_res, request)
       @net_http_res = net_http_res
-      @args = args
       @file = tempfile
       @request = request
     end
diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb
index a9a0e60..a875dc0 100644
--- a/lib/restclient/request.rb
+++ b/lib/restclient/request.rb
@@ -16,106 +16,75 @@ module RestClient
   # * :url
   # Optional parameters (have a look at ssl and/or uri for some explanations):
   # * :headers a hash containing the request headers
-  # * :cookies will replace possible cookies in the :headers
+  # * :cookies may be a Hash{String/Symbol => String} of cookie values, an
+  #     Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
+  #     will be added to a cookie jar before the request is sent.
   # * :user and :password for basic auth, will be replaced by a user/password available in the :url
   # * :block_response call the provided block with the HTTPResponse as parameter
   # * :raw_response return a low-level RawResponse instead of a Response
   # * :max_redirects maximum number of redirections (default to 10)
+  # * :proxy An HTTP proxy URI to use for this request. Any value here
+  #   (including nil) will override RestClient.proxy.
   # * :verify_ssl enable ssl verification, possible values are constants from
   #     OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
-  # * :timeout and :open_timeout are how long to wait for a response and to
-  #     open a connection, in seconds. Pass nil to disable the timeout.
+  # * :read_timeout and :open_timeout are how long to wait for a response and
+  #     to open a connection, in seconds. Pass nil to disable the timeout.
+  # * :timeout can be used to set both timeouts
   # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
   #     :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
   # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
   # * :ssl_ciphers sets SSL ciphers for the connection. See
   #     OpenSSL::SSL::SSLContext#ciphers=
+  # * :before_execution_proc a Proc to call before executing the request. This
+  #      proc, like procs from RestClient.before_execution_procs, will be
+  #      called with the HTTP request and request params.
   class Request
 
-    attr_reader :method, :url, :headers, :cookies,
-                :payload, :user, :password, :timeout, :max_redirects,
+    attr_reader :method, :uri, :url, :headers, :payload, :proxy,
+                :user, :password, :read_timeout, :max_redirects,
                 :open_timeout, :raw_response, :processed_headers, :args,
                 :ssl_opts
 
+    # An array of previous redirection responses
+    attr_accessor :redirection_history
+
     def self.execute(args, & block)
       new(args).execute(& block)
     end
 
-    # This is similar to the list now in ruby core, but adds HIGH and RC4-MD5
-    # for better compatibility (similar to Firefox) and moves AES-GCM cipher
-    # suites above DHE/ECDHE CBC suites (similar to Chromium).
-    # https://github.com/ruby/ruby/commit/699b209cf8cf11809620e12985ad33ae33b119ee
-    #
-    # This list will be used by default if the Ruby global OpenSSL default
-    # ciphers appear to be a weak list.
-    DefaultCiphers = %w{
-      !aNULL
-      !eNULL
-      !EXPORT
-      !SSLV2
-      !LOW
-
-      ECDHE-ECDSA-AES128-GCM-SHA256
-      ECDHE-RSA-AES128-GCM-SHA256
-      ECDHE-ECDSA-AES256-GCM-SHA384
-      ECDHE-RSA-AES256-GCM-SHA384
-      DHE-RSA-AES128-GCM-SHA256
-      DHE-DSS-AES128-GCM-SHA256
-      DHE-RSA-AES256-GCM-SHA384
-      DHE-DSS-AES256-GCM-SHA384
-      AES128-GCM-SHA256
-      AES256-GCM-SHA384
-      ECDHE-ECDSA-AES128-SHA256
-      ECDHE-RSA-AES128-SHA256
-      ECDHE-ECDSA-AES128-SHA
-      ECDHE-RSA-AES128-SHA
-      ECDHE-ECDSA-AES256-SHA384
-      ECDHE-RSA-AES256-SHA384
-      ECDHE-ECDSA-AES256-SHA
-      ECDHE-RSA-AES256-SHA
-      DHE-RSA-AES128-SHA256
-      DHE-RSA-AES256-SHA256
-      DHE-RSA-AES128-SHA
-      DHE-RSA-AES256-SHA
-      DHE-DSS-AES128-SHA256
-      DHE-DSS-AES256-SHA256
-      DHE-DSS-AES128-SHA
-      DHE-DSS-AES256-SHA
-      AES128-SHA256
-      AES256-SHA256
-      AES128-SHA
-      AES256-SHA
-      ECDHE-ECDSA-RC4-SHA
-      ECDHE-RSA-RC4-SHA
-      RC4-SHA
-
-      HIGH
-      +RC4
-      RC4-MD5
-    }.join(":")
-
-    # A set of weak default ciphers that we will override by default.
-    WeakDefaultCiphers = Set.new([
-      "ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW",
-    ])
-
     SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
                        version ciphers verify_callback verify_callback_warnings}
 
+    def inspect
+      "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
+    end
+
     def initialize args
-      @method = args[:method] or raise ArgumentError, "must pass :method"
-      @headers = args[:headers] || {}
+      @method = normalize_method(args[:method])
+      @headers = (args[:headers] || {}).dup
       if args[:url]
-        @url = process_url_params(args[:url], headers)
+        @url = process_url_params(normalize_url(args[:url]), headers)
       else
         raise ArgumentError, "must pass :url"
       end
-      @cookies = @headers.delete(:cookies) || args[:cookies] || {}
+
+      @user = @password = nil
+      parse_url_with_auth!(url)
+
+      # process cookie arguments found in headers or args
+      @cookie_jar = process_cookie_args!(@uri, @headers, args)
+
       @payload = Payload.generate(args[:payload])
-      @user = args[:user]
-      @password = args[:password]
+
+      @user = args[:user] if args.include?(:user)
+      @password = args[:password] if args.include?(:password)
+
       if args.include?(:timeout)
-        @timeout = args[:timeout]
+        @read_timeout = args[:timeout]
+        @open_timeout = args[:timeout]
+      end
+      if args.include?(:read_timeout)
+        @read_timeout = args[:read_timeout]
       end
       if args.include?(:open_timeout)
         @open_timeout = args[:open_timeout]
@@ -123,6 +92,8 @@ module RestClient
       @block_response = args[:block_response]
       @raw_response = args[:raw_response] || false
 
+      @proxy = args.fetch(:proxy) if args.include?(:proxy)
+
       @ssl_opts = {}
 
       if args.include?(:verify_ssl)
@@ -151,17 +122,12 @@ module RestClient
         end
       end
 
-      # If there's no CA file, CA path, or cert store provided, use default
-      if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
-        @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
-      end
+      # Set some other default SSL options, but only if we have an HTTPS URI.
+      if use_ssl?
 
-      unless @ssl_opts.include?(:ciphers)
-        # If we're on a Ruby version that has insecure default ciphers,
-        # override it with our default list.
-        if WeakDefaultCiphers.include?(
-             OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.fetch(:ciphers))
-          @ssl_opts[:ciphers] = DefaultCiphers
+        # If there's no CA file, CA path, or cert store provided, use default
+        if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
+          @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
         end
       end
 
@@ -169,11 +135,14 @@ module RestClient
       @max_redirects = args[:max_redirects] || 10
       @processed_headers = make_headers headers
       @args = args
+
+      @before_execution_proc = args[:before_execution_proc]
     end
 
     def execute & block
-      uri = parse_url_with_auth(url)
-      transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
+      # With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
+      # IPv6 addresses in [] for use in the Host request header.
+      transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
     ensure
       payload.close if payload
     end
@@ -188,82 +157,291 @@ module RestClient
       end
     end
 
+    # Return true if the request URI will use HTTPS.
+    #
+    # @return [Boolean]
+    #
+    def use_ssl?
+      uri.is_a?(URI::HTTPS)
+    end
+
     # Extract the query parameters and append them to the url
-    def process_url_params url, headers
-      url_params = {}
+    #
+    # Look through the headers hash for a :params option (case-insensitive,
+    # may be string or symbol). If present and the value is a Hash or
+    # RestClient::ParamsArray, *delete* the key/value pair from the headers
+    # hash and encode the value into a query string. Append this query string
+    # to the URL and return the resulting URL.
+    #
+    # @param [String] url
+    # @param [Hash] headers An options/headers hash to process. Mutation
+    #   warning: the params key may be removed if present!
+    #
+    # @return [String] resulting url with query string
+    #
+    def process_url_params(url, headers)
+      url_params = nil
+
+      # find and extract/remove "params" key if the value is a Hash/ParamsArray
       headers.delete_if do |key, value|
-        if 'params' == key.to_s.downcase && value.is_a?(Hash)
-          url_params.merge! value
+        if key.to_s.downcase == 'params' &&
+            (value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
+          if url_params
+            raise ArgumentError.new("Multiple 'params' options passed")
+          end
+          url_params = value
           true
         else
           false
         end
       end
-      unless url_params.empty?
-        query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
-        url + "?#{query_string}"
+
+      # build resulting URL with query string
+      if url_params && !url_params.empty?
+        query_string = RestClient::Utils.encode_query_string(url_params)
+
+        if url.include?('?')
+          url + '&' + query_string
+        else
+          url + '?' + query_string
+        end
       else
         url
       end
     end
 
-    def make_headers user_headers
-      unless @cookies.empty?
+    # Render a hash of key => value pairs for cookies in the Request#cookie_jar
+    # that are valid for the Request#uri. This will not necessarily include all
+    # cookies if there are duplicate keys. It's safer to use the cookie_jar
+    # directly if that's a concern.
+    #
+    # @see Request#cookie_jar
+    #
+    # @return [Hash]
+    #
+    def cookies
+      hash = {}
 
-        # Validate that the cookie names and values look sane. If you really
-        # want to pass scary characters, just set the Cookie header directly.
-        # RFC6265 is actually much more restrictive than we are.
-        @cookies.each do |key, val|
-          unless valid_cookie_key?(key)
-            raise ArgumentError.new("Invalid cookie name: #{key.inspect}")
-          end
-          unless valid_cookie_value?(val)
-            raise ArgumentError.new("Invalid cookie value: #{val.inspect}")
+      @cookie_jar.cookies(uri).each do |c|
+        hash[c.name] = c.value
+      end
+
+      hash
+    end
+
+    # @return [HTTP::CookieJar]
+    def cookie_jar
+      @cookie_jar
+    end
+
+    # Render a Cookie HTTP request header from the contents of the @cookie_jar,
+    # or nil if the jar is empty.
+    #
+    # @see Request#cookie_jar
+    #
+    # @return [String, nil]
+    #
+    def make_cookie_header
+      return nil if cookie_jar.nil?
+
+      arr = cookie_jar.cookies(url)
+      return nil if arr.empty?
+
+      return HTTP::Cookie.cookie_value(arr)
+    end
+
+    # Process cookies passed as hash or as HTTP::CookieJar. For backwards
+    # compatibility, these may be passed as a :cookies option masquerading
+    # inside the headers hash. To avoid confusion, if :cookies is passed in
+    # both headers and Request#initialize, raise an error.
+    #
+    # :cookies may be a:
+    # - Hash{String/Symbol => String}
+    # - Array<HTTP::Cookie>
+    # - HTTP::CookieJar
+    #
+    # Passing as a hash:
+    #   Keys may be symbols or strings. Values must be strings.
+    #   Infer the domain name from the request URI and allow subdomains (as
+    #   though '.example.com' had been set in a Set-Cookie header). Assume a
+    #   path of '/'.
+    #
+    #     RestClient::Request.new(url: 'http://example.com', method: :get,
+    #       :cookies => {:foo => 'Value', 'bar' => '123'}
+    #     )
+    #
+    # results in cookies as though set from the server by:
+    #     Set-Cookie: foo=Value; Domain=.example.com; Path=/
+    #     Set-Cookie: bar=123; Domain=.example.com; Path=/
+    #
+    # which yields a client cookie header of:
+    #     Cookie: foo=Value; bar=123
+    #
+    # Passing as HTTP::CookieJar, which will be passed through directly:
+    #
+    #     jar = HTTP::CookieJar.new
+    #     jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
+    #                              path: '/', for_domain: false))
+    #
+    #     RestClient::Request.new(..., :cookies => jar)
+    #
+    # @param [URI::HTTP] uri The URI for the request. This will be used to
+    # infer the domain name for cookies passed as strings in a hash. To avoid
+    # this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
+    # values.
+    # @param [Hash] headers The headers hash from which to pull the :cookies
+    #   option. MUTATION NOTE: This key will be deleted from the hash if
+    #   present.
+    # @param [Hash] args The options passed to Request#initialize. This hash
+    #   will be used as another potential source for the :cookies key.
+    #   These args will not be mutated.
+    #
+    # @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
+    #
+    def process_cookie_args!(uri, headers, args)
+
+      # Avoid ambiguity in whether options from headers or options from
+      # Request#initialize should take precedence by raising ArgumentError when
+      # both are present. Prior versions of rest-client claimed to give
+      # precedence to init options, but actually gave precedence to headers.
+      # Avoid that mess by erroring out instead.
+      if headers[:cookies] && args[:cookies]
+        raise ArgumentError.new(
+          "Cannot pass :cookies in Request.new() and in headers hash")
+      end
+
+      cookies_data = headers.delete(:cookies) || args[:cookies]
+
+      # return copy of cookie jar as is
+      if cookies_data.is_a?(HTTP::CookieJar)
+        return cookies_data.dup
+      end
+
+      # convert cookies hash into a CookieJar
+      jar = HTTP::CookieJar.new
+
+      (cookies_data || []).each do |key, val|
+
+        # Support for Array<HTTP::Cookie> mode:
+        # If key is a cookie object, add it to the jar directly and assert that
+        # there is no separate val.
+        if key.is_a?(HTTP::Cookie)
+          if val
+            raise ArgumentError.new("extra cookie val: #{val.inspect}")
           end
+
+          jar.add(key)
+          next
+        end
+
+        if key.is_a?(Symbol)
+          key = key.to_s
         end
 
-        user_headers[:cookie] = @cookies.map { |key, val| "#{key}=#{val}" }.sort.join('; ')
+        # assume implicit domain from the request URI, and set for_domain to
+        # permit subdomains
+        jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
+                                 path: '/', for_domain: true))
       end
-      headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
-      headers.merge!(@payload.headers) if @payload
-      headers
+
+      jar
     end
 
-    # Do some sanity checks on cookie keys.
+    # Generate headers for use by a request. Header keys will be stringified
+    # using `#stringify_headers` to normalize them as capitalized strings.
     #
-    # Properly it should be a valid TOKEN per RFC 2616, but lots of servers are
-    # more liberal.
+    # The final headers consist of:
+    #   - default headers from #default_headers
+    #   - user_headers provided here
+    #   - headers from the payload object (e.g. Content-Type, Content-Lenth)
+    #   - cookie headers from #make_cookie_header
     #
-    # Disallow the empty string as well as keys containing control characters,
-    # equals sign, semicolon, comma, or space.
+    # @param [Hash] user_headers User-provided headers to include
     #
-    def valid_cookie_key?(string)
-      return false if string.empty?
+    # @return [Hash<String, String>] A hash of HTTP headers => values
+    #
+    def make_headers(user_headers)
+      headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
 
-      ! Regexp.new('[\x0-\x1f\x7f=;, ]').match(string)
+      # override headers from the payload (e.g. Content-Type, Content-Length)
+      if @payload
+        payload_headers = @payload.headers
+
+        # Warn the user if we override any headers that were previously
+        # present. This usually indicates that rest-client was passed
+        # conflicting information, e.g. if it was asked to render a payload as
+        # x-www-form-urlencoded but a Content-Type application/json was
+        # also supplied by the user.
+        payload_headers.each_pair do |key, val|
+          if headers.include?(key) && headers[key] != val
+            warn("warning: Overriding #{key.inspect} header " +
+                 "#{headers.fetch(key).inspect} with #{val.inspect} " +
+                 "due to payload")
+          end
+        end
+
+        headers.merge!(payload_headers)
+      end
+
+      # merge in cookies
+      cookies = make_cookie_header
+      if cookies && !cookies.empty?
+        if headers['Cookie']
+          warn('warning: overriding "Cookie" header with :cookies option')
+        end
+        headers['Cookie'] = cookies
+      end
+
+      headers
     end
 
-    # Validate cookie values. Rather than following RFC 6265, allow anything
-    # but control characters, comma, and semicolon.
-    def valid_cookie_value?(value)
-      ! Regexp.new('[\x0-\x1f\x7f,;]').match(value)
+    # The proxy URI for this request. If `:proxy` was provided on this request,
+    # use it over `RestClient.proxy`.
+    #
+    # Return false if a proxy was explicitly set and is falsy.
+    #
+    # @return [URI, false, nil]
+    #
+    def proxy_uri
+      if defined?(@proxy)
+        if @proxy
+          URI.parse(@proxy)
+        else
+          false
+        end
+      elsif RestClient.proxy_set?
+        if RestClient.proxy
+          URI.parse(RestClient.proxy)
+        else
+          false
+        end
+      else
+        nil
+      end
     end
 
-    def net_http_class
-      if RestClient.proxy
-        proxy_uri = URI.parse(RestClient.proxy)
-        Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
+    def net_http_object(hostname, port)
+      p_uri = proxy_uri
+
+      if p_uri.nil?
+        # no proxy set
+        Net::HTTP.new(hostname, port)
+      elsif !p_uri
+        # proxy explicitly set to none
+        Net::HTTP.new(hostname, port, nil, nil, nil, nil)
       else
-        Net::HTTP
+        Net::HTTP.new(hostname, port,
+                      p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
+
       end
     end
 
     def net_http_request_class(method)
-      Net::HTTP.const_get(method.to_s.capitalize)
+      Net::HTTP.const_get(method.capitalize, false)
     end
 
     def net_http_do_request(http, req, body=nil, &block)
-      if body != nil && body.respond_to?(:read)
+      if body && body.respond_to?(:read)
         req.body_stream = body
         return http.request(req, nil, &block)
       else
@@ -271,36 +449,19 @@ module RestClient
       end
     end
 
-    def parse_url(url)
-      url = "http://#{url}" unless url.match(/^http/)
-      URI.parse(url)
-    end
-
-    def parse_url_with_auth(url)
-      uri = parse_url(url)
-      @user = CGI.unescape(uri.user) if uri.user
-      @password = CGI.unescape(uri.password) if uri.password
-      if !@user && !@password
-        @user, @password = Netrc.read[uri.host]
-      end
-      uri
-    end
-
-    def process_payload(p=nil, parent_key=nil)
-      unless p.is_a?(Hash)
-        p
-      else
-        @headers[:content_type] ||= 'application/x-www-form-urlencoded'
-        p.keys.map do |k|
-          key = parent_key ? "#{parent_key}[#{k}]" : k
-          if p[k].is_a? Hash
-            process_payload(p[k], key)
-          else
-            value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
-            "#{key}=#{value}"
-          end
-        end.join("&")
-      end
+    # Normalize a URL by adding a protocol if none is present.
+    #
+    # If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
+    # scheme of 'http' will be added. This mimics the behavior of browsers and
+    # user agents like cURL.
+    #
+    # @param [String] url A URL string.
+    #
+    # @return [String]
+    #
+    def normalize_url(url)
+      url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
+      url
     end
 
     # Return a certificate store that can be used to validate certificates with
@@ -332,6 +493,122 @@ module RestClient
       cert_store
     end
 
+    def self.decode content_encoding, body
+      if (!body) || body.empty?
+        body
+      elsif content_encoding == 'gzip'
+        Zlib::GzipReader.new(StringIO.new(body)).read
+      elsif content_encoding == 'deflate'
+        begin
+          Zlib::Inflate.new.inflate body
+        rescue Zlib::DataError
+          # No luck with Zlib decompression. Let's try with raw deflate,
+          # like some broken web servers do.
+          Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
+        end
+      else
+        body
+      end
+    end
+
+    def redacted_uri
+      if uri.password
+        sanitized_uri = uri.dup
+        sanitized_uri.password = 'REDACTED'
+        sanitized_uri
+      else
+        uri
+      end
+    end
+
+    def redacted_url
+      redacted_uri.to_s
+    end
+
+    def log_request
+      return unless RestClient.log
+
+      out = []
+
+      out << "RestClient.#{method} #{redacted_url.inspect}"
+      out << payload.short_inspect if payload
+      out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
+      RestClient.log << out.join(', ') + "\n"
+    end
+
+    def log_response res
+      return unless RestClient.log
+
+      size = if @raw_response
+               File.size(@tf.path)
+             else
+               res.body.nil? ? 0 : res.body.size
+             end
+
+      RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
+    end
+
+    # Return a hash of headers whose keys are capitalized strings
+    def stringify_headers headers
+      headers.inject({}) do |result, (key, value)|
+        if key.is_a? Symbol
+          key = key.to_s.split(/_/).map(&:capitalize).join('-')
+        end
+        if 'CONTENT-TYPE' == key.upcase
+          result[key] = maybe_convert_extension(value.to_s)
+        elsif 'ACCEPT' == key.upcase
+          # Accept can be composed of several comma-separated values
+          if value.is_a? Array
+            target_values = value
+          else
+            target_values = value.to_s.split ','
+          end
+          result[key] = target_values.map { |ext|
+            maybe_convert_extension(ext.to_s.strip)
+          }.join(', ')
+        else
+          result[key] = value.to_s
+        end
+        result
+      end
+    end
+
+    def default_headers
+      {
+        :accept => '*/*',
+        :accept_encoding => 'gzip, deflate',
+        :user_agent => RestClient::Platform.default_user_agent,
+      }
+    end
+
+    private
+
+    # Parse the `@url` string into a URI object and save it as
+    # `@uri`. Also save any basic auth user or password as @user and @password.
+    # If no auth info was passed, check for credentials in a Netrc file.
+    #
+    # @param [String] url A URL string.
+    #
+    # @return [URI]
+    #
+    # @raise URI::InvalidURIError on invalid URIs
+    #
+    def parse_url_with_auth!(url)
+      uri = URI.parse(url)
+
+      if uri.hostname.nil?
+        raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
+      end
+
+      @user = CGI.unescape(uri.user) if uri.user
+      @password = CGI.unescape(uri.password) if uri.password
+      if !@user && !@password
+        @user, @password = Netrc.read[uri.hostname]
+      end
+
+      @uri = uri
+    end
+
     def print_verify_callback_warnings
       warned = false
       if RestClient::Platform.mac_mri?
@@ -346,10 +623,32 @@ module RestClient
       warned
     end
 
+    # Parse a method and return a normalized string version.
+    #
+    # Raise ArgumentError if the method is falsy, but otherwise do no
+    # validation.
+    #
+    # @param method [String, Symbol]
+    #
+    # @return [String]
+    #
+    # @see net_http_request_class
+    #
+    def normalize_method(method)
+      raise ArgumentError.new('must pass :method') unless method
+      method.to_s.downcase
+    end
+
     def transmit uri, req, payload, & block
+
+      # We set this to true in the net/http block so that we can distinguish
+      # read_timeout from open_timeout. Now that we only support Ruby 2.0+,
+      # this is only needed for Timeout exceptions thrown outside of Net::HTTP.
+      established_connection = false
+
       setup_credentials req
 
-      net = net_http_class.new(uri.host, uri.port)
+      net = net_http_object(uri.hostname, uri.port)
       net.use_ssl = uri.is_a?(URI::HTTPS)
       net.ssl_version = ssl_version if ssl_version
       net.ciphers = ssl_ciphers if ssl_ciphers
@@ -388,16 +687,16 @@ module RestClient
         warn('Try passing :verify_ssl => false instead.')
       end
 
-      if defined? @timeout
-        if @timeout == -1
-          warn 'To disable read timeouts, please set timeout to nil instead of -1'
-          @timeout = nil
+      if defined? @read_timeout
+        if @read_timeout == -1
+          warn 'Deprecated: to disable timeouts, please use nil instead of -1'
+          @read_timeout = nil
         end
-        net.read_timeout = @timeout
+        net.read_timeout = @read_timeout
       end
       if defined? @open_timeout
         if @open_timeout == -1
-          warn 'To disable open timeouts, please set open_timeout to nil instead of -1'
+          warn 'Deprecated: to disable timeouts, please use nil instead of -1'
           @open_timeout = nil
         end
         net.open_timeout = @open_timeout
@@ -407,24 +706,38 @@ module RestClient
         before_proc.call(req, args)
       end
 
-      log_request
+      if @before_execution_proc
+        @before_execution_proc.call(req, args)
+      end
 
+      log_request
 
       net.start do |http|
+        established_connection = true
+
         if @block_response
-          net_http_do_request(http, req, payload ? payload.to_s : nil,
-                              &@block_response)
+          net_http_do_request(http, req, payload, &@block_response)
         else
-          res = net_http_do_request(http, req, payload ? payload.to_s : nil) \
-            { |http_response| fetch_body(http_response) }
+          res = net_http_do_request(http, req, payload) { |http_response|
+            fetch_body(http_response)
+          }
           log_response res
           process_result res, & block
         end
       end
     rescue EOFError
       raise RestClient::ServerBrokeConnection
-    rescue Timeout::Error, Errno::ETIMEDOUT
-      raise RestClient::RequestTimeout
+    rescue Net::OpenTimeout => err
+      raise RestClient::Exceptions::OpenTimeout.new(nil, err)
+    rescue Net::ReadTimeout => err
+      raise RestClient::Exceptions::ReadTimeout.new(nil, err)
+    rescue Timeout::Error, Errno::ETIMEDOUT => err
+      # handling for non-Net::HTTP timeouts
+      if established_connection
+        raise RestClient::Exceptions::ReadTimeout.new(nil, err)
+      else
+        raise RestClient::Exceptions::OpenTimeout.new(nil, err)
+      end
 
     rescue OpenSSL::SSL::SSLError => error
       # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
@@ -449,7 +762,7 @@ module RestClient
     end
 
     def setup_credentials(req)
-      req.basic_auth(user, password) if user
+      req.basic_auth(user, password) if user && !headers.has_key?("Authorization")
     end
 
     def fetch_body(http_response)
@@ -457,9 +770,9 @@ module RestClient
         # Taken from Chef, which as in turn...
         # Stolen from http://www.ruby-forum.com/topic/166423
         # Kudos to _why!
-        @tf = Tempfile.new("rest-client")
+        @tf = Tempfile.new('rest-client.')
         @tf.binmode
-        size, total = 0, http_response.header['Content-Length'].to_i
+        size, total = 0, http_response['Content-Length'].to_i
         http_response.read_body do |chunk|
           @tf.write chunk
           size += chunk.size
@@ -484,101 +797,20 @@ module RestClient
     def process_result res, & block
       if @raw_response
         # We don't decode raw requests
-        response = RawResponse.new(@tf, res, args, self)
+        response = RawResponse.new(@tf, res, self)
       else
-        response = Response.create(Request.decode(res['content-encoding'], res.body), res, args, self)
+        decoded = Request.decode(res['content-encoding'], res.body)
+        response = Response.create(decoded, res, self)
       end
 
       if block_given?
         block.call(response, self, res, & block)
       else
-        response.return!(self, res, & block)
-      end
-
-    end
-
-    def self.decode content_encoding, body
-      if (!body) || body.empty?
-        body
-      elsif content_encoding == 'gzip'
-        Zlib::GzipReader.new(StringIO.new(body)).read
-      elsif content_encoding == 'deflate'
-        begin
-          Zlib::Inflate.new.inflate body
-        rescue Zlib::DataError
-          # No luck with Zlib decompression. Let's try with raw deflate,
-          # like some broken web servers do.
-          Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
-        end
-      else
-        body
-      end
-    end
-
-    def log_request
-      return unless RestClient.log
-
-      out = []
-      sanitized_url = begin
-        uri = URI.parse(url)
-        uri.password = "REDACTED" if uri.password
-        uri.to_s
-      rescue URI::InvalidURIError
-        # An attacker may be able to manipulate the URL to be
-        # invalid, which could force discloure of a password if
-        # we show any of the un-parsed URL here.
-        "[invalid uri]"
-      end
-
-      out << "RestClient.#{method} #{sanitized_url.inspect}"
-      out << payload.short_inspect if payload
-      out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
-      RestClient.log << out.join(', ') + "\n"
-    end
-
-    def log_response res
-      return unless RestClient.log
-
-      size = if @raw_response
-               File.size(@tf.path)
-             else
-               res.body.nil? ? 0 : res.body.size
-             end
-
-      RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
-    end
-
-    # Return a hash of headers whose keys are capitalized strings
-    def stringify_headers headers
-      headers.inject({}) do |result, (key, value)|
-        if key.is_a? Symbol
-          key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
-        end
-        if 'CONTENT-TYPE' == key.upcase
-          result[key] = maybe_convert_extension(value.to_s)
-        elsif 'ACCEPT' == key.upcase
-          # Accept can be composed of several comma-separated values
-          if value.is_a? Array
-            target_values = value
-          else
-            target_values = value.to_s.split ','
-          end
-          result[key] = target_values.map { |ext|
-            maybe_convert_extension(ext.to_s.strip)
-          }.join(', ')
-        else
-          result[key] = value.to_s
-        end
-        result
+        response.return!(&block)
       end
-    end
 
-    def default_headers
-      {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
     end
 
-    private
-
     def parser
       URI.const_defined?(:Parser) ? URI::Parser.new : URI
     end
diff --git a/lib/restclient/resource.rb b/lib/restclient/resource.rb
index 0df636a..955f682 100644
--- a/lib/restclient/resource.rb
+++ b/lib/restclient/resource.rb
@@ -14,7 +14,7 @@ module RestClient
   #
   # With a timeout (seconds):
   #
-  #   RestClient::Resource.new('http://slow', :timeout => 10)
+  #   RestClient::Resource.new('http://slow', :read_timeout => 10)
   #
   # With an open timeout (seconds):
   #
@@ -113,8 +113,8 @@ module RestClient
       options[:headers] || {}
     end
 
-    def timeout
-      options[:timeout]
+    def read_timeout
+      options[:read_timeout]
     end
 
     def open_timeout
diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb
index 47d8af5..1f0ba89 100644
--- a/lib/restclient/response.rb
+++ b/lib/restclient/response.rb
@@ -2,20 +2,79 @@ module RestClient
 
   # A Response from RestClient, you can access the response body, the code or the headers.
   #
-  module Response
+  class Response < String
 
     include AbstractResponse
 
+    # Return the HTTP response body.
+    #
+    # Future versions of RestClient will deprecate treating response objects
+    # directly as strings, so it will be necessary to call `.body`.
+    #
+    # @return [String]
+    #
     def body
-      self
+      # Benchmarking suggests that "#{self}" is fastest, and that caching the
+      # body string in an instance variable doesn't make it enough faster to be
+      # worth the extra memory storage.
+      String.new(self)
     end
 
-    def self.create body, net_http_res, args, request
-      result = body || ''
-      result.extend Response
-      result.response_set_vars(net_http_res, args, request)
+    # Convert the HTTP response body to a pure String object.
+    #
+    # @return [String]
+    def to_s
+      body
+    end
+
+    # Convert the HTTP response body to a pure String object.
+    #
+    # @return [String]
+    def to_str
+      body
+    end
+
+    def inspect
+      "<RestClient::Response #{code.inspect} #{body_truncated(10).inspect}>"
+    end
+
+    def self.create(body, net_http_res, request)
+      result = self.new(body || '')
+
+      result.response_set_vars(net_http_res, request)
+      fix_encoding(result)
+
       result
     end
 
+    def self.fix_encoding(response)
+      charset = RestClient::Utils.get_encoding_from_headers(response.headers)
+      encoding = nil
+
+      begin
+        encoding = Encoding.find(charset) if charset
+      rescue ArgumentError
+        if RestClient.log
+          RestClient.log << "No such encoding: #{charset.inspect}"
+        end
+      end
+
+      return unless encoding
+
+      response.force_encoding(encoding)
+
+      response
+    end
+
+    private
+
+    def body_truncated(length)
+      b = body
+      if b.length > length
+        b[0..length] + '...'
+      else
+        b
+      end
+    end
   end
 end
diff --git a/lib/restclient/utils.rb b/lib/restclient/utils.rb
new file mode 100644
index 0000000..d41eefa
--- /dev/null
+++ b/lib/restclient/utils.rb
@@ -0,0 +1,235 @@
+module RestClient
+  # Various utility methods
+  module Utils
+
+    # Return encoding from an HTTP header hash.
+    #
+    # We use the RFC 7231 specification and do not impose a default encoding on
+    # text. This differs from the older RFC 2616 behavior, which specifies
+    # using ISO-8859-1 for text/* content types without a charset.
+    #
+    # Strings will use the default encoding when this method returns nil. This
+    # default is likely to be UTF-8 for Ruby >= 2.0
+    #
+    # @param headers [Hash<Symbol,String>]
+    #
+    # @return [String, nil] encoding Return the string encoding or nil if no
+    #   header is found.
+    #
+    # @example
+    #   >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
+    #   => "UTF-8"
+    #
+    def self.get_encoding_from_headers(headers)
+      type_header = headers[:content_type]
+      return nil unless type_header
+
+      _content_type, params = cgi_parse_header(type_header)
+
+      if params.include?('charset')
+        return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
+      end
+
+      nil
+    end
+
+    # Parse semi-colon separated, potentially quoted header string iteratively.
+    #
+    # @private
+    #
+    def self._cgi_parseparam(s)
+      return enum_for(__method__, s) unless block_given?
+
+      while s[0] == ';'
+        s = s[1..-1]
+        ends = s.index(';')
+        while ends && ends > 0 \
+              && (s[0...ends].count('"') -
+                  s[0...ends].scan('\"').count) % 2 != 0
+          ends = s.index(';', ends + 1)
+        end
+        if ends.nil?
+          ends = s.length
+        end
+        f = s[0...ends]
+        yield f.strip
+        s = s[ends..-1]
+      end
+      nil
+    end
+
+    # Parse a Content-Type like header.
+    #
+    # Return the main content-type and a hash of options.
+    #
+    # This method was ported directly from Python's cgi.parse_header(). It
+    # probably doesn't read or perform particularly well in ruby.
+    # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331
+    #
+    #
+    # @param [String] line
+    # @return [Array(String, Hash)]
+    #
+    def self.cgi_parse_header(line)
+      parts = _cgi_parseparam(';' + line)
+      key = parts.next
+      pdict = {}
+
+      begin
+        while (p = parts.next)
+          i = p.index('=')
+          if i
+            name = p[0...i].strip.downcase
+            value = p[i+1..-1].strip
+            if value.length >= 2 && value[0] == '"' && value[-1] == '"'
+              value = value[1...-1]
+              value = value.gsub('\\\\', '\\').gsub('\\"', '"')
+            end
+            pdict[name] = value
+          end
+        end
+      rescue StopIteration
+      end
+
+      [key, pdict]
+    end
+
+    # Serialize a ruby object into HTTP query string parameters.
+    #
+    # There is no standard for doing this, so we choose our own slightly
+    # idiosyncratic format. The output closely matches the format understood by
+    # Rails, Rack, and PHP.
+    #
+    # If you don't want handling of complex objects and only want to handle
+    # simple flat hashes, you may want to use `URI.encode_www_form` instead,
+    # which implements HTML5-compliant URL encoded form data.
+    #
+    # @param [Hash,ParamsArray] object The object to serialize
+    #
+    # @return [String] A string appropriate for use as an HTTP query string
+    #
+    # @see {flatten_params}
+    #
+    # @see URI.encode_www_form
+    #
+    # @see See also Object#to_query in ActiveSupport
+    # @see http://php.net/manual/en/function.http-build-query.php
+    #   http_build_query in PHP
+    # @see See also Rack::Utils.build_nested_query in Rack
+    #
+    # Notable differences from the ActiveSupport implementation:
+    #
+    # - Empty hash and empty array are treated the same as nil instead of being
+    #   omitted entirely from the output. Rather than disappearing, they will
+    #   appear to be nil instead.
+    #
+    # It's most common to pass a Hash as the object to serialize, but you can
+    # also use a ParamsArray if you want to be able to pass the same key with
+    # multiple values and not use the rack/rails array convention.
+    #
+    # @since 2.0.0
+    #
+    # @example Simple hashes
+    #   >> encode_query_string({foo: 123, bar: 456})
+    #   => 'foo=123&bar=456'
+    #
+    # @example Simple arrays
+    #   >> encode_query_string({foo: [1,2,3]})
+    #   => 'foo[]=1&foo[]=2&foo[]=3'
+    #
+    # @example Nested hashes
+    #   >> encode_query_string({outer: {foo: 123, bar: 456}})
+    #   => 'outer[foo]=123&outer[bar]=456'
+    #
+    # @example Deeply nesting
+    #   >> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]})
+    #   => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3'
+    #
+    # @example Null and empty values
+    #   >> encode_query_string({string: '', empty: nil, list: [], hash: {}})
+    #   => 'string=&empty&list&hash'
+    #
+    # @example Nested nulls
+    #   >> encode_query_string({foo: {string: '', empty: nil}})
+    #   => 'foo[string]=&foo[empty]'
+    #
+    # @example Multiple fields with the same name using ParamsArray
+    #   >> encode_query_string(RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]))
+    #   => 'foo=1&foo=2&foo=3'
+    #
+    # @example Nested ParamsArray
+    #   >> encode_query_string({foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])})
+    #   => 'foo[a]=1&foo[a]=2'
+    #
+    #   >> encode_query_string(RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]))
+    #   => 'foo[a]=1&foo[a]=2'
+    #
+    def self.encode_query_string(object)
+      flatten_params(object, true).map {|k, v| v.nil? ? k : "#{k}=#{v}" }.join('&')
+    end
+
+    # Transform deeply nested param containers into a flat array of [key,
+    # value] pairs.
+    #
+    # @example
+    #   >> flatten_params({key1: {key2: 123}})
+    #   => [["key1[key2]", 123]]
+    #
+    # @example
+    #   >> flatten_params({key1: {key2: 123, arr: [1,2,3]}})
+    #   => [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]]
+    #
+    # @param object [Hash, ParamsArray] The container to flatten
+    # @param uri_escape [Boolean] Whether to URI escape keys and values
+    # @param parent_key [String] Should not be passed (used for recursion)
+    #
+    def self.flatten_params(object, uri_escape=false, parent_key=nil)
+      unless object.is_a?(Hash) || object.is_a?(ParamsArray) ||
+             (parent_key && object.is_a?(Array))
+        raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect)
+      end
+
+      # transform empty collections into nil, where possible
+      if object.empty? && parent_key
+        return [[parent_key, nil]]
+      end
+
+      # This is essentially .map(), but we need to do += for nested containers
+      object.reduce([]) { |result, item|
+        if object.is_a?(Array)
+          # item is already the value
+          k = nil
+          v = item
+        else
+          # item is a key, value pair
+          k, v = item
+          k = escape(k.to_s) if uri_escape
+        end
+
+        processed_key = parent_key ? "#{parent_key}[#{k}]" : k
+
+        case v
+        when Array, Hash, ParamsArray
+          result.concat flatten_params(v, uri_escape, processed_key)
+        else
+          v = escape(v.to_s) if uri_escape && v
+          result << [processed_key, v]
+        end
+      }
+    end
+
+    # Encode string for safe transport by URI or form encoding. This uses a CGI
+    # style escape, which transforms ` ` into `+` and various special
+    # characters into percent encoded forms.
+    #
+    # This calls URI.encode_www_form_component for the implementation. The only
+    # difference between this and CGI.escape is that it does not escape `*`.
+    # http://stackoverflow.com/questions/25085992/
+    #
+    # @see URI.encode_www_form_component
+    #
+    def self.escape(string)
+      URI.encode_www_form_component(string)
+    end
+  end
+end
diff --git a/lib/restclient/version.rb b/lib/restclient/version.rb
index 5f71842..d7367a2 100644
--- a/lib/restclient/version.rb
+++ b/lib/restclient/version.rb
@@ -1,5 +1,6 @@
 module RestClient
-  VERSION = '1.8.0' unless defined?(self::VERSION)
+  VERSION_INFO = [2, 0, 2] unless defined?(self::VERSION_INFO)
+  VERSION = VERSION_INFO.map(&:to_s).join('.') unless defined?(self::VERSION)
 
   def self.version
     VERSION
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 2668e98..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,253 +0,0 @@
---- !ruby/object:Gem::Specification
-name: rest-client
-version: !ruby/object:Gem::Version
-  version: 1.8.0
-platform: ruby
-authors:
-- REST Client Team
-autorequire: 
-bindir: bin
-cert_chain: []
-date: 2015-03-24 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
-  name: webmock
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1.4'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1.4'
-- !ruby/object:Gem::Dependency
-  name: rspec
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '2.4'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '2.4'
-- !ruby/object:Gem::Dependency
-  name: pry
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: pry-doc
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: rdoc
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 2.4.2
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '5.0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 2.4.2
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '5.0'
-- !ruby/object:Gem::Dependency
-  name: http-cookie
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.0.2
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '2.0'
-  type: :runtime
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.0.2
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '2.0'
-- !ruby/object:Gem::Dependency
-  name: mime-types
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '1.16'
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '3.0'
-  type: :runtime
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '1.16'
-    - - "<"
-      - !ruby/object:Gem::Version
-        version: '3.0'
-- !ruby/object:Gem::Dependency
-  name: netrc
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '0.7'
-  type: :runtime
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '0.7'
-description: 'A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework
-  style of specifying actions: get, put, post, delete.'
-email: rest.client at librelist.com
-executables:
-- restclient
-extensions: []
-extra_rdoc_files:
-- README.rdoc
-- history.md
-files:
-- ".gitignore"
-- ".rspec"
-- ".travis.yml"
-- AUTHORS
-- Gemfile
-- LICENSE
-- README.rdoc
-- Rakefile
-- bin/restclient
-- history.md
-- lib/rest-client.rb
-- lib/rest_client.rb
-- lib/restclient.rb
-- lib/restclient/abstract_response.rb
-- lib/restclient/exceptions.rb
-- lib/restclient/payload.rb
-- lib/restclient/platform.rb
-- lib/restclient/raw_response.rb
-- lib/restclient/request.rb
-- lib/restclient/resource.rb
-- lib/restclient/response.rb
-- lib/restclient/version.rb
-- lib/restclient/windows.rb
-- lib/restclient/windows/root_certs.rb
-- rest-client.gemspec
-- rest-client.windows.gemspec
-- spec/integration/capath_digicert/244b5494.0
-- spec/integration/capath_digicert/81b9768f.0
-- spec/integration/capath_digicert/README
-- spec/integration/capath_digicert/digicert.crt
-- spec/integration/capath_verisign/415660c1.0
-- spec/integration/capath_verisign/7651b327.0
-- spec/integration/capath_verisign/README
-- spec/integration/capath_verisign/verisign.crt
-- spec/integration/certs/digicert.crt
-- spec/integration/certs/verisign.crt
-- spec/integration/integration_spec.rb
-- spec/integration/request_spec.rb
-- spec/spec_helper.rb
-- spec/unit/abstract_response_spec.rb
-- spec/unit/exceptions_spec.rb
-- spec/unit/master_shake.jpg
-- spec/unit/payload_spec.rb
-- spec/unit/raw_response_spec.rb
-- spec/unit/request2_spec.rb
-- spec/unit/request_spec.rb
-- spec/unit/resource_spec.rb
-- spec/unit/response_spec.rb
-- spec/unit/restclient_spec.rb
-- spec/unit/windows/root_certs_spec.rb
-homepage: https://github.com/rest-client/rest-client
-licenses:
-- MIT
-metadata: {}
-post_install_message: 
-rdoc_options: []
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
-  requirements:
-  - - ">="
-    - !ruby/object:Gem::Version
-      version: 1.9.2
-required_rubygems_version: !ruby/object:Gem::Requirement
-  requirements:
-  - - ">="
-    - !ruby/object:Gem::Version
-      version: '0'
-requirements: []
-rubyforge_project: 
-rubygems_version: 2.2.2
-signing_key: 
-specification_version: 4
-summary: Simple HTTP and REST client for Ruby, inspired by microframework syntax for
-  specifying actions.
-test_files:
-- spec/integration/capath_digicert/244b5494.0
-- spec/integration/capath_digicert/81b9768f.0
-- spec/integration/capath_digicert/README
-- spec/integration/capath_digicert/digicert.crt
-- spec/integration/capath_verisign/415660c1.0
-- spec/integration/capath_verisign/7651b327.0
-- spec/integration/capath_verisign/README
-- spec/integration/capath_verisign/verisign.crt
-- spec/integration/certs/digicert.crt
-- spec/integration/certs/verisign.crt
-- spec/integration/integration_spec.rb
-- spec/integration/request_spec.rb
-- spec/spec_helper.rb
-- spec/unit/abstract_response_spec.rb
-- spec/unit/exceptions_spec.rb
-- spec/unit/master_shake.jpg
-- spec/unit/payload_spec.rb
-- spec/unit/raw_response_spec.rb
-- spec/unit/request2_spec.rb
-- spec/unit/request_spec.rb
-- spec/unit/resource_spec.rb
-- spec/unit/response_spec.rb
-- spec/unit/restclient_spec.rb
-- spec/unit/windows/root_certs_spec.rb
-has_rdoc: 
diff --git a/rest-client.gemspec b/rest-client.gemspec
index 967457c..e780a12 100644
--- a/rest-client.gemspec
+++ b/rest-client.gemspec
@@ -10,21 +10,22 @@ Gem::Specification.new do |s|
   s.license = 'MIT'
   s.email = 'rest.client at librelist.com'
   s.executables = ['restclient']
-  s.extra_rdoc_files = ['README.rdoc', 'history.md']
+  s.extra_rdoc_files = ['README.md', 'history.md']
   s.files = `git ls-files -z`.split("\0")
   s.test_files = `git ls-files -z spec/`.split("\0")
   s.homepage = 'https://github.com/rest-client/rest-client'
   s.summary = 'Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.'
 
-  s.add_development_dependency('webmock', '~> 1.4')
-  s.add_development_dependency('rspec', '~> 2.4')
-  s.add_development_dependency('pry')
-  s.add_development_dependency('pry-doc')
-  s.add_development_dependency('rdoc', '>= 2.4.2', '< 5.0')
+  s.add_development_dependency('webmock', '~> 2.0')
+  s.add_development_dependency('rspec', '~> 3.0')
+  s.add_development_dependency('pry', '~> 0')
+  s.add_development_dependency('pry-doc', '~> 0')
+  s.add_development_dependency('rdoc', '>= 2.4.2', '< 6.0')
+  s.add_development_dependency('rubocop', '~> 0')
 
   s.add_dependency('http-cookie', '>= 1.0.2', '< 2.0')
-  s.add_dependency('mime-types', '>= 1.16', '< 3.0')
-  s.add_dependency('netrc', '~> 0.7')
+  s.add_dependency('mime-types', '>= 1.16', '< 4.0')
+  s.add_dependency('netrc', '~> 0.8')
 
-  s.required_ruby_version = '>= 1.9.2'
+  s.required_ruby_version = '>= 2.0.0'
 end
diff --git a/spec/helpers.rb b/spec/helpers.rb
new file mode 100644
index 0000000..1de717d
--- /dev/null
+++ b/spec/helpers.rb
@@ -0,0 +1,22 @@
+require 'uri'
+
+module Helpers
+  def response_double(opts={})
+    double('response', {:to_hash => {}}.merge(opts))
+  end
+
+  def fake_stderr
+    original_stderr = $stderr
+    $stderr = StringIO.new
+    yield
+    $stderr.string
+  ensure
+    $stderr = original_stderr
+  end
+
+  def request_double(url: 'http://example.com', method: 'get')
+    double('request', url: url, uri: URI.parse(url), method: method,
+           user: nil, password: nil, cookie_jar: HTTP::CookieJar.new,
+           redirection_history: nil, args: {url: url, method: method})
+  end
+end
diff --git a/spec/integration/_lib.rb b/spec/integration/_lib.rb
new file mode 100644
index 0000000..935238d
--- /dev/null
+++ b/spec/integration/_lib.rb
@@ -0,0 +1 @@
+require_relative '../spec_helper'
diff --git a/spec/integration/httpbin_spec.rb b/spec/integration/httpbin_spec.rb
new file mode 100644
index 0000000..8c83b37
--- /dev/null
+++ b/spec/integration/httpbin_spec.rb
@@ -0,0 +1,87 @@
+require_relative '_lib'
+require 'json'
+
+describe RestClient::Request do
+  before(:all) do
+    WebMock.disable!
+  end
+
+  after(:all) do
+    WebMock.enable!
+  end
+
+  def default_httpbin_url
+    # add a hack to work around java/jruby bug
+    # java.lang.RuntimeException: Could not generate DH keypair with backtrace
+    # Also (2017-04-09) Travis Jruby versions have a broken CA keystore
+    if ENV['TRAVIS_RUBY_VERSION'] =~ /\Ajruby-/
+      'http://httpbin.org/'
+    else
+      'https://httpbin.org/'
+    end
+  end
+
+  def httpbin(suffix='')
+    url = ENV.fetch('HTTPBIN_URL', default_httpbin_url)
+    unless url.end_with?('/')
+      url += '/'
+    end
+
+    url + suffix
+  end
+
+  def execute_httpbin(suffix, opts={})
+    opts = {url: httpbin(suffix)}.merge(opts)
+    RestClient::Request.execute(opts)
+  end
+
+  def execute_httpbin_json(suffix, opts={})
+    JSON.parse(execute_httpbin(suffix, opts))
+  end
+
+  describe '.execute' do
+    it 'sends a user agent' do
+      data = execute_httpbin_json('user-agent', method: :get)
+      expect(data['user-agent']).to match(/rest-client/)
+    end
+
+    it 'receives cookies on 302' do
+      expect {
+        execute_httpbin('cookies/set?foo=bar', method: :get, max_redirects: 0)
+      }.to raise_error(RestClient::Found) { |ex|
+        expect(ex.http_code).to eq 302
+        expect(ex.response.cookies['foo']).to eq 'bar'
+      }
+    end
+
+    it 'passes along cookies through 302' do
+      data = execute_httpbin_json('cookies/set?foo=bar', method: :get)
+      expect(data).to have_key('cookies')
+      expect(data['cookies']['foo']).to eq 'bar'
+    end
+
+    it 'handles quote wrapped cookies' do
+      expect {
+        execute_httpbin('cookies/set?foo=' + CGI.escape('"bar:baz"'),
+                        method: :get, max_redirects: 0)
+      }.to raise_error(RestClient::Found) { |ex|
+        expect(ex.http_code).to eq 302
+        expect(ex.response.cookies['foo']).to eq '"bar:baz"'
+      }
+    end
+
+    it 'sends basic auth' do
+      user = 'user'
+      pass = 'pass'
+
+      data = execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: pass)
+      expect(data).to eq({'authenticated' => true, 'user' => user})
+
+      expect {
+        execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: 'badpass')
+      }.to raise_error(RestClient::Unauthorized) { |ex|
+        expect(ex.http_code).to eq 401
+      }
+    end
+  end
+end
diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb
index a0f32ce..963bad0 100644
--- a/spec/integration/integration_spec.rb
+++ b/spec/integration/integration_spec.rb
@@ -1,4 +1,6 @@
-require 'spec_helper'
+# -*- coding: utf-8 -*-
+require_relative '_lib'
+require 'base64'
 
 describe RestClient do
 
@@ -6,15 +8,15 @@ describe RestClient do
     body = 'abc'
     stub_request(:get, "www.example.com").to_return(:body => body, :status => 200)
     response = RestClient.get "www.example.com"
-    response.code.should eq 200
-    response.body.should eq body
+    expect(response.code).to eq 200
+    expect(response.body).to eq body
   end
 
   it "a simple request with gzipped content" do
     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' } )
     response = RestClient.get "www.example.com"
-    response.code.should eq 200
-    response.body.should eq "i'm gziped\n"
+    expect(response.code).to eq 200
+    expect(response.body).to eq "i'm gziped\n"
   end
 
   it "a 404" do
@@ -24,12 +26,100 @@ describe RestClient do
       RestClient.get "www.example.com"
       raise
     rescue RestClient::ResourceNotFound => e
-      e.http_code.should eq 404
-      e.response.code.should eq 404
-      e.response.body.should eq body
-      e.http_body.should eq body
+      expect(e.http_code).to eq 404
+      expect(e.response.code).to eq 404
+      expect(e.response.body).to eq body
+      expect(e.http_body).to eq body
     end
   end
 
+  describe 'charset parsing' do
+    it 'handles utf-8' do
+      body = "λ".force_encoding('ASCII-8BIT')
+      stub_request(:get, "www.example.com").to_return(
+        :body => body, :status => 200, :headers => {
+          'Content-Type' => 'text/plain; charset=UTF-8'
+      })
+      response = RestClient.get "www.example.com"
+      expect(response.encoding).to eq Encoding::UTF_8
+      expect(response.valid_encoding?).to eq true
+    end
+
+    it 'handles windows-1252' do
+      body = "\xff".force_encoding('ASCII-8BIT')
+      stub_request(:get, "www.example.com").to_return(
+        :body => body, :status => 200, :headers => {
+          'Content-Type' => 'text/plain; charset=windows-1252'
+      })
+      response = RestClient.get "www.example.com"
+      expect(response.encoding).to eq Encoding::WINDOWS_1252
+      expect(response.encode('utf-8')).to eq "ÿ"
+      expect(response.valid_encoding?).to eq true
+    end
+
+    it 'handles binary' do
+      body = "\xfe".force_encoding('ASCII-8BIT')
+      stub_request(:get, "www.example.com").to_return(
+        :body => body, :status => 200, :headers => {
+          'Content-Type' => 'application/octet-stream; charset=binary'
+      })
+      response = RestClient.get "www.example.com"
+      expect(response.encoding).to eq Encoding::BINARY
+      expect {
+        response.encode('utf-8')
+      }.to raise_error(Encoding::UndefinedConversionError)
+      expect(response.valid_encoding?).to eq true
+    end
+
+    it 'handles euc-jp' do
+      body = "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA".
+        force_encoding(Encoding::BINARY)
+      body_utf8 = 'あいうえお'
+      expect(body_utf8.encoding).to eq Encoding::UTF_8
+
+      stub_request(:get, 'www.example.com').to_return(
+        :body => body, :status => 200, :headers => {
+          'Content-Type' => 'text/plain; charset=EUC-JP'
+      })
+      response = RestClient.get 'www.example.com'
+      expect(response.encoding).to eq Encoding::EUC_JP
+      expect(response.valid_encoding?).to eq true
+      expect(response.length).to eq 5
+      expect(response.encode('utf-8')).to eq body_utf8
+    end
+
+    it 'defaults to the default encoding' do
+      stub_request(:get, 'www.example.com').to_return(
+        body: 'abc', status: 200, headers: {
+          'Content-Type' => 'text/plain'
+        })
 
+      response = RestClient.get 'www.example.com'
+      # expect(response.encoding).to eq Encoding.default_external
+      expect(response.encoding).to eq Encoding::UTF_8
+    end
+
+    it 'handles invalid encoding' do
+      stub_request(:get, 'www.example.com').to_return(
+        body: 'abc', status: 200, headers: {
+          'Content-Type' => 'text; charset=plain'
+        })
+
+      response = RestClient.get 'www.example.com'
+      # expect(response.encoding).to eq Encoding.default_external
+      expect(response.encoding).to eq Encoding::UTF_8
+    end
+
+    it 'leaves images as binary' do
+      gif = Base64.strict_decode64('R0lGODlhAQABAAAAADs=')
+
+      stub_request(:get, 'www.example.com').to_return(
+        body: gif, status: 200, headers: {
+          'Content-Type' => 'image/gif'
+        })
+
+      response = RestClient.get 'www.example.com'
+      expect(response.encoding).to eq Encoding::BINARY
+    end
+  end
 end
diff --git a/spec/integration/request_spec.rb b/spec/integration/request_spec.rb
index 5b0011d..99bfefa 100644
--- a/spec/integration/request_spec.rb
+++ b/spec/integration/request_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::Request do
   before(:all) do
@@ -75,7 +75,7 @@ describe RestClient::Request do
         },
       )
       expect {request.execute }.to_not raise_error
-      ran_callback.should eq(true)
+      expect(ran_callback).to eq(true)
     end
 
     it "fails verification when the callback returns false",
@@ -101,4 +101,27 @@ describe RestClient::Request do
       expect { request.execute }.to_not raise_error
     end
   end
+
+  describe "timeouts" do
+    it "raises OpenTimeout when it hits an open timeout" do
+      request = RestClient::Request.new(
+        :method => :get,
+        :url => 'http://www.mozilla.org',
+        :open_timeout => 1e-10,
+      )
+      expect { request.execute }.to(
+        raise_error(RestClient::Exceptions::OpenTimeout))
+    end
+
+    it "raises ReadTimeout when it hits a read timeout via :read_timeout" do
+      request = RestClient::Request.new(
+        :method => :get,
+        :url => 'https://www.mozilla.org',
+        :read_timeout => 1e-10,
+      )
+      expect { request.execute }.to(
+        raise_error(RestClient::Exceptions::ReadTimeout))
+    end
+  end
+
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 60cf27e..bca8e28 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,2 +1,29 @@
 require 'webmock/rspec'
-require 'restclient'
+require 'rest-client'
+
+require_relative './helpers'
+
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+  config.raise_errors_for_deprecations!
+
+  # Run specs in random order to surface order dependencies. If you find an
+  # order dependency and want to debug it, you can fix the order by providing
+  # the seed, which is printed after each run.
+  #     --seed 1234
+  config.order = 'random'
+
+  # always run with ruby warnings enabled
+  # TODO: figure out why this is so obscenely noisy (rspec bug?)
+  # config.warnings = true
+
+  # add helpers
+  config.include Helpers, :include_helpers
+
+  config.mock_with :rspec do |mocks|
+    mocks.yield_receiver_to_any_instance_implementation_blocks = true
+  end
+end
+
+# always run with ruby warnings enabled (see above)
+$VERBOSE = true
diff --git a/spec/unit/_lib.rb b/spec/unit/_lib.rb
new file mode 100644
index 0000000..935238d
--- /dev/null
+++ b/spec/unit/_lib.rb
@@ -0,0 +1 @@
+require_relative '../spec_helper'
diff --git a/spec/unit/abstract_response_spec.rb b/spec/unit/abstract_response_spec.rb
index 7e259a1..0407165 100644
--- a/spec/unit/abstract_response_spec.rb
+++ b/spec/unit/abstract_response_spec.rb
@@ -1,6 +1,6 @@
-require 'spec_helper'
+require_relative '_lib'
 
-describe RestClient::AbstractResponse do
+describe RestClient::AbstractResponse, :include_helpers do
 
   class MyAbstractResponse
 
@@ -8,9 +8,8 @@ describe RestClient::AbstractResponse do
 
     attr_accessor :size
 
-    def initialize net_http_res, args, request
+    def initialize net_http_res, request
       @net_http_res = net_http_res
-      @args = args
       @request = request
     end
 
@@ -18,71 +17,129 @@ describe RestClient::AbstractResponse do
 
   before do
     @net_http_res = double('net http response')
-    @request = double('restclient request', :url => 'http://example.com')
-    @response = MyAbstractResponse.new(@net_http_res, {}, @request)
+    @request = request_double(url: 'http://example.com', method: 'get')
+    @response = MyAbstractResponse.new(@net_http_res, @request)
   end
 
   it "fetches the numeric response code" do
-    @net_http_res.should_receive(:code).and_return('200')
-    @response.code.should eq 200
+    expect(@net_http_res).to receive(:code).and_return('200')
+    expect(@response.code).to eq 200
   end
 
   it "has a nice description" do
-    @net_http_res.should_receive(:to_hash).and_return({'Content-Type' => ['application/pdf']})
-    @net_http_res.should_receive(:code).and_return('200')
-    @response.description.should eq "200 OK | application/pdf  bytes\n"
+    expect(@net_http_res).to receive(:to_hash).and_return({'Content-Type' => ['application/pdf']})
+    expect(@net_http_res).to receive(:code).and_return('200')
+    expect(@response.description).to eq "200 OK | application/pdf  bytes\n"
   end
 
-  it "beautifies the headers by turning the keys to symbols" do
-    h = RestClient::AbstractResponse.beautify_headers('content-type' => [ 'x' ])
-    h.keys.first.should eq :content_type
-  end
+  describe '.beautify_headers' do
+    it "beautifies the headers by turning the keys to symbols" do
+      h = RestClient::AbstractResponse.beautify_headers('content-type' => [ 'x' ])
+      expect(h.keys.first).to eq :content_type
+    end
 
-  it "beautifies the headers by turning the values to strings instead of one-element arrays" do
-    h = RestClient::AbstractResponse.beautify_headers('x' => [ 'text/html' ] )
-    h.values.first.should eq 'text/html'
+    it "beautifies the headers by turning the values to strings instead of one-element arrays" do
+      h = RestClient::AbstractResponse.beautify_headers('x' => [ 'text/html' ] )
+      expect(h.values.first).to eq 'text/html'
+    end
+
+    it 'joins multiple header values by comma' do
+      expect(RestClient::AbstractResponse.beautify_headers(
+        {'My-Header' => ['one', 'two']}
+      )).to eq({:my_header => 'one, two'})
+    end
+
+    it 'leaves set-cookie headers as array' do
+      expect(RestClient::AbstractResponse.beautify_headers(
+        {'Set-Cookie' => ['cookie1=foo', 'cookie2=bar']}
+      )).to eq({:set_cookie => ['cookie1=foo', 'cookie2=bar']})
+    end
   end
 
   it "fetches the headers" do
-    @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
-    @response.headers.should eq({ :content_type => 'text/html' })
+    expect(@net_http_res).to receive(:to_hash).and_return('content-type' => [ 'text/html' ])
+    expect(@response.headers).to eq({ :content_type => 'text/html' })
   end
 
   it "extracts cookies from response headers" do
-    @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
-    @response.cookies.should eq({ 'session_id' => '1' })
+    expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+    expect(@response.cookies).to eq({ 'session_id' => '1' })
   end
 
   it "extract strange cookies" do
-    @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/'])
-    @response.headers.should eq({:set_cookie => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']})
-    @response.cookies.should eq({ 'session_id' => 'ZJ/HQVH6YE+rVkTpn0zvTQ==' })
+    expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/'])
+    expect(@response.headers).to eq({:set_cookie => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']})
+    expect(@response.cookies).to eq({ 'session_id' => 'ZJ/HQVH6YE+rVkTpn0zvTQ==' })
   end
 
   it "doesn't escape cookies" do
-    @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca; path=/'])
-    @response.cookies.should eq({ 'session_id' => 'BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca' })
+    expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca; path=/'])
+    expect(@response.cookies).to eq({ 'session_id' => 'BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca' })
+  end
+
+  describe '.cookie_jar' do
+    it 'extracts cookies into cookie jar' do
+      expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+      expect(@response.cookie_jar).to be_a HTTP::CookieJar
+
+      cookie = @response.cookie_jar.cookies.first
+      expect(cookie.domain).to eq 'example.com'
+      expect(cookie.name).to eq 'session_id'
+      expect(cookie.value).to eq '1'
+      expect(cookie.path).to eq '/'
+    end
+
+    it 'handles cookies when URI scheme is implicit' do
+      net_http_res = double('net http response')
+      expect(net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+      request = double(url: 'example.com', uri: URI.parse('http://example.com'),
+                       method: 'get', cookie_jar: HTTP::CookieJar.new)
+      response = MyAbstractResponse.new(net_http_res, request)
+      expect(response.cookie_jar).to be_a HTTP::CookieJar
+
+      cookie = response.cookie_jar.cookies.first
+      expect(cookie.domain).to eq 'example.com'
+      expect(cookie.name).to eq 'session_id'
+      expect(cookie.value).to eq '1'
+      expect(cookie.path).to eq '/'
+    end
   end
 
   it "can access the net http result directly" do
-    @response.net_http_res.should eq @net_http_res
+    expect(@response.net_http_res).to eq @net_http_res
   end
 
   describe "#return!" do
     it "should return the response itself on 200-codes" do
-      @net_http_res.should_receive(:code).and_return('200')
-      @response.return!.should be_equal(@response)
+      expect(@net_http_res).to receive(:code).and_return('200')
+      expect(@response.return!).to be_equal(@response)
     end
 
     it "should raise RequestFailed on unknown codes" do
-      @net_http_res.should_receive(:code).and_return('1000')
-      lambda { @response.return! }.should raise_error RestClient::RequestFailed
+      expect(@net_http_res).to receive(:code).and_return('1000')
+      expect { @response.return! }.to raise_error RestClient::RequestFailed
     end
 
     it "should raise an error on a redirection after non-GET/HEAD requests" do
-      @net_http_res.should_receive(:code).and_return('301')
-      @response.args.merge(:method => :put)
-      lambda { @response.return! }.should raise_error RestClient::RequestFailed
+      expect(@net_http_res).to receive(:code).and_return('301')
+      expect(@request).to receive(:method).and_return('put')
+      expect(@response).not_to receive(:follow_redirection)
+      expect { @response.return! }.to raise_error RestClient::RequestFailed
+    end
+
+    it "should follow 302 redirect" do
+      expect(@net_http_res).to receive(:code).and_return('302')
+      expect(@response).to receive(:check_max_redirects).and_return('fake-check')
+      expect(@response).to receive(:follow_redirection).and_return('fake-redirection')
+      expect(@response.return!).to eq 'fake-redirection'
+    end
+
+    it "should gracefully handle 302 redirect with no location header" do
+      @net_http_res = response_double(code: 302, location: nil)
+      @request = request_double()
+      @response = MyAbstractResponse.new(@net_http_res, @request)
+      expect(@response).to receive(:check_max_redirects).and_return('fake-check')
+      expect { @response.return! }.to raise_error RestClient::Found
     end
   end
 end
diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb
index 5d86373..8675584 100644
--- a/spec/unit/exceptions_spec.rb
+++ b/spec/unit/exceptions_spec.rb
@@ -1,32 +1,32 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::Exception do
   it "returns a 'message' equal to the class name if the message is not set, because 'message' should not be nil" do
     e = RestClient::Exception.new
-    e.message.should eq "RestClient::Exception"
+    expect(e.message).to eq "RestClient::Exception"
   end
 
   it "returns the 'message' that was set" do
     e = RestClient::Exception.new
     message = "An explicitly set message"
     e.message = message
-    e.message.should eq message
+    expect(e.message).to eq message
   end
 
   it "sets the exception message to ErrorMessage" do
-    RestClient::ResourceNotFound.new.message.should eq 'Resource Not Found'
+    expect(RestClient::ResourceNotFound.new.message).to eq 'Not Found'
   end
 
   it "contains exceptions in RestClient" do
-    RestClient::Unauthorized.new.should be_a_kind_of(RestClient::Exception)
-    RestClient::ServerBrokeConnection.new.should be_a_kind_of(RestClient::Exception)
+    expect(RestClient::Unauthorized.new).to be_a_kind_of(RestClient::Exception)
+    expect(RestClient::ServerBrokeConnection.new).to be_a_kind_of(RestClient::Exception)
   end
 end
 
 describe RestClient::ServerBrokeConnection do
   it "should have a default message of 'Server broke connection'" do
     e = RestClient::ServerBrokeConnection.new
-    e.message.should eq 'Server broke connection'
+    expect(e.message).to eq 'Server broke connection'
   end
 end
 
@@ -40,21 +40,21 @@ describe RestClient::RequestFailed do
     begin
       raise RestClient::RequestFailed, response
     rescue RestClient::RequestFailed => e
-      e.response.should eq response
+      expect(e.response).to eq response
     end
   end
 
   it "http_code convenience method for fetching the code as an integer" do
-    RestClient::RequestFailed.new(@response).http_code.should eq 502
+    expect(RestClient::RequestFailed.new(@response).http_code).to eq 502
   end
 
   it "http_body convenience method for fetching the body (decoding when necessary)" do
-    RestClient::RequestFailed.new(@response).http_code.should eq 502
-    RestClient::RequestFailed.new(@response).message.should eq 'HTTP status code 502'
+    expect(RestClient::RequestFailed.new(@response).http_code).to eq 502
+    expect(RestClient::RequestFailed.new(@response).message).to eq 'HTTP status code 502'
   end
 
   it "shows the status code in the message" do
-    RestClient::RequestFailed.new(@response).to_s.should match(/502/)
+    expect(RestClient::RequestFailed.new(@response).to_s).to match(/502/)
   end
 end
 
@@ -64,32 +64,45 @@ describe RestClient::ResourceNotFound do
     begin
       raise RestClient::ResourceNotFound, response
     rescue RestClient::ResourceNotFound => e
-      e.response.should eq response
+      expect(e.response).to eq response
+    end
+  end
+
+  it 'stores the body on the response of the exception' do
+    body = "body"
+    stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
+    begin
+      RestClient.get "www.example.com"
+      raise
+    rescue RestClient::ResourceNotFound => e
+      expect(e.response.body).to eq body
     end
   end
 end
 
 describe "backwards compatibility" do
-  it "alias RestClient::Request::Redirect to RestClient::Redirect" do
-    RestClient::Request::Redirect.should eq RestClient::Redirect
+  it 'aliases RestClient::NotFound as ResourceNotFound' do
+    expect(RestClient::ResourceNotFound).to eq RestClient::NotFound
   end
 
-  it "alias RestClient::Request::Unauthorized to RestClient::Unauthorized" do
-    RestClient::Request::Unauthorized.should eq RestClient::Unauthorized
+  it 'aliases old names for HTTP 413, 414, 416' do
+    expect(RestClient::RequestEntityTooLarge).to eq RestClient::PayloadTooLarge
+    expect(RestClient::RequestURITooLong).to eq RestClient::URITooLong
+    expect(RestClient::RequestedRangeNotSatisfiable).to eq RestClient::RangeNotSatisfiable
   end
 
-  it "alias RestClient::Request::RequestFailed to RestClient::RequestFailed" do
-    RestClient::Request::RequestFailed.should eq RestClient::RequestFailed
+  it 'subclasses NotFound from RequestFailed, ExceptionWithResponse' do
+    expect(RestClient::NotFound).to be < RestClient::RequestFailed
+    expect(RestClient::NotFound).to be < RestClient::ExceptionWithResponse
   end
 
-  it "make the exception's response act like an Net::HTTPResponse" do
-    body = "body"
-    stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
-    begin
-      RestClient.get "www.example.com"
-      raise
-    rescue RestClient::ResourceNotFound => e
-      e.response.body.should eq body
-    end
+  it 'subclasses timeout from RestClient::RequestTimeout, RequestFailed, EWR' do
+    expect(RestClient::Exceptions::OpenTimeout).to be < RestClient::Exceptions::Timeout
+    expect(RestClient::Exceptions::ReadTimeout).to be < RestClient::Exceptions::Timeout
+
+    expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestTimeout
+    expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestFailed
+    expect(RestClient::Exceptions::Timeout).to be < RestClient::ExceptionWithResponse
   end
+
 end
diff --git a/spec/unit/params_array_spec.rb b/spec/unit/params_array_spec.rb
new file mode 100644
index 0000000..926f088
--- /dev/null
+++ b/spec/unit/params_array_spec.rb
@@ -0,0 +1,36 @@
+require_relative '_lib'
+
+describe RestClient::ParamsArray do
+
+  describe '.new' do
+    it 'accepts various types of containers' do
+      as_array = [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]]
+      [
+        [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]],
+        [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}],
+        [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}],
+        [{foo: 123}, [:foo, 456], {bar: 789}, {empty: nil}],
+        [{foo: 123}, [:foo, 456], {bar: 789}, [:empty]],
+      ].each do |input|
+        expect(RestClient::ParamsArray.new(input).to_a).to eq as_array
+      end
+
+      expect(RestClient::ParamsArray.new([]).to_a).to eq []
+      expect(RestClient::ParamsArray.new([]).empty?).to eq true
+    end
+
+    it 'rejects various invalid input' do
+      expect {
+        RestClient::ParamsArray.new([[]])
+      }.to raise_error(IndexError)
+
+      expect {
+        RestClient::ParamsArray.new([[1,2,3]])
+      }.to raise_error(ArgumentError)
+
+      expect {
+        RestClient::ParamsArray.new([1,2,3])
+      }.to raise_error(NoMethodError)
+    end
+  end
+end
diff --git a/spec/unit/payload_spec.rb b/spec/unit/payload_spec.rb
index 5fc7457..d34634a 100644
--- a/spec/unit/payload_spec.rb
+++ b/spec/unit/payload_spec.rb
@@ -1,60 +1,68 @@
 # encoding: binary
 
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::Payload do
+  context "Base Payload" do
+    it "should reset stream after to_s" do
+      payload = RestClient::Payload::Base.new('foobar')
+      expect(payload.to_s).to eq 'foobar'
+      expect(payload.to_s).to eq 'foobar'
+    end
+  end
+
   context "A regular Payload" do
     it "should use standard enctype as default content-type" do
-      RestClient::Payload::UrlEncoded.new({}).headers['Content-Type'].
-          should eq 'application/x-www-form-urlencoded'
+      expect(RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']).
+        to eq 'application/x-www-form-urlencoded'
     end
 
     it "should form properly encoded params" do
-      RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s.
-          should eq "foo=bar"
-      ["foo=bar&baz=qux", "baz=qux&foo=bar"].should include(
+      expect(RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s).
+        to eq "foo=bar"
+      expect(["foo=bar&baz=qux", "baz=qux&foo=bar"]).to include(
                                                         RestClient::Payload::UrlEncoded.new({:foo => 'bar', :baz => 'qux'}).to_s)
     end
 
     it "should escape parameters" do
-      RestClient::Payload::UrlEncoded.new({'foo ' => 'bar'}).to_s.
-          should eq "foo%20=bar"
+      expect(RestClient::Payload::UrlEncoded.new({'foo + bar' => 'baz'}).to_s).
+        to eq "foo+%2B+bar=baz"
     end
 
     it "should properly handle hashes as parameter" do
-      RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz'}}).to_s.
-          should eq "foo[bar]=baz"
-      RestClient::Payload::UrlEncoded.new({:foo => {:bar => {:baz => 'qux'}}}).to_s.
-          should eq "foo[bar][baz]=qux"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz'}}).to_s).
+        to eq "foo[bar]=baz"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => {:baz => 'qux'}}}).to_s).
+        to eq "foo[bar][baz]=qux"
     end
 
     it "should handle many attributes inside a hash" do
       parameters = RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz', :baz => 'qux'}}).to_s
-      parameters.should include("foo[bar]=baz", "foo[baz]=qux")
+      expect(parameters).to eq 'foo[bar]=baz&foo[baz]=qux'
     end
 
-    it "should handle attributes inside a an array inside an hash" do
+    it "should handle attributes inside an array inside an hash" do
       parameters = RestClient::Payload::UrlEncoded.new({"foo" => [{"bar" => 'baz'}, {"bar" => 'qux'}]}).to_s
-      parameters.should include("foo[bar]=baz", "foo[bar]=qux")
+      expect(parameters).to eq 'foo[][bar]=baz&foo[][bar]=qux'
     end
 
-    it "should handle attributes inside a an array inside an array inside an hash" do
-      parameters = RestClient::Payload::UrlEncoded.new({"foo" => [[{"bar" => 'baz'}, {"bar" => 'qux'}]]}).to_s
-      parameters.should include("foo[bar]=baz", "foo[bar]=qux")
+    it "should handle arrays inside a hash inside a hash" do
+      parameters = RestClient::Payload::UrlEncoded.new({"foo" => {'even' => [0, 2], 'odd' => [1, 3]}}).to_s
+      expect(parameters).to eq 'foo[even][]=0&foo[even][]=2&foo[odd][]=1&foo[odd][]=3'
     end
 
     it "should form properly use symbols as parameters" do
-      RestClient::Payload::UrlEncoded.new({:foo => :bar}).to_s.
-          should eq "foo=bar"
-      RestClient::Payload::UrlEncoded.new({:foo => {:bar => :baz}}).to_s.
-          should eq "foo[bar]=baz"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => :bar}).to_s).
+        to eq "foo=bar"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => :baz}}).to_s).
+        to eq "foo[bar]=baz"
     end
 
     it "should properly handle arrays as repeated parameters" do
-      RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s.
-          should eq "foo[]=bar"
-      RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s.
-          should eq "foo[]=bar&foo[]=baz"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s).
+        to eq "foo[]=bar"
+      expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s).
+        to eq "foo[]=bar&foo[]=baz"
     end
 
     it 'should not close if stream already closed' do
@@ -67,8 +75,8 @@ describe RestClient::Payload do
   context "A multipart Payload" do
     it "should use standard enctype as default content-type" do
       m = RestClient::Payload::Multipart.new({})
-      m.stub(:boundary).and_return(123)
-      m.headers['Content-Type'].should eq 'multipart/form-data; boundary=123'
+      allow(m).to receive(:boundary).and_return(123)
+      expect(m.headers['Content-Type']).to eq 'multipart/form-data; boundary=123'
     end
 
     it 'should not error on close if stream already closed' do
@@ -78,7 +86,7 @@ describe RestClient::Payload do
 
     it "should form properly separated multipart data" do
       m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]])
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="bar"\r
 \r
@@ -93,7 +101,7 @@ bar\r
 
     it "should not escape parameters names" do
       m = RestClient::Payload::Multipart.new([["bar ", "baz"]])
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="bar "\r
 \r
@@ -105,7 +113,7 @@ baz\r
     it "should form properly separated multipart data" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
       m = RestClient::Payload::Multipart.new({:foo => f})
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r
 Content-Type: image/jpeg\r
@@ -118,7 +126,7 @@ Content-Type: image/jpeg\r
     it "should ignore the name attribute when it's not set" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
       m = RestClient::Payload::Multipart.new({nil => f})
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; filename="master_shake.jpg"\r
 Content-Type: image/jpeg\r
@@ -133,7 +141,7 @@ Content-Type: image/jpeg\r
       f.instance_eval "def content_type; 'text/plain'; end"
       f.instance_eval "def original_filename; 'foo.txt'; end"
       m = RestClient::Payload::Multipart.new({:foo => f})
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="foo"; filename="foo.txt"\r
 Content-Type: text/plain\r
@@ -145,7 +153,7 @@ Content-Type: text/plain\r
 
     it "should handle hash in hash parameters" do
       m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}})
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="bar[baz]"\r
 \r
@@ -157,7 +165,7 @@ foo\r
       f.instance_eval "def content_type; 'text/plain'; end"
       f.instance_eval "def original_filename; 'foo.txt'; end"
       m = RestClient::Payload::Multipart.new({:foo => {:bar => f}})
-      m.to_s.should eq <<-EOS
+      expect(m.to_s).to eq <<-EOS
 --#{m.boundary}\r
 Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r
 Content-Type: text/plain\r
@@ -167,29 +175,36 @@ Content-Type: text/plain\r
       EOS
     end
 
+    it 'should correctly format hex boundary' do
+      allow(SecureRandom).to receive(:base64).with(12).and_return('TGs89+ttw/xna6TV')
+      f = File.new(File.dirname(__FILE__) + '/master_shake.jpg')
+      m = RestClient::Payload::Multipart.new({:foo => f})
+      expect(m.boundary).to eq('-' * 4 + 'RubyFormBoundary' + 'TGs89AttwBxna6TV')
+    end
+
   end
 
   context "streamed payloads" do
     it "should properly determine the size of file payloads" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
       payload = RestClient::Payload.generate(f)
-      payload.size.should eq 76_988
-      payload.length.should eq 76_988
+      expect(payload.size).to eq 76_988
+      expect(payload.length).to eq 76_988
     end
 
     it "should properly determine the size of other kinds of streaming payloads" do
       s = StringIO.new 'foo'
       payload = RestClient::Payload.generate(s)
-      payload.size.should eq 3
-      payload.length.should eq 3
+      expect(payload.size).to eq 3
+      expect(payload.length).to eq 3
 
       begin
         f = Tempfile.new "rest-client"
         f.write 'foo bar'
 
         payload = RestClient::Payload.generate(f)
-        payload.size.should eq 7
-        payload.length.should eq 7
+        expect(payload.size).to eq 7
+        expect(payload.length).to eq 7
       ensure
         f.close
       end
@@ -198,48 +213,51 @@ Content-Type: text/plain\r
 
   context "Payload generation" do
     it "should recognize standard urlencoded params" do
-      RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded)
+      expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded)
     end
 
     it "should recognize multipart params" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
-      RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart)
+      expect(RestClient::Payload.generate({"foo" => f})).to be_kind_of(RestClient::Payload::Multipart)
     end
 
     it "should be multipart if forced" do
-      RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart)
+      expect(RestClient::Payload.generate({"foo" => "bar", :multipart => true})).to be_kind_of(RestClient::Payload::Multipart)
     end
 
+    it "should handle deeply nested multipart" do
+      f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
+      params = {foo: RestClient::ParamsArray.new({nested: f})}
+      expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart)
+    end
+
+
     it "should return data if no of the above" do
-      RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base)
+      expect(RestClient::Payload.generate("data")).to be_kind_of(RestClient::Payload::Base)
     end
 
     it "should recognize nested multipart payloads in hashes" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
-      RestClient::Payload.generate({"foo" => {"file" => f}}).should be_kind_of(RestClient::Payload::Multipart)
+      expect(RestClient::Payload.generate({"foo" => {"file" => f}})).to be_kind_of(RestClient::Payload::Multipart)
     end
 
     it "should recognize nested multipart payloads in arrays" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
-      RestClient::Payload.generate({"foo" => [f]}).should be_kind_of(RestClient::Payload::Multipart)
+      expect(RestClient::Payload.generate({"foo" => [f]})).to be_kind_of(RestClient::Payload::Multipart)
     end
 
     it "should recognize file payloads that can be streamed" do
       f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
-      RestClient::Payload.generate(f).should be_kind_of(RestClient::Payload::Streamed)
+      expect(RestClient::Payload.generate(f)).to be_kind_of(RestClient::Payload::Streamed)
     end
 
     it "should recognize other payloads that can be streamed" do
-      RestClient::Payload.generate(StringIO.new('foo')).should be_kind_of(RestClient::Payload::Streamed)
+      expect(RestClient::Payload.generate(StringIO.new('foo'))).to be_kind_of(RestClient::Payload::Streamed)
     end
 
     # hashery gem introduces Hash#read convenience method. Existence of #read method used to determine of content is streameable :/
     it "shouldn't treat hashes as streameable" do
-      RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded)
+      expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded)
     end
   end
-
-  class HashMapForTesting < Hash
-    alias :read :[]
-  end
 end
diff --git a/spec/unit/raw_response_spec.rb b/spec/unit/raw_response_spec.rb
index 0bc5fa5..13f859d 100644
--- a/spec/unit/raw_response_spec.rb
+++ b/spec/unit/raw_response_spec.rb
@@ -1,18 +1,18 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::RawResponse do
   before do
     @tf = double("Tempfile", :read => "the answer is 42", :open => true)
     @net_http_res = double('net http response')
     @request = double('http request')
-    @response = RestClient::RawResponse.new(@tf, @net_http_res, {}, @request)
+    @response = RestClient::RawResponse.new(@tf, @net_http_res, @request)
   end
 
   it "behaves like string" do
-    @response.to_s.should eq 'the answer is 42'
+    expect(@response.to_s).to eq 'the answer is 42'
   end
 
   it "exposes a Tempfile" do
-    @response.file.should eq @tf
+    expect(@response.file).to eq @tf
   end
 end
diff --git a/spec/unit/request2_spec.rb b/spec/unit/request2_spec.rb
index 5088ee8..71e5d6e 100644
--- a/spec/unit/request2_spec.rb
+++ b/spec/unit/request2_spec.rb
@@ -1,13 +1,35 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::Request do
 
-  it "manage params for get requests" do
-    stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
-    RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}).body.should eq 'foo'
+  context 'params for GET requests' do
+    it "manage params for get requests" do
+      stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}).body).to eq 'foo'
+
+      stub_request(:get, 'http://some/resource').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => :a}).body).to eq 'foo'
+    end
+
+    it 'adds GET params when params are present in URL' do
+      stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+      expect(RestClient::Request.execute(:url => 'http://some/resource?a=b', :method => :get, :headers => {:foo => :bar, :params => {:c => 'd'}}).body).to eq 'foo'
+    end
+
+    it 'encodes nested GET params' do
+      stub_request(:get, 'http://some/resource?a[foo][]=1&a[foo][]=2&a[bar]&b=foo+bar&math=2+%2B+2+%3D%3D+4').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
+      expect(RestClient::Request.execute(url: 'http://some/resource', method: :get, headers: {
+        params: {
+          a: {
+            foo: [1,2],
+            bar: nil,
+          },
+          b: 'foo bar',
+          math: '2 + 2 == 4',
+        }
+      }).body).to eq 'foo'
+    end
 
-    stub_request(:get, 'http://some/resource').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar', 'params' => 'a'}).to_return(:body => 'foo', :status => 200)
-    RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => :a}).body.should eq 'foo'
   end
 
   it "can use a block to process response" do
@@ -15,18 +37,18 @@ describe RestClient::Request do
     block = proc do |http_response|
       response_value = http_response.body
     end
-    stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+    stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
     RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}, :block_response => block)
-    response_value.should eq "foo"
+    expect(response_value).to eq "foo"
   end
 
   it 'closes payload if not nil' do
     test_file = File.new(File.join( File.dirname(File.expand_path(__FILE__)), 'master_shake.jpg'))
 
-    stub_request(:post, 'http://some/resource').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
+    stub_request(:post, 'http://some/resource').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
     RestClient::Request.execute(:url => 'http://some/resource', :method => :post, :payload => {:file => test_file})
 
-    test_file.closed?.should be_true
+    expect(test_file.closed?).to be true
   end
 
 end
diff --git a/spec/unit/request_spec.rb b/spec/unit/request_spec.rb
index 7c00873..bdffd66 100644
--- a/spec/unit/request_spec.rb
+++ b/spec/unit/request_spec.rb
@@ -1,120 +1,230 @@
-require 'spec_helper'
+require_relative './_lib'
 
-describe RestClient::Request do
+describe RestClient::Request, :include_helpers do
   before do
     @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
 
     @uri = double("uri")
-    @uri.stub(:request_uri).and_return('/resource')
-    @uri.stub(:host).and_return('some')
-    @uri.stub(:port).and_return(80)
+    allow(@uri).to receive(:request_uri).and_return('/resource')
+    allow(@uri).to receive(:hostname).and_return('some')
+    allow(@uri).to receive(:port).and_return(80)
 
     @net = double("net::http base")
     @http = double("net::http connection")
-    Net::HTTP.stub(:new).and_return(@net)
-    @net.stub(:start).and_yield(@http)
-    @net.stub(:use_ssl=)
-    @net.stub(:verify_mode=)
-    @net.stub(:verify_callback=)
+
+    allow(Net::HTTP).to receive(:new).and_return(@net)
+
+    allow(@net).to receive(:start).and_yield(@http)
+    allow(@net).to receive(:use_ssl=)
+    allow(@net).to receive(:verify_mode=)
+    allow(@net).to receive(:verify_callback=)
     allow(@net).to receive(:ciphers=)
     allow(@net).to receive(:cert_store=)
     RestClient.log = nil
   end
 
-  it "accept */* mimetype, preferring xml" do
-    @request.default_headers[:accept].should eq '*/*; q=0.5, application/xml'
+  it "accept */* mimetype" do
+    expect(@request.default_headers[:accept]).to eq '*/*'
   end
 
   describe "compression" do
 
     it "decodes an uncompressed result body by passing it straight through" do
-      RestClient::Request.decode(nil, 'xyz').should eq 'xyz'
+      expect(RestClient::Request.decode(nil, 'xyz')).to eq 'xyz'
     end
 
     it "doesn't fail for nil bodies" do
-      RestClient::Request.decode('gzip', nil).should be_nil
+      expect(RestClient::Request.decode('gzip', nil)).to be_nil
     end
 
 
     it "decodes a gzip body" do
-      RestClient::Request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000").should eq "i'm gziped\n"
+      expect(RestClient::Request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000")).to eq "i'm gziped\n"
     end
 
     it "ingores gzip for empty bodies" do
-      RestClient::Request.decode('gzip', '').should be_empty
+      expect(RestClient::Request.decode('gzip', '')).to be_empty
     end
 
     it "decodes a deflated body" do
-      RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should eq "some deflated text"
+      expect(RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363")).to eq "some deflated text"
     end
   end
 
   it "processes a successful result" do
-    res = double("result")
-    res.stub(:code).and_return("200")
-    res.stub(:body).and_return('body')
-    res.stub(:[]).with('content-encoding').and_return(nil)
-    @request.process_result(res).body.should eq 'body'
-    @request.process_result(res).to_s.should eq 'body'
+    res = response_double
+    allow(res).to receive(:code).and_return("200")
+    allow(res).to receive(:body).and_return('body')
+    allow(res).to receive(:[]).with('content-encoding').and_return(nil)
+    expect(@request.send(:process_result, res).body).to eq 'body'
+    expect(@request.send(:process_result, res).to_s).to eq 'body'
   end
 
   it "doesn't classify successful requests as failed" do
     203.upto(207) do |code|
-      res = double("result")
-      res.stub(:code).and_return(code.to_s)
-      res.stub(:body).and_return("")
-      res.stub(:[]).with('content-encoding').and_return(nil)
-      @request.process_result(res).should be_empty
+      res = response_double
+      allow(res).to receive(:code).and_return(code.to_s)
+      allow(res).to receive(:body).and_return("")
+      allow(res).to receive(:[]).with('content-encoding').and_return(nil)
+      expect(@request.send(:process_result, res)).to be_empty
     end
   end
 
-  it "parses a url into a URI object" do
-    URI.should_receive(:parse).with('http://example.com/resource')
-    @request.parse_url('http://example.com/resource')
-  end
+  describe '.normalize_url' do
+    it "adds http:// to the front of resources specified in the syntax example.com/resource" do
+      expect(@request.normalize_url('example.com/resource')).to eq 'http://example.com/resource'
+    end
+
+    it 'adds http:// to resources containing a colon' do
+      expect(@request.normalize_url('example.com:1234')).to eq 'http://example.com:1234'
+    end
+
+    it 'does not add http:// to the front of https resources' do
+      expect(@request.normalize_url('https://example.com/resource')).to eq 'https://example.com/resource'
+    end
+
+    it 'does not add http:// to the front of capital HTTP resources' do
+      expect(@request.normalize_url('HTTP://example.com/resource')).to eq 'HTTP://example.com/resource'
+    end
+
+    it 'does not add http:// to the front of capital HTTPS resources' do
+      expect(@request.normalize_url('HTTPS://example.com/resource')).to eq 'HTTPS://example.com/resource'
+    end
 
-  it "adds http:// to the front of resources specified in the syntax example.com/resource" do
-    URI.should_receive(:parse).with('http://example.com/resource')
-    @request.parse_url('example.com/resource')
+    it 'raises with invalid URI' do
+      expect {
+        RestClient::Request.new(method: :get, url: 'http://a@b:c')
+      }.to raise_error(URI::InvalidURIError)
+      expect {
+        RestClient::Request.new(method: :get, url: 'http://::')
+      }.to raise_error(URI::InvalidURIError)
+    end
   end
 
   describe "user - password" do
     it "extracts the username and password when parsing http://user:password@example.com/" do
-      URI.stub(:parse).and_return(double('uri', :user => 'joe', :password => 'pass1'))
-      @request.parse_url_with_auth('http://joe:pass1@example.com/resource')
-      @request.user.should eq 'joe'
-      @request.password.should eq 'pass1'
+      @request.send(:parse_url_with_auth!, 'http://joe:pass1@example.com/resource')
+      expect(@request.user).to eq 'joe'
+      expect(@request.password).to eq 'pass1'
     end
 
     it "extracts with escaping the username and password when parsing http://user:password@example.com/" do
-      URI.stub(:parse).and_return(double('uri', :user => 'joe%20', :password => 'pass1'))
-      @request.parse_url_with_auth('http://joe%20:pass1@example.com/resource')
-      @request.user.should eq 'joe '
-      @request.password.should eq 'pass1'
+      @request.send(:parse_url_with_auth!, 'http://joe%20:pass1@example.com/resource')
+      expect(@request.user).to eq 'joe '
+      expect(@request.password).to eq 'pass1'
     end
 
     it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
-      URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil))
-      @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
-      @request.parse_url_with_auth('http://example.com/resource')
-      @request.user.should eq 'beth'
-      @request.password.should eq 'pass2'
+      request = RestClient::Request.new(method: :get, url: 'http://example.com/resource', user: 'beth', password: 'pass2')
+      expect(request.user).to eq 'beth'
+      expect(request.password).to eq 'pass2'
+    end
+
+    it 'uses the username and password from the URL' do
+      request = RestClient::Request.new(method: :get, url: 'http://person:secret@example.com/resource')
+      expect(request.user).to eq 'person'
+      expect(request.password).to eq 'secret'
+    end
+
+    it 'overrides URL user/pass with explicit options' do
+      request = RestClient::Request.new(method: :get, url: 'http://person:secret@example.com/resource', user: 'beth', password: 'pass2')
+      expect(request.user).to eq 'beth'
+      expect(request.password).to eq 'pass2'
     end
   end
 
   it "correctly formats cookies provided to the constructor" do
-    URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil))
-    @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1', :user_id => "someone" })
-    @request.should_receive(:default_headers).and_return({'Foo' => 'bar'})
-    @request.make_headers({}).should eq({ 'Foo' => 'bar', 'Cookie' => 'session_id=1; user_id=someone'})
+    cookies_arr = [
+      HTTP::Cookie.new('session_id', '1', domain: 'example.com', path: '/'),
+      HTTP::Cookie.new('user_id', 'someone', domain: 'example.com', path: '/'),
+    ]
+
+    jar = HTTP::CookieJar.new
+    cookies_arr.each {|c| jar << c }
+
+    # test Hash, HTTP::CookieJar, and Array<HTTP::Cookie> modes
+    [
+      {session_id: '1', user_id: 'someone'},
+      jar,
+      cookies_arr
+    ].each do |cookies|
+      [true, false].each do |in_headers|
+        if in_headers
+          opts = {headers: {cookies: cookies}}
+        else
+          opts = {cookies: cookies}
+        end
+
+        request = RestClient::Request.new(method: :get, url: 'example.com', **opts)
+        expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+        expect(request.make_headers({})).to eq({'Foo' => 'bar', 'Cookie' => 'session_id=1; user_id=someone'})
+        expect(request.make_cookie_header).to eq 'session_id=1; user_id=someone'
+        expect(request.cookies).to eq({'session_id' => '1', 'user_id' => 'someone'})
+        expect(request.cookie_jar.cookies.length).to eq 2
+        expect(request.cookie_jar.object_id).not_to eq jar.object_id # make sure we dup it
+      end
+    end
+
+    # test with no cookies
+    request = RestClient::Request.new(method: :get, url: 'example.com')
+    expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+    expect(request.make_headers({})).to eq({'Foo' => 'bar'})
+    expect(request.make_cookie_header).to be_nil
+    expect(request.cookies).to eq({})
+    expect(request.cookie_jar.cookies.length).to eq 0
+  end
+
+  it 'strips out cookies set for a different domain name' do
+    jar = HTTP::CookieJar.new
+    jar << HTTP::Cookie.new('session_id', '1', domain: 'other.example.com', path: '/')
+    jar << HTTP::Cookie.new('user_id', 'someone', domain: 'other.example.com', path: '/')
+
+    request = RestClient::Request.new(method: :get, url: 'www.example.com', cookies: jar)
+    expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+    expect(request.make_headers({})).to eq({'Foo' => 'bar'})
+    expect(request.make_cookie_header).to eq nil
+    expect(request.cookies).to eq({})
+    expect(request.cookie_jar.cookies.length).to eq 2
+  end
+
+  it 'assumes default domain and path for cookies set by hash' do
+    request = RestClient::Request.new(method: :get, url: 'www.example.com', cookies: {'session_id' => '1'})
+    expect(request.cookie_jar.cookies.length).to eq 1
+
+    cookie = request.cookie_jar.cookies.first
+    expect(cookie).to be_a(HTTP::Cookie)
+    expect(cookie.domain).to eq('www.example.com')
+    expect(cookie.for_domain?).to be_truthy
+    expect(cookie.path).to eq('/')
+  end
+
+  it 'rejects or warns with contradictory cookie options' do
+    # same opt in two different places
+    expect {
+      RestClient::Request.new(method: :get, url: 'example.com',
+                              cookies: {bar: '456'},
+                              headers: {cookies: {foo: '123'}})
+    }.to raise_error(ArgumentError, /Cannot pass :cookies in Request.*headers/)
+
+    # :cookies opt and Cookie header
+    [
+      {cookies: {foo: '123'}, headers: {cookie: 'foo'}},
+      {cookies: {foo: '123'}, headers: {'Cookie' => 'foo'}},
+      {headers: {cookies: {foo: '123'}, cookie: 'foo'}},
+      {headers: {cookies: {foo: '123'}, 'Cookie' => 'foo'}},
+    ].each do |opts|
+      expect(fake_stderr {
+        RestClient::Request.new(method: :get, url: 'example.com', **opts)
+      }).to match(/warning: overriding "Cookie" header with :cookies option/)
+    end
   end
 
   it "does not escape or unescape cookies" do
     cookie = 'Foo%20:Bar%0A~'
     @request = RestClient::Request.new(:method => 'get', :url => 'example.com',
                                        :cookies => {:test => cookie})
-    @request.should_receive(:default_headers).and_return({'Foo' => 'bar'})
-    @request.make_headers({}).should eq({
+    expect(@request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+    expect(@request.make_headers({})).to eq({
       'Foo' => 'bar',
       'Cookie' => "test=#{cookie}"
     })
@@ -124,244 +234,408 @@ describe RestClient::Request do
     # Cookie validity is something of a mess, but we should reject the worst of
     # the RFC 6265 (4.1.1) prohibited characters such as control characters.
 
-    ['', 'foo=bar', 'foo;bar', "foo\nbar"].each do |cookie_name|
-      lambda {
+    ['foo=bar', 'foo;bar', "foo\nbar"].each do |cookie_name|
+      expect {
         RestClient::Request.new(:method => 'get', :url => 'example.com',
                                 :cookies => {cookie_name => 'value'})
-      }.should raise_error(ArgumentError, /\AInvalid cookie name/)
+      }.to raise_error(ArgumentError, /\AInvalid cookie name/i)
     end
+
+    cookie_name = ''
+    expect {
+      RestClient::Request.new(:method => 'get', :url => 'example.com',
+                              :cookies => {cookie_name => 'value'})
+    }.to raise_error(ArgumentError, /cookie name cannot be empty/i)
   end
 
   it "rejects cookie values containing invalid characters" do
     # Cookie validity is something of a mess, but we should reject the worst of
     # the RFC 6265 (4.1.1) prohibited characters such as control characters.
 
-    ['foo,bar', 'foo;bar', "foo\nbar"].each do |cookie_value|
-      lambda {
+    ["foo\tbar", "foo\nbar"].each do |cookie_value|
+      expect {
         RestClient::Request.new(:method => 'get', :url => 'example.com',
                                 :cookies => {'test' => cookie_value})
-      }.should raise_error(ArgumentError, /\AInvalid cookie value/)
+      }.to raise_error(ArgumentError, /\AInvalid cookie value/i)
     end
   end
 
+  it 'warns when overriding existing headers via payload' do
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com',
+                              payload: {'foo' => 1}, headers: {content_type: :json})
+    }).to match(/warning: Overriding "Content-Type" header/i)
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com',
+                              payload: {'foo' => 1}, headers: {'Content-Type' => 'application/json'})
+    }).to match(/warning: Overriding "Content-Type" header/i)
+
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com',
+                              payload: '123456', headers: {content_length: '20'})
+    }).to match(/warning: Overriding "Content-Length" header/i)
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com',
+                              payload: '123456', headers: {'Content-Length' => '20'})
+    }).to match(/warning: Overriding "Content-Length" header/i)
+  end
+
+  it "does not warn when overriding user header with header derived from payload if those header values were identical" do
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com',
+                              payload: {'foo' => '123456'}, headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
+    }).not_to match(/warning: Overriding "Content-Type" header/i)
+  end
+
+  it 'does not warn for a normal looking payload' do
+    expect(fake_stderr {
+      RestClient::Request.new(method: :post, url: 'example.com', payload: 'payload')
+      RestClient::Request.new(method: :post, url: 'example.com', payload: 'payload', headers: {content_type: :json})
+      RestClient::Request.new(method: :post, url: 'example.com', payload: {'foo' => 'bar'})
+    }).to eq ''
+  end
+
   it "uses netrc credentials" do
-    URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil, :host => 'example.com'))
-    Netrc.stub(:read).and_return('example.com' => ['a', 'b'])
-    @request.parse_url_with_auth('http://example.com/resource')
-    @request.user.should eq 'a'
-    @request.password.should eq 'b'
+    expect(Netrc).to receive(:read).and_return('example.com' => ['a', 'b'])
+    request = RestClient::Request.new(:method => :put, :url => 'http://example.com/', :payload => 'payload')
+    expect(request.user).to eq 'a'
+    expect(request.password).to eq 'b'
   end
 
   it "uses credentials in the url in preference to netrc" do
-    URI.stub(:parse).and_return(double('uri', :user => 'joe%20', :password => 'pass1', :host => 'example.com'))
-    Netrc.stub(:read).and_return('example.com' => ['a', 'b'])
-    @request.parse_url_with_auth('http://joe%20:pass1@example.com/resource')
-    @request.user.should eq 'joe '
-    @request.password.should eq 'pass1'
+    allow(Netrc).to receive(:read).and_return('example.com' => ['a', 'b'])
+    request = RestClient::Request.new(:method => :put, :url =>  'http://joe%20:pass1@example.com/', :payload => 'payload')
+    expect(request.user).to eq 'joe '
+    expect(request.password).to eq 'pass1'
   end
 
   it "determines the Net::HTTP class to instantiate by the method name" do
-    @request.net_http_request_class(:put).should eq Net::HTTP::Put
+    expect(@request.net_http_request_class(:put)).to eq Net::HTTP::Put
   end
 
   describe "user headers" do
     it "merges user headers with the default headers" do
-      @request.should_receive(:default_headers).and_return({ :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' })
+      expect(@request).to receive(:default_headers).and_return({ :accept => '*/*', :accept_encoding => 'gzip, deflate' })
       headers = @request.make_headers("Accept" => "application/json", :accept_encoding => 'gzip')
-      headers.should have_key "Accept-Encoding"
-      headers["Accept-Encoding"].should eq "gzip"
-      headers.should have_key "Accept"
-      headers["Accept"].should eq "application/json"
+      expect(headers).to have_key "Accept-Encoding"
+      expect(headers["Accept-Encoding"]).to eq "gzip"
+      expect(headers).to have_key "Accept"
+      expect(headers["Accept"]).to eq "application/json"
     end
 
     it "prefers the user header when the same header exists in the defaults" do
-      @request.should_receive(:default_headers).and_return({ '1' => '2' })
+      expect(@request).to receive(:default_headers).and_return({ '1' => '2' })
       headers = @request.make_headers('1' => '3')
-      headers.should have_key('1')
-      headers['1'].should eq '3'
+      expect(headers).to have_key('1')
+      expect(headers['1']).to eq '3'
     end
 
     it "converts user headers to string before calling CGI::unescape which fails on non string values" do
-      @request.should_receive(:default_headers).and_return({ '1' => '2' })
+      expect(@request).to receive(:default_headers).and_return({ '1' => '2' })
       headers = @request.make_headers('1' => 3)
-      headers.should have_key('1')
-      headers['1'].should eq '3'
+      expect(headers).to have_key('1')
+      expect(headers['1']).to eq '3'
     end
   end
 
   describe "header symbols" do
 
     it "converts header symbols from :content_type to 'Content-Type'" do
-      @request.should_receive(:default_headers).and_return({})
+      expect(@request).to receive(:default_headers).and_return({})
       headers = @request.make_headers(:content_type => 'abc')
-      headers.should have_key('Content-Type')
-      headers['Content-Type'].should eq 'abc'
+      expect(headers).to have_key('Content-Type')
+      expect(headers['Content-Type']).to eq 'abc'
     end
 
     it "converts content-type from extension to real content-type" do
-      @request.should_receive(:default_headers).and_return({})
+      expect(@request).to receive(:default_headers).and_return({})
       headers = @request.make_headers(:content_type => 'json')
-      headers.should have_key('Content-Type')
-      headers['Content-Type'].should eq 'application/json'
+      expect(headers).to have_key('Content-Type')
+      expect(headers['Content-Type']).to eq 'application/json'
     end
 
     it "converts accept from extension(s) to real content-type(s)" do
-      @request.should_receive(:default_headers).and_return({})
+      expect(@request).to receive(:default_headers).and_return({})
       headers = @request.make_headers(:accept => 'json, mp3')
-      headers.should have_key('Accept')
-      headers['Accept'].should eq 'application/json, audio/mpeg'
+      expect(headers).to have_key('Accept')
+      expect(headers['Accept']).to eq 'application/json, audio/mpeg'
 
-      @request.should_receive(:default_headers).and_return({})
+      expect(@request).to receive(:default_headers).and_return({})
       headers = @request.make_headers(:accept => :json)
-      headers.should have_key('Accept')
-      headers['Accept'].should eq 'application/json'
+      expect(headers).to have_key('Accept')
+      expect(headers['Accept']).to eq 'application/json'
     end
 
     it "only convert symbols in header" do
-      @request.should_receive(:default_headers).and_return({})
+      expect(@request).to receive(:default_headers).and_return({})
       headers = @request.make_headers({:foo_bar => 'value', "bar_bar" => 'value'})
-      headers['Foo-Bar'].should eq 'value'
-      headers['bar_bar'].should eq 'value'
+      expect(headers['Foo-Bar']).to eq 'value'
+      expect(headers['bar_bar']).to eq 'value'
     end
 
     it "converts header values to strings" do
-      @request.make_headers('A' => 1)['A'].should eq '1'
+      expect(@request.make_headers('A' => 1)['A']).to eq '1'
     end
   end
 
   it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
-    @request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
     klass = double("net:http class")
-    @request.should_receive(:net_http_request_class).with(:put).and_return(klass)
-    klass.should_receive(:new).and_return('result')
-    @request.should_receive(:transmit).with(@uri, 'result', kind_of(RestClient::Payload::Base))
+    expect(@request).to receive(:net_http_request_class).with('put').and_return(klass)
+    expect(klass).to receive(:new).and_return('result')
+    expect(@request).to receive(:transmit).with(@request.uri, 'result', kind_of(RestClient::Payload::Base))
     @request.execute
   end
 
+  it "IPv6: executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
+    @request = RestClient::Request.new(:method => :put, :url => 'http://[::1]/some/resource', :payload => 'payload')
+    klass = double("net:http class")
+    expect(@request).to receive(:net_http_request_class).with('put').and_return(klass)
+
+    if RUBY_VERSION >= "2.0.0"
+      expect(klass).to receive(:new).with(kind_of(URI), kind_of(Hash)).and_return('result')
+    else
+      expect(klass).to receive(:new).with(kind_of(String), kind_of(Hash)).and_return('result')
+    end
+
+    expect(@request).to receive(:transmit)
+    @request.execute
+  end
+
+  # TODO: almost none of these tests should actually call transmit, which is
+  # part of the private API
+
   it "transmits the request with Net::HTTP" do
-    @http.should_receive(:request).with('req', 'payload')
-    @request.should_receive(:process_result)
-    @request.transmit(@uri, 'req', 'payload')
+    expect(@http).to receive(:request).with('req', 'payload')
+    expect(@request).to receive(:process_result)
+    @request.send(:transmit, @uri, 'req', 'payload')
   end
 
+  # TODO: most of these payload tests are historical relics that actually
+  # belong in payload_spec.rb. Or we need new tests that actually cover the way
+  # that Request#initialize or Request#execute uses the payload.
   describe "payload" do
     it "sends nil payloads" do
-      @http.should_receive(:request).with('req', nil)
-      @request.should_receive(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', nil)
+      expect(@http).to receive(:request).with('req', nil)
+      expect(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', nil)
     end
 
     it "passes non-hash payloads straight through" do
-      @request.process_payload("x").should eq "x"
+      expect(RestClient::Payload.generate("x").to_s).to eq "x"
     end
 
     it "converts a hash payload to urlencoded data" do
-      @request.process_payload(:a => 'b c+d').should eq "a=b%20c%2Bd"
+      expect(RestClient::Payload.generate(:a => 'b c+d').to_s).to eq "a=b+c%2Bd"
     end
 
     it "accepts nested hashes in payload" do
-      payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
-      payload.should include('user[name]=joe')
-      payload.should include('user[location][country]=USA')
-      payload.should include('user[location][state]=CA')
+      payload = RestClient::Payload.generate(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }}).to_s
+      expect(payload).to include('user[name]=joe')
+      expect(payload).to include('user[location][country]=USA')
+      expect(payload).to include('user[location][state]=CA')
     end
   end
 
   it "set urlencoded content_type header on hash payloads" do
-    @request.process_payload(:a => 1)
-    @request.headers[:content_type].should eq 'application/x-www-form-urlencoded'
+    req = RestClient::Request.new(method: :post, url: 'http://some/resource', payload: {a: 1})
+    expect(req.processed_headers.fetch('Content-Type')).to eq 'application/x-www-form-urlencoded'
   end
 
   describe "credentials" do
     it "sets up the credentials prior to the request" do
-      @http.stub(:request)
+      allow(@http).to receive(:request)
 
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @request.stub(:user).and_return('joe')
-      @request.stub(:password).and_return('mypass')
-      @request.should_receive(:setup_credentials).with('req')
+      allow(@request).to receive(:user).and_return('joe')
+      allow(@request).to receive(:password).and_return('mypass')
+      expect(@request).to receive(:setup_credentials).with('req')
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
     end
 
     it "does not attempt to send any credentials if user is nil" do
-      @request.stub(:user).and_return(nil)
+      allow(@request).to receive(:user).and_return(nil)
       req = double("request")
-      req.should_not_receive(:basic_auth)
-      @request.setup_credentials(req)
+      expect(req).not_to receive(:basic_auth)
+      @request.send(:setup_credentials, req)
     end
 
     it "setup credentials when there's a user" do
-      @request.stub(:user).and_return('joe')
-      @request.stub(:password).and_return('mypass')
+      allow(@request).to receive(:user).and_return('joe')
+      allow(@request).to receive(:password).and_return('mypass')
       req = double("request")
-      req.should_receive(:basic_auth).with('joe', 'mypass')
-      @request.setup_credentials(req)
+      expect(req).to receive(:basic_auth).with('joe', 'mypass')
+      @request.send(:setup_credentials, req)
+    end
+
+    it "does not attempt to send credentials if Authorization header is set" do
+      @request.headers['Authorization'] = 'Token abc123'
+      allow(@request).to receive(:user).and_return('joe')
+      allow(@request).to receive(:password).and_return('mypass')
+      req = double("request")
+      expect(req).not_to receive(:basic_auth)
+      @request.send(:setup_credentials, req)
     end
   end
 
   it "catches EOFError and shows the more informative ServerBrokeConnection" do
-    @http.stub(:request).and_raise(EOFError)
-    lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
+    allow(@http).to receive(:request).and_raise(EOFError)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::ServerBrokeConnection)
   end
 
   it "catches OpenSSL::SSL::SSLError and raise it back without more informative message" do
-    @http.stub(:request).and_raise(OpenSSL::SSL::SSLError)
-    lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(OpenSSL::SSL::SSLError)
+    allow(@http).to receive(:request).and_raise(OpenSSL::SSL::SSLError)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(OpenSSL::SSL::SSLError)
+  end
+
+  it "catches Timeout::Error and raise the more informative ReadTimeout" do
+    allow(@http).to receive(:request).and_raise(Timeout::Error)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
+  end
+
+  it "catches Errno::ETIMEDOUT and raise the more informative ReadTimeout" do
+    allow(@http).to receive(:request).and_raise(Errno::ETIMEDOUT)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
+  end
+
+  it "catches Net::ReadTimeout and raises RestClient's ReadTimeout",
+     :if => defined?(Net::ReadTimeout) do
+    allow(@http).to receive(:request).and_raise(Net::ReadTimeout)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
   end
 
-  it "catches Timeout::Error and raise the more informative RequestTimeout" do
-    @http.stub(:request).and_raise(Timeout::Error)
-    lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::RequestTimeout)
+  it "catches Net::OpenTimeout and raises RestClient's OpenTimeout",
+     :if => defined?(Net::OpenTimeout) do
+    allow(@http).to receive(:request).and_raise(Net::OpenTimeout)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::OpenTimeout)
   end
 
-  it "catches Timeout::Error and raise the more informative RequestTimeout" do
-    @http.stub(:request).and_raise(Errno::ETIMEDOUT)
-    lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::RequestTimeout)
+  it "uses correct error message for ReadTimeout",
+     :if => defined?(Net::ReadTimeout) do
+    allow(@http).to receive(:request).and_raise(Net::ReadTimeout)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout, 'Timed out reading data from server')
   end
 
+  it "uses correct error message for OpenTimeout",
+     :if => defined?(Net::OpenTimeout) do
+    allow(@http).to receive(:request).and_raise(Net::OpenTimeout)
+    expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::OpenTimeout, 'Timed out connecting to server')
+  end
+
+
   it "class method execute wraps constructor" do
     req = double("rest request")
-    RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
-    req.should_receive(:execute)
+    expect(RestClient::Request).to receive(:new).with(1 => 2).and_return(req)
+    expect(req).to receive(:execute)
     RestClient::Request.execute(1 => 2)
   end
 
   describe "exception" do
     it "raises Unauthorized when the response is 401" do
-      res = double('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' )
-      lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
+      res = response_double(:code => '401', :[] => ['content-encoding' => ''], :body => '' )
+      expect { @request.send(:process_result, res) }.to raise_error(RestClient::Unauthorized)
     end
 
     it "raises ResourceNotFound when the response is 404" do
-      res = double('response', :code => '404', :[] => ['content-encoding' => ''], :body => '' )
-      lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
+      res = response_double(:code => '404', :[] => ['content-encoding' => ''], :body => '' )
+      expect { @request.send(:process_result, res) }.to raise_error(RestClient::ResourceNotFound)
     end
 
     it "raises RequestFailed otherwise" do
-      res = double('response', :code => '500', :[] => ['content-encoding' => ''], :body => '' )
-      lambda { @request.process_result(res) }.should raise_error(RestClient::InternalServerError)
+      res = response_double(:code => '500', :[] => ['content-encoding' => ''], :body => '' )
+      expect { @request.send(:process_result, res) }.to raise_error(RestClient::InternalServerError)
     end
   end
 
   describe "block usage" do
     it "returns what asked to" do
-      res = double('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' )
-      @request.process_result(res){|response, request| "foo"}.should eq "foo"
+      res = response_double(:code => '401', :[] => ['content-encoding' => ''], :body => '' )
+      expect(@request.send(:process_result, res){|response, request| "foo"}).to eq "foo"
     end
   end
 
   describe "proxy" do
+    before do
+      # unstub Net::HTTP creation since we need to test it
+      allow(Net::HTTP).to receive(:new).and_call_original
+
+      @proxy_req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+    end
+
     it "creates a proxy class if a proxy url is given" do
-      RestClient.stub(:proxy).and_return("http://example.com/")
-      @request.net_http_class.proxy_class?.should be_true
+      allow(RestClient).to receive(:proxy).and_return("http://example.com/")
+      allow(RestClient).to receive(:proxy_set?).and_return(true)
+      expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+    end
+
+    it "creates a proxy class with the correct address if a IPv6 proxy url is given" do
+      allow(RestClient).to receive(:proxy).and_return("http://[::1]/")
+      allow(RestClient).to receive(:proxy_set?).and_return(true)
+      expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+      expect(@proxy_req.net_http_object('host', 80).proxy_address).to eq('::1')
     end
 
     it "creates a non-proxy class if a proxy url is not given" do
-      @request.net_http_class.proxy_class?.should be_false
+      expect(@proxy_req.net_http_object('host', 80).proxy?).to be_falsey
+    end
+
+    it "disables proxy on a per-request basis" do
+      allow(RestClient).to receive(:proxy).and_return('http://example.com')
+      allow(RestClient).to receive(:proxy_set?).and_return(true)
+      expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+
+      disabled_req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+      expect(disabled_req.net_http_object('host', 80).proxy?).to be_falsey
+    end
+
+    it "sets proxy on a per-request basis" do
+      expect(@proxy_req.net_http_object('some', 80).proxy?).to be_falsey
+
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => 'http://example.com')
+      expect(req.net_http_object('host', 80).proxy?).to be true
+    end
+
+    it "overrides proxy from environment", if: RUBY_VERSION >= '2.0' do
+      allow(ENV).to receive(:[]).with("http_proxy").and_return("http://127.0.0.1")
+      allow(ENV).to receive(:[]).with("no_proxy").and_return(nil)
+      allow(ENV).to receive(:[]).with("NO_PROXY").and_return(nil)
+      allow(Netrc).to receive(:read).and_return({})
+
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+      obj = req.net_http_object('host', 80)
+      expect(obj.proxy?).to be true
+      expect(obj.proxy_address).to eq '127.0.0.1'
+
+      # test original method .proxy?
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+      obj = req.net_http_object('host', 80)
+      expect(obj.proxy?).to be_falsey
+
+      # stub RestClient.proxy_set? to peek into implementation
+      allow(RestClient).to receive(:proxy_set?).and_return(true)
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+      obj = req.net_http_object('host', 80)
+      expect(obj.proxy?).to be_falsey
+
+      # test stubbed Net::HTTP.new
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+      expect(Net::HTTP).to receive(:new).with('host', 80, nil, nil, nil, nil)
+      req.net_http_object('host', 80)
+    end
+
+    it "overrides global proxy with per-request proxy" do
+      allow(RestClient).to receive(:proxy).and_return('http://example.com')
+      allow(RestClient).to receive(:proxy_set?).and_return(true)
+      obj = @proxy_req.net_http_object('host', 80)
+      expect(obj.proxy?).to be true
+      expect(obj.proxy_address).to eq 'example.com'
+
+      req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => 'http://127.0.0.1/')
+      expect(req.net_http_object('host', 80).proxy?).to be true
+      expect(req.net_http_object('host', 80).proxy_address).to eq('127.0.0.1')
     end
   end
 
@@ -369,189 +643,221 @@ describe RestClient::Request do
   describe "logging" do
     it "logs a get request" do
       log = RestClient.log = []
-      RestClient::Request.new(:method => :get, :url => 'http://url').log_request
-      log[0].should eq %Q{RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate"\n}
+      RestClient::Request.new(:method => :get, :url => 'http://url', :headers => {:user_agent => 'rest-client'}).log_request
+      expect(log[0]).to eq %Q{RestClient.get "http://url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
     end
 
     it "logs a post request with a small payload" do
       log = RestClient.log = []
-      RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request
-      log[0].should eq %Q{RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"3"\n}
+      RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo', :headers => {:user_agent => 'rest-client'}).log_request
+      expect(log[0]).to eq %Q{RestClient.post "http://url", "foo", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"3", "User-Agent"=>"rest-client"\n}
     end
 
     it "logs a post request with a large payload" do
       log = RestClient.log = []
-      RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request
-      log[0].should eq %Q{RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"1000"\n}
+      RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000), :headers => {:user_agent => 'rest-client'}).log_request
+      expect(log[0]).to eq %Q{RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"1000", "User-Agent"=>"rest-client"\n}
     end
 
     it "logs input headers as a hash" do
       log = RestClient.log = []
-      RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request
-      log[0].should eq %Q{RestClient.get "http://url", "Accept"=>"text/plain", "Accept-Encoding"=>"gzip, deflate"\n}
+      RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain', :user_agent => 'rest-client' }).log_request
+      expect(log[0]).to eq %Q{RestClient.get "http://url", "Accept"=>"text/plain", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
     end
 
     it "logs a response including the status code, content type, and result body size in bytes" do
       log = RestClient.log = []
       res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
-      res.stub(:[]).with('Content-type').and_return('text/html')
+      allow(res).to receive(:[]).with('Content-type').and_return('text/html')
       @request.log_response res
-      log[0].should eq "# => 200 OK | text/html 4 bytes\n"
+      expect(log[0]).to eq "# => 200 OK | text/html 4 bytes\n"
     end
 
     it "logs a response with a nil Content-type" do
       log = RestClient.log = []
       res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
-      res.stub(:[]).with('Content-type').and_return(nil)
+      allow(res).to receive(:[]).with('Content-type').and_return(nil)
       @request.log_response res
-      log[0].should eq "# => 200 OK |  4 bytes\n"
+      expect(log[0]).to eq "# => 200 OK |  4 bytes\n"
     end
 
     it "logs a response with a nil body" do
       log = RestClient.log = []
       res = double('result', :code => '200', :class => Net::HTTPOK, :body => nil)
-      res.stub(:[]).with('Content-type').and_return('text/html; charset=utf-8')
+      allow(res).to receive(:[]).with('Content-type').and_return('text/html; charset=utf-8')
       @request.log_response res
-      log[0].should eq "# => 200 OK | text/html 0 bytes\n"
+      expect(log[0]).to eq "# => 200 OK | text/html 0 bytes\n"
     end
 
     it 'does not log request password' do
       log = RestClient.log = []
-      RestClient::Request.new(:method => :get, :url => 'http://user:password@url', :headers => {:user_agent => 'rest-client', :accept => '*/*'}).log_request
-      log[0].should eq %Q{RestClient.get "http://user:REDACTED@url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
-    end
-
-    it 'logs invalid URIs, even though they will fail elsewhere' do
-      log = RestClient.log = []
-      RestClient::Request.new(:method => :get, :url => 'http://a@b:c', :headers => {:user_agent => 'rest-client', :accept => '*/*'}).log_request
-      log[0].should eq %Q{RestClient.get "[invalid uri]", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
+      RestClient::Request.new(:method => :get, :url => 'http://user:password@url', :headers => {:user_agent => 'rest-client'}).log_request
+      expect(log[0]).to eq %Q{RestClient.get "http://user:REDACTED@url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
     end
   end
 
   it "strips the charset from the response content type" do
     log = RestClient.log = []
     res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
-    res.stub(:[]).with('Content-type').and_return('text/html; charset=utf-8')
+    allow(res).to receive(:[]).with('Content-type').and_return('text/html; charset=utf-8')
     @request.log_response res
-    log[0].should eq "# => 200 OK | text/html 4 bytes\n"
+    expect(log[0]).to eq "# => 200 OK | text/html 4 bytes\n"
   end
 
   describe "timeout" do
     it "does not set timeouts if not specified" do
       @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @net.should_not_receive(:read_timeout=)
-      @net.should_not_receive(:open_timeout=)
+      expect(@net).not_to receive(:read_timeout=)
+      expect(@net).not_to receive(:open_timeout=)
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
     end
 
-    it "set read_timeout" do
-      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+    it 'sets read_timeout' do
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => 123)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @net.should_receive(:read_timeout=).with(123)
+      expect(@net).to receive(:read_timeout=).with(123)
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
     end
 
-    it "set open_timeout" do
+    it "sets open_timeout" do
       @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @net.should_receive(:open_timeout=).with(123)
+      expect(@net).to receive(:open_timeout=).with(123)
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
     end
 
+    it 'sets both timeouts with :timeout' do
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+
+      expect(@net).to receive(:open_timeout=).with(123)
+      expect(@net).to receive(:read_timeout=).with(123)
+
+      @request.send(:transmit, @uri, 'req', nil)
+    end
+
+    it 'supersedes :timeout with open/read_timeout' do
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123, :open_timeout => 34, :read_timeout => 56)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+
+      expect(@net).to receive(:open_timeout=).with(34)
+      expect(@net).to receive(:read_timeout=).with(56)
+
+      @request.send(:transmit, @uri, 'req', nil)
+    end
+
+
     it "disable timeout by setting it to nil" do
-      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => nil, :open_timeout => nil)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => nil, :open_timeout => nil)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @net.should_receive(:read_timeout=).with(nil)
-      @net.should_receive(:open_timeout=).with(nil)
+      expect(@net).to receive(:read_timeout=).with(nil)
+      expect(@net).to receive(:open_timeout=).with(nil)
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
+    end
+
+    it 'deprecated: warns when disabling timeout by setting it to -1' do
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => -1)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+
+      expect(@net).to receive(:read_timeout=).with(nil)
+
+      expect(fake_stderr {
+        @request.send(:transmit, @uri, 'req', nil)
+      }).to match(/^Deprecated: .*timeout.* nil instead of -1$/)
     end
 
     it "deprecated: disable timeout by setting it to -1" do
-      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => -1, :open_timeout => -1)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
+      @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => -1, :open_timeout => -1)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
 
-      @request.should_receive(:warn)
-      @net.should_receive(:read_timeout=).with(nil)
+      expect(@request).to receive(:warn)
+      expect(@net).to receive(:read_timeout=).with(nil)
 
-      @request.should_receive(:warn)
-      @net.should_receive(:open_timeout=).with(nil)
+      expect(@request).to receive(:warn)
+      expect(@net).to receive(:open_timeout=).with(nil)
 
-      @request.transmit(@uri, 'req', nil)
+      @request.send(:transmit, @uri, 'req', nil)
     end
   end
 
   describe "ssl" do
     it "uses SSL when the URI refers to a https address" do
-      @uri.stub(:is_a?).with(URI::HTTPS).and_return(true)
-      @net.should_receive(:use_ssl=).with(true)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      allow(@uri).to receive(:is_a?).with(URI::HTTPS).and_return(true)
+      expect(@net).to receive(:use_ssl=).with(true)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should default to verifying ssl certificates" do
-      @request.verify_ssl.should eq OpenSSL::SSL::VERIFY_PEER
+      expect(@request.verify_ssl).to eq OpenSSL::SSL::VERIFY_PEER
     end
 
     it "should have expected values for VERIFY_PEER and VERIFY_NONE" do
-      OpenSSL::SSL::VERIFY_NONE.should eq(0)
-      OpenSSL::SSL::VERIFY_PEER.should eq(1)
+      expect(OpenSSL::SSL::VERIFY_NONE).to eq(0)
+      expect(OpenSSL::SSL::VERIFY_PEER).to eq(1)
     end
 
     it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
       @request = RestClient::Request.new(:method => :put, :verify_ssl => false, :url => 'http://some/resource', :payload => 'payload')
-      @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do
       @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
-      @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set net.verify_mode to OpenSSL::SSL::VERIFY_PEER if verify_ssl is true" do
       @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
-      @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set net.verify_mode to OpenSSL::SSL::VERIFY_PEER if verify_ssl is not given" do
       @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload')
-      @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
@@ -560,15 +866,15 @@ describe RestClient::Request do
                                           :url => 'https://some/resource',
                                           :payload => 'payload',
                                           :verify_ssl => mode )
-      @net.should_receive(:verify_mode=).with(mode)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:verify_mode=).with(mode)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should default to not having an ssl_client_cert" do
-      @request.ssl_client_cert.should be(nil)
+      expect(@request.ssl_client_cert).to be(nil)
     end
 
     it "should set the ssl_version if provided" do
@@ -578,11 +884,11 @@ describe RestClient::Request do
         :payload => 'payload',
         :ssl_version => "TLSv1"
       )
-      @net.should_receive(:ssl_version=).with("TLSv1")
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:ssl_version=).with("TLSv1")
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_version if not provided" do
@@ -591,11 +897,11 @@ describe RestClient::Request do
         :url => 'https://some/resource',
         :payload => 'payload'
       )
-      @net.should_not_receive(:ssl_version=).with("TLSv1")
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:ssl_version=).with("TLSv1")
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set the ssl_ciphers if provided" do
@@ -606,11 +912,11 @@ describe RestClient::Request do
         :payload => 'payload',
         :ssl_ciphers => ciphers
       )
-      @net.should_receive(:ciphers=).with(ciphers)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:ciphers=).with(ciphers)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_ciphers if set to nil" do
@@ -620,61 +926,11 @@ describe RestClient::Request do
         :payload => 'payload',
         :ssl_ciphers => nil,
       )
-      @net.should_not_receive(:ciphers=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
-    end
-
-    it "should override ssl_ciphers with better defaults with weak default ciphers" do
-      stub_const(
-        '::OpenSSL::SSL::SSLContext::DEFAULT_PARAMS',
-        {
-          :ssl_version=>"SSLv23",
-          :verify_mode=>1,
-          :ciphers=>"ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW",
-          :options=>-2147480577,
-        }
-      )
-
-      @request = RestClient::Request.new(
-        :method => :put,
-        :url => 'https://some/resource',
-        :payload => 'payload',
-      )
-
-      @net.should_receive(:ciphers=).with(RestClient::Request::DefaultCiphers)
-
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
-    end
-
-    it "should not override ssl_ciphers with better defaults with different default ciphers" do
-      stub_const(
-        '::OpenSSL::SSL::SSLContext::DEFAULT_PARAMS',
-        {
-          :ssl_version=>"SSLv23",
-          :verify_mode=>1,
-          :ciphers=>"HIGH:!aNULL:!eNULL:!EXPORT:!LOW:!MEDIUM:!SSLv2",
-          :options=>-2147480577,
-        }
-      )
-
-      @request = RestClient::Request.new(
-        :method => :put,
-        :url => 'https://some/resource',
-        :payload => 'payload',
-      )
-
-      @net.should_not_receive(:ciphers=)
-
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:ciphers=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set the ssl_client_cert if provided" do
@@ -684,11 +940,11 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_client_cert => "whatsupdoc!"
       )
-      @net.should_receive(:cert=).with("whatsupdoc!")
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:cert=).with("whatsupdoc!")
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_client_cert if it is not provided" do
@@ -697,15 +953,15 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload'
       )
-      @net.should_not_receive(:cert=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:cert=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should default to not having an ssl_client_key" do
-      @request.ssl_client_key.should be(nil)
+      expect(@request.ssl_client_key).to be(nil)
     end
 
     it "should set the ssl_client_key if provided" do
@@ -715,11 +971,11 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_client_key => "whatsupdoc!"
       )
-      @net.should_receive(:key=).with("whatsupdoc!")
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:key=).with("whatsupdoc!")
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_client_key if it is not provided" do
@@ -728,15 +984,15 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload'
       )
-      @net.should_not_receive(:key=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:key=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should default to not having an ssl_ca_file" do
-      @request.ssl_ca_file.should be(nil)
+      expect(@request.ssl_ca_file).to be(nil)
     end
 
     it "should set the ssl_ca_file if provided" do
@@ -746,12 +1002,12 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_ca_file => "Certificate Authority File"
       )
-      @net.should_receive(:ca_file=).with("Certificate Authority File")
-      @net.should_not_receive(:cert_store=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:ca_file=).with("Certificate Authority File")
+      expect(@net).not_to receive(:cert_store=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_ca_file if it is not provided" do
@@ -760,15 +1016,15 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload'
       )
-      @net.should_not_receive(:ca_file=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:ca_file=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should default to not having an ssl_ca_path" do
-      @request.ssl_ca_path.should be(nil)
+      expect(@request.ssl_ca_path).to be(nil)
     end
 
     it "should set the ssl_ca_path if provided" do
@@ -778,12 +1034,12 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_ca_path => "Certificate Authority Path"
       )
-      @net.should_receive(:ca_path=).with("Certificate Authority Path")
-      @net.should_not_receive(:cert_store=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:ca_path=).with("Certificate Authority Path")
+      expect(@net).not_to receive(:cert_store=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_ca_path if it is not provided" do
@@ -792,11 +1048,11 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload'
       )
-      @net.should_not_receive(:ca_path=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:ca_path=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set the ssl_cert_store if provided" do
@@ -809,13 +1065,13 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_cert_store => store
       )
-      @net.should_receive(:cert_store=).with(store)
-      @net.should_not_receive(:ca_path=)
-      @net.should_not_receive(:ca_file=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:cert_store=).with(store)
+      expect(@net).not_to receive(:ca_path=)
+      expect(@net).not_to receive(:ca_file=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should by default set the ssl_cert_store if no CA info is provided" do
@@ -824,13 +1080,13 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload'
       )
-      @net.should_receive(:cert_store=)
-      @net.should_not_receive(:ca_path=)
-      @net.should_not_receive(:ca_file=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).to receive(:cert_store=)
+      expect(@net).not_to receive(:ca_path=)
+      expect(@net).not_to receive(:ca_file=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_cert_store if it is set falsy" do
@@ -840,11 +1096,11 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_cert_store => nil,
       )
-      @net.should_not_receive(:cert_store=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:cert_store=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should not set the ssl_verify_callback by default" do
@@ -853,11 +1109,11 @@ describe RestClient::Request do
               :url => 'https://some/resource',
               :payload => 'payload',
       )
-      @net.should_not_receive(:verify_callback=)
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      expect(@net).not_to receive(:verify_callback=)
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     it "should set the ssl_verify_callback if passed" do
@@ -868,7 +1124,7 @@ describe RestClient::Request do
               :payload => 'payload',
               :ssl_verify_callback => callback,
       )
-      @net.should_receive(:verify_callback=).with(callback)
+      expect(@net).to receive(:verify_callback=).with(callback)
 
       # we'll read cert_store on jruby
       # https://github.com/jruby/jruby/issues/597
@@ -876,10 +1132,10 @@ describe RestClient::Request do
         allow(@net).to receive(:cert_store)
       end
 
-      @http.stub(:request)
-      @request.stub(:process_result)
-      @request.stub(:response_log)
-      @request.transmit(@uri, 'req', 'payload')
+      allow(@http).to receive(:request)
+      allow(@request).to receive(:process_result)
+      allow(@request).to receive(:response_log)
+      @request.send(:transmit, @uri, 'req', 'payload')
     end
 
     # </ssl>
@@ -892,11 +1148,11 @@ describe RestClient::Request do
             :payload => 'payload'
     )
     net_http_res = Net::HTTPNoContent.new("", "204", "No Content")
-    net_http_res.stub(:read_body).and_return(nil)
-    @http.should_receive(:request).and_return(@request.fetch_body(net_http_res))
-    response = @request.transmit(@uri, 'req', 'payload')
-    response.should_not be_nil
-    response.code.should eq 204
+    allow(net_http_res).to receive(:read_body).and_return(nil)
+    expect(@http).to receive(:request).and_return(@request.send(:fetch_body, net_http_res))
+    response = @request.send(:transmit, @uri, 'req', 'payload')
+    expect(response).not_to be_nil
+    expect(response.code).to eq 204
   end
 
   describe "raw response" do
@@ -904,14 +1160,91 @@ describe RestClient::Request do
       @request = RestClient::Request.new(:method => "get", :url => "example.com", :raw_response => true)
 
       tempfile = double("tempfile")
-      tempfile.should_receive(:binmode)
-      tempfile.stub(:open)
-      tempfile.stub(:close)
-      Tempfile.should_receive(:new).with("rest-client").and_return(tempfile)
+      expect(tempfile).to receive(:binmode)
+      allow(tempfile).to receive(:open)
+      allow(tempfile).to receive(:close)
+      expect(Tempfile).to receive(:new).with("rest-client.").and_return(tempfile)
 
       net_http_res = Net::HTTPOK.new(nil, "200", "body")
-      net_http_res.stub(:read_body).and_return("body")
-      @request.fetch_body(net_http_res)
+      allow(net_http_res).to receive(:read_body).and_return("body")
+      @request.send(:fetch_body, net_http_res)
+    end
+  end
+
+  describe 'payloads' do
+    it 'should accept string payloads' do
+      payload = 'Foo'
+      @request = RestClient::Request.new(method: :get, url: 'example.com', :payload => payload)
+      expect(@request).to receive(:process_result)
+      expect(@http).to receive(:request).with('req', payload)
+      @request.send(:transmit, @uri, 'req', payload)
+    end
+
+    it 'should accept streaming IO payloads' do
+      payload = StringIO.new('streamed')
+
+      @request = RestClient::Request.new(method: :get, url: 'example.com', :payload => payload)
+      expect(@request).to receive(:process_result)
+
+      @get = double('net::http::get')
+      expect(@get).to receive(:body_stream=).with(instance_of(RestClient::Payload::Streamed))
+
+      allow(@request.net_http_request_class(:GET)).to receive(:new).and_return(@get)
+      expect(@http).to receive(:request).with(@get, nil)
+      @request.execute
+    end
+  end
+
+  describe 'constructor' do
+    it 'should reject valid URIs with no hostname' do
+      expect(URI.parse('http:///').hostname).to be_nil
+
+      expect {
+        RestClient::Request.new(method: :get, url: 'http:///')
+      }.to raise_error(URI::InvalidURIError, /\Abad URI/)
+    end
+
+    it 'should reject invalid URIs' do
+      expect {
+        RestClient::Request.new(method: :get, url: 'http://::')
+      }.to raise_error(URI::InvalidURIError)
+    end
+  end
+
+  describe 'process_url_params' do
+    it 'should handle basic URL params' do
+      expect(@request.process_url_params('https://example.com/foo', params: {key1: 123, key2: 'abc'})).
+        to eq 'https://example.com/foo?key1=123&key2=abc'
+
+      expect(@request.process_url_params('https://example.com/foo', params: {'key1' => 123})).
+        to eq 'https://example.com/foo?key1=123'
+
+      expect(@request.process_url_params('https://example.com/path',
+                                  params: {foo: 'one two', bar: 'three + four == seven'})).
+        to eq 'https://example.com/path?foo=one+two&bar=three+%2B+four+%3D%3D+seven'
+    end
+
+    it 'should combine with & when URL params already exist' do
+      expect(@request.process_url_params('https://example.com/path?foo=1', params: {bar: 2})).
+        to eq 'https://example.com/path?foo=1&bar=2'
+    end
+
+    it 'should handle complex nested URL params per Rack / Rails conventions' do
+      expect(@request.process_url_params('https://example.com/', params: {
+        foo: [1,2,3],
+        null: nil,
+        false: false,
+        math: '2+2=4',
+        nested: {'key + escaped' => 'value + escaped', other: [], arr: [1,2]},
+      })).to eq 'https://example.com/?foo[]=1&foo[]=2&foo[]=3&null&false=false&math=2%2B2%3D4' \
+                   '&nested[key+%2B+escaped]=value+%2B+escaped&nested[other]' \
+                   '&nested[arr][]=1&nested[arr][]=2'
+    end
+
+    it 'should handle ParamsArray objects' do
+      expect(@request.process_url_params('https://example.com/',
+        params: RestClient::ParamsArray.new([[:foo, 1], [:foo, 2]])
+      )).to eq 'https://example.com/?foo=1&foo=2'
     end
   end
 end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index 1657ee8..3777767 100644
--- a/spec/unit/resource_spec.rb
+++ b/spec/unit/resource_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient::Resource do
   before do
@@ -7,37 +7,37 @@ describe RestClient::Resource do
 
   context "Resource delegation" do
     it "GET" do
-      RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.get
     end
 
     it "HEAD" do
-      RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.head
     end
 
     it "POST" do
-      RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.post 'abc', :content_type => 'image/jpg'
     end
 
     it "PUT" do
-      RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.put 'abc', :content_type => 'image/jpg'
     end
 
     it "PATCH" do
-      RestClient::Request.should_receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.patch 'abc', :content_type => 'image/jpg'
     end
 
     it "DELETE" do
-      RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
       @resource.delete
     end
 
     it "overrides resource headers" do
-      RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '2'}, :user => 'jane', :password => 'mypass')
+      expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '2'}, :user => 'jane', :password => 'mypass')
       @resource.get 'X-Something' => '2'
     end
   end
@@ -48,41 +48,41 @@ describe RestClient::Resource do
 
   it "is backwards compatible with previous constructor" do
     @resource = RestClient::Resource.new('http://some/resource', 'user', 'pass')
-    @resource.user.should eq 'user'
-    @resource.password.should eq 'pass'
+    expect(@resource.user).to eq 'user'
+    expect(@resource.password).to eq 'pass'
   end
 
   it "concatenates urls, inserting a slash when it needs one" do
-    @resource.concat_urls('http://example.com', 'resource').should eq 'http://example.com/resource'
+    expect(@resource.concat_urls('http://example.com', 'resource')).to eq 'http://example.com/resource'
   end
 
   it "concatenates urls, using no slash if the first url ends with a slash" do
-    @resource.concat_urls('http://example.com/', 'resource').should eq 'http://example.com/resource'
+    expect(@resource.concat_urls('http://example.com/', 'resource')).to eq 'http://example.com/resource'
   end
 
   it "concatenates urls, using no slash if the second url starts with a slash" do
-    @resource.concat_urls('http://example.com', '/resource').should eq 'http://example.com/resource'
+    expect(@resource.concat_urls('http://example.com', '/resource')).to eq 'http://example.com/resource'
   end
 
   it "concatenates even non-string urls, :posts + 1 => 'posts/1'" do
-    @resource.concat_urls(:posts, 1).should eq 'posts/1'
+    expect(@resource.concat_urls(:posts, 1)).to eq 'posts/1'
   end
 
   it "offers subresources via []" do
     parent = RestClient::Resource.new('http://example.com')
-    parent['posts'].url.should eq 'http://example.com/posts'
+    expect(parent['posts'].url).to eq 'http://example.com/posts'
   end
 
   it "transports options to subresources" do
     parent = RestClient::Resource.new('http://example.com', :user => 'user', :password => 'password')
-    parent['posts'].user.should eq 'user'
-    parent['posts'].password.should eq 'password'
+    expect(parent['posts'].user).to eq 'user'
+    expect(parent['posts'].password).to eq 'password'
   end
 
   it "passes a given block to subresources" do
     block = proc {|r| r}
     parent = RestClient::Resource.new('http://example.com', &block)
-    parent['posts'].block.should eq block
+    expect(parent['posts'].block).to eq block
   end
 
   it "the block should be overrideable" do
@@ -90,40 +90,44 @@ describe RestClient::Resource do
     block2 = proc {|r| }
     parent = RestClient::Resource.new('http://example.com', &block1)
     # parent['posts', &block2].block.should eq block2 # ruby 1.9 syntax
-    parent.send(:[], 'posts', &block2).block.should eq block2
-    parent.send(:[], 'posts', &block2).block.should_not eq block1
+    expect(parent.send(:[], 'posts', &block2).block).to eq block2
+    expect(parent.send(:[], 'posts', &block2).block).not_to eq block1
   end
 
-  it "the block should be overrideable in ruby 1.9 syntax" do
+  # Test fails on jruby 9.1.[0-5].* due to
+  # https://github.com/jruby/jruby/issues/4217
+  it "the block should be overrideable in ruby 1.9 syntax",
+      :unless => (RUBY_ENGINE == 'jruby' && JRUBY_VERSION =~ /\A9\.1\.[0-5]\./) \
+  do
     block1 = proc {|r| r}
     block2 = ->(r) {}
 
     parent = RestClient::Resource.new('http://example.com', &block1)
-    parent['posts', &block2].block.should eq block2
-    parent['posts', &block2].block.should_not eq block1
+    expect(parent['posts', &block2].block).to eq block2
+    expect(parent['posts', &block2].block).not_to eq block1
   end
 
   it "prints its url with to_s" do
-    RestClient::Resource.new('x').to_s.should eq 'x'
+    expect(RestClient::Resource.new('x').to_s).to eq 'x'
   end
 
   describe 'block' do
     it 'can use block when creating the resource' do
       stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
       resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' }
-      resource.get.should eq 'foo'
+      expect(resource.get).to eq 'foo'
     end
 
     it 'can use block when executing the resource' do
       stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
       resource = RestClient::Resource.new('www.example.com')
-      resource.get { |response, request| 'foo' }.should eq 'foo'
+      expect(resource.get { |response, request| 'foo' }).to eq 'foo'
     end
 
     it 'execution block override resource block' do
       stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
       resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' }
-      resource.get { |response, request| 'bar' }.should eq 'bar'
+      expect(resource.get { |response, request| 'bar' }).to eq 'bar'
     end
 
   end
diff --git a/spec/unit/response_spec.rb b/spec/unit/response_spec.rb
index 2507676..096c2ab 100644
--- a/spec/unit/response_spec.rb
+++ b/spec/unit/response_spec.rb
@@ -1,26 +1,40 @@
-require 'spec_helper'
+require_relative '_lib'
 
-describe RestClient::Response do
+describe RestClient::Response, :include_helpers do
   before do
     @net_http_res = double('net http response', :to_hash => {"Status" => ["200 OK"]}, :code => 200)
     @example_url = 'http://example.com'
-    @request = double('http request', :user => nil, :password => nil, :url => @example_url)
-    @response = RestClient::Response.create('abc', @net_http_res, {}, @request)
+    @request = request_double(url: @example_url, method: 'get')
+    @response = RestClient::Response.create('abc', @net_http_res, @request)
   end
 
   it "behaves like string" do
-    @response.to_s.should eq 'abc'
-    @response.to_str.should eq 'abc'
-    @response.to_i.should eq 200
+    expect(@response.to_s).to eq 'abc'
+    expect(@response.to_str).to eq 'abc'
+
+    expect(@response).to receive(:warn)
+    expect(@response.to_i).to eq 0
   end
 
   it "accepts nil strings and sets it to empty for the case of HEAD" do
-    RestClient::Response.create(nil, @net_http_res, {}, @request).to_s.should eq ""
+    expect(RestClient::Response.create(nil, @net_http_res, @request).to_s).to eq ""
   end
 
-  it "test headers and raw headers" do
-    @response.raw_headers["Status"][0].should eq "200 OK"
-    @response.headers[:status].should eq "200 OK"
+  describe 'header processing' do
+    it "test headers and raw headers" do
+      expect(@response.raw_headers["Status"][0]).to eq "200 OK"
+      expect(@response.headers[:status]).to eq "200 OK"
+    end
+
+    it 'handles multiple headers by joining with comma' do
+      @net_http_res = double('net http response', :to_hash => {'My-Header' => ['foo', 'bar']}, :code => 200)
+      @example_url = 'http://example.com'
+      @request = request_double(url: @example_url, method: 'get')
+      @response = RestClient::Response.create('abc', @net_http_res, @request)
+
+      expect(@response.raw_headers['My-Header']).to eq ['foo', 'bar']
+      expect(@response.headers[:my_header]).to eq 'foo, bar'
+    end
   end
 
   describe "cookie processing" do
@@ -28,16 +42,16 @@ describe RestClient::Response do
       header_val = "main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT".freeze
 
       net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => [header_val]})
-      response = RestClient::Response.create('abc', net_http_res, {}, @request)
-      response.headers[:set_cookie].should eq [header_val]
-      response.cookies.should eq({ "main_page" => "main_page_no_rewrite" })
+      response = RestClient::Response.create('abc', net_http_res, @request)
+      expect(response.headers[:set_cookie]).to eq [header_val]
+      expect(response.cookies).to eq({ "main_page" => "main_page_no_rewrite" })
     end
 
     it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do
       net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]})
-      response = RestClient::Response.create('abc', net_http_res, {}, @request)
-      response.headers[:set_cookie].should eq ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]
-      response.cookies.should eq({
+      response = RestClient::Response.create('abc', net_http_res, @request)
+      expect(response.headers[:set_cookie]).to eq ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]
+      expect(response.cookies).to eq({
         "main_page" => "main_page_no_rewrite",
         "remember_me" => "",
         "user" => "somebody"
@@ -46,8 +60,8 @@ describe RestClient::Response do
 
     it "should correctly deal with multiple cookies [one Set-Cookie header with multiple cookies]" do
       net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT, remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT, user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]})
-      response = RestClient::Response.create('abc', net_http_res, {}, @request)
-      response.cookies.should eq({
+      response = RestClient::Response.create('abc', net_http_res, @request)
+      expect(response.cookies).to eq({
         "main_page" => "main_page_no_rewrite",
         "remember_me" => "",
         "user" => "somebody"
@@ -58,18 +72,19 @@ describe RestClient::Response do
   describe "exceptions processing" do
     it "should return itself for normal codes" do
       (200..206).each do |code|
-        net_http_res = double('net http response', :code => '200')
-        response = RestClient::Response.create('abc', net_http_res, {}, @request)
-        response.return! @request
+        net_http_res = response_double(:code => '200')
+        resp = RestClient::Response.create('abc', net_http_res, @request)
+        resp.return!
       end
     end
 
     it "should throw an exception for other codes" do
-      RestClient::Exceptions::EXCEPTIONS_MAP.each_key do |code|
+      RestClient::Exceptions::EXCEPTIONS_MAP.each_pair do |code, exc|
         unless (200..207).include? code
-          net_http_res = double('net http response', :code => code.to_i)
-          response = RestClient::Response.create('abc', net_http_res, {}, @request)
-          lambda { response.return!}.should raise_error
+          net_http_res = response_double(:code => code.to_i)
+          resp = RestClient::Response.create('abc', net_http_res, @request)
+          allow(@request).to receive(:max_redirects).and_return(5)
+          expect { resp.return! }.to raise_error(exc)
         end
       end
     end
@@ -81,95 +96,146 @@ describe RestClient::Response do
     it "follows a redirection when the request is a get" do
       stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
       stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
+    end
+
+    it "keeps redirection history" do
+      stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
+      stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
+      r = RestClient::Request.execute(url: 'http://some/resource', method: :get)
+      expect(r.body).to eq 'Foo'
+      expect(r.history.length).to eq 1
+      expect(r.history.fetch(0)).to be_a(RestClient::Response)
+      expect(r.history.fetch(0).code).to be 301
     end
 
     it "follows a redirection and keep the parameters" do
-      stub_request(:get, 'http://foo:bar@some/resource').with(:headers => {'Accept' => 'application/json'}).to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
-      stub_request(:get, 'http://foo:bar@new/resource').with(:headers => {'Accept' => 'application/json'}).to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :user => 'foo', :password => 'bar', :headers => {:accept => :json}).body.should eq 'Foo'
+      stub_request(:get, 'http://some/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
+      stub_request(:get, 'http://new/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => 'Foo')
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :user => 'foo', :password => 'bar', :headers => {:accept => :json}).body).to eq 'Foo'
     end
 
     it "follows a redirection and keep the cookies" do
       stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://some/new_resource', })
       stub_request(:get, 'http://some/new_resource').with(:headers => {'Cookie' => 'Foo=Bar'}).to_return(:body => 'Qux')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Qux'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Qux'
     end
 
-    it 'does not keep cookies across domains' do
-      stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://new/resource', })
-      stub_request(:get, 'http://new/resource').with(:headers => {'Cookie' => ''}).to_return(:body => 'Qux')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Qux'
+    it 'respects cookie domains on redirect' do
+      stub_request(:get, 'http://some.example.com/').to_return(:body => '', :status => 301,
+        :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://new.example.com/', })
+      stub_request(:get, 'http://new.example.com/').with(
+        :headers => {'Cookie' => 'passedthrough=1'}).to_return(:body => 'Qux')
+
+      expect(RestClient::Request.execute(:url => 'http://some.example.com/', :method => :get, cookies: [HTTP::Cookie.new('passedthrough', '1', domain: 'new.example.com', path: '/')]).body).to eq 'Qux'
     end
 
     it "doesn't follow a 301 when the request is a post" do
-      net_http_res = double('net http response', :code => 301)
-      response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
-      lambda { response.return!(@request)}.should raise_error(RestClient::MovedPermanently)
+      net_http_res = response_double(:code => 301)
+
+      response = RestClient::Response.create('abc', net_http_res,
+                                             request_double(method: 'post'))
+      expect {
+        response.return!
+      }.to raise_error(RestClient::MovedPermanently)
     end
 
     it "doesn't follow a 302 when the request is a post" do
-      net_http_res = double('net http response', :code => 302)
-      response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
-      lambda { response.return!(@request)}.should raise_error(RestClient::Found)
+      net_http_res = response_double(:code => 302)
+      response = RestClient::Response.create('abc', net_http_res,
+                                             request_double(method: 'post'))
+      expect {
+        response.return!
+      }.to raise_error(RestClient::Found)
     end
 
     it "doesn't follow a 307 when the request is a post" do
-      net_http_res = double('net http response', :code => 307)
-      response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
-      lambda { response.return!(@request)}.should raise_error(RestClient::TemporaryRedirect)
+      net_http_res = response_double(:code => 307)
+      response = RestClient::Response.create('abc', net_http_res,
+                                             request_double(method: 'post'))
+      expect(response).not_to receive(:follow_redirection)
+      expect {
+        response.return!
+      }.to raise_error(RestClient::TemporaryRedirect)
     end
 
     it "doesn't follow a redirection when the request is a put" do
-      net_http_res = double('net http response', :code => 301)
-      response = RestClient::Response.create('abc', net_http_res, {:method => :put}, @request)
-      lambda { response.return!(@request)}.should raise_error(RestClient::MovedPermanently)
+      net_http_res = response_double(:code => 301)
+      response = RestClient::Response.create('abc', net_http_res,
+                                             request_double(method: 'put'))
+      expect {
+        response.return!
+      }.to raise_error(RestClient::MovedPermanently)
     end
 
     it "follows a redirection when the request is a post and result is a 303" do
       stub_request(:put, 'http://some/resource').to_return(:body => '', :status => 303, :headers => {'Location' => 'http://new/resource'})
       stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body).to eq 'Foo'
     end
 
     it "follows a redirection when the request is a head" do
       stub_request(:head, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
       stub_request(:head, 'http://new/resource').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body).to eq 'Foo'
     end
 
     it "handles redirects with relative paths" do
       stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index'})
       stub_request(:get, 'http://some/index').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
     end
 
     it "handles redirects with relative path and query string" do
       stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index?q=1'})
       stub_request(:get, 'http://some/index?q=1').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
     end
 
     it "follow a redirection when the request is a get and the response is in the 30x range" do
       stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
       stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
-      RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+      expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
     end
 
     it "follows no more than 10 redirections before raising error" do
       stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
       stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
-      lambda { RestClient::Request.execute(:url => 'http://some/redirect-1', :method => :get) }.should raise_error(RestClient::MaxRedirectsReached)
-      WebMock.should have_requested(:get, 'http://some/redirect-2').times(10)
+      expect {
+        RestClient::Request.execute(url: 'http://some/redirect-1', method: :get)
+      }.to raise_error(RestClient::MovedPermanently) { |ex|
+        ex.response.history.each {|r| expect(r).to be_a(RestClient::Response) }
+        expect(ex.response.history.length).to eq 10
+      }
+      expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(10)
     end
 
     it "follows no more than max_redirects redirections, if specified" do
       stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
       stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
-      lambda { RestClient::Request.execute(:url => 'http://some/redirect-1', :method => :get, :max_redirects => 5) }.should raise_error(RestClient::MaxRedirectsReached)
-      WebMock.should have_requested(:get, 'http://some/redirect-2').times(5)
+      expect {
+        RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 5)
+      }.to raise_error(RestClient::MovedPermanently) { |ex|
+        expect(ex.response.history.length).to eq 5
+      }
+      expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(5)
+    end
+
+    it "allows for manual following of redirects" do
+      stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/resource'})
+      stub_request(:get, 'http://some/resource').to_return(:body => 'Qux', :status => 200)
+
+      begin
+        RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 0)
+      rescue RestClient::MovedPermanently => err
+        resp = err.response.follow_redirection
+      else
+        raise 'notreached'
+      end
+
+      expect(resp.code).to eq 200
+      expect(resp.body).to eq 'Qux'
     end
   end
 
-
 end
diff --git a/spec/unit/restclient_spec.rb b/spec/unit/restclient_spec.rb
index 190d1bf..cb4dfe0 100644
--- a/spec/unit/restclient_spec.rb
+++ b/spec/unit/restclient_spec.rb
@@ -1,39 +1,39 @@
-require 'spec_helper'
+require_relative '_lib'
 
 describe RestClient do
   describe "API" do
     it "GET" do
-      RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
       RestClient.get('http://some/resource')
     end
 
     it "POST" do
-      RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
       RestClient.post('http://some/resource', 'payload')
     end
 
     it "PUT" do
-      RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
       RestClient.put('http://some/resource', 'payload')
     end
 
     it "PATCH" do
-      RestClient::Request.should_receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'payload', :headers => {})
       RestClient.patch('http://some/resource', 'payload')
     end
 
     it "DELETE" do
-      RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
       RestClient.delete('http://some/resource')
     end
 
     it "HEAD" do
-      RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
       RestClient.head('http://some/resource')
     end
 
     it "OPTIONS" do
-      RestClient::Request.should_receive(:execute).with(:method => :options, :url => 'http://some/resource', :headers => {})
+      expect(RestClient::Request).to receive(:execute).with(:method => :options, :url => 'http://some/resource', :headers => {})
       RestClient.options('http://some/resource')
     end
   end
@@ -45,35 +45,35 @@ describe RestClient do
 
     it "uses << if the log is not a string" do
       log = RestClient.log = []
-      log.should_receive(:<<).with('xyz')
+      expect(log).to receive(:<<).with('xyz')
       RestClient.log << 'xyz'
     end
 
     it "displays the log to stdout" do
       RestClient.log = 'stdout'
-      STDOUT.should_receive(:puts).with('xyz')
+      expect(STDOUT).to receive(:puts).with('xyz')
       RestClient.log << 'xyz'
     end
 
     it "displays the log to stderr" do
       RestClient.log = 'stderr'
-      STDERR.should_receive(:puts).with('xyz')
+      expect(STDERR).to receive(:puts).with('xyz')
       RestClient.log << 'xyz'
     end
 
     it "append the log to the requested filename" do
       RestClient.log = '/tmp/restclient.log'
       f = double('file handle')
-      File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
-      f.should_receive(:puts).with('xyz')
+      expect(File).to receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
+      expect(f).to receive(:puts).with('xyz')
       RestClient.log << 'xyz'
     end
   end
 
   describe 'version' do
-    it 'has a version ~> 1.8.0.alpha' do
+    it 'has a version ~> 2.0.0.alpha' do
       ver = Gem::Version.new(RestClient.version)
-      Gem::Requirement.new('~> 1.8.0.alpha').should be_satisfied_by(ver)
+      expect(Gem::Requirement.new('~> 2.0.0.alpha')).to be_satisfied_by(ver)
     end
   end
 end
diff --git a/spec/unit/utils_spec.rb b/spec/unit/utils_spec.rb
new file mode 100644
index 0000000..9c90ec0
--- /dev/null
+++ b/spec/unit/utils_spec.rb
@@ -0,0 +1,147 @@
+require_relative '_lib'
+
+describe RestClient::Utils do
+  describe '.get_encoding_from_headers' do
+    it 'assumes no encoding by default for text' do
+      headers = {:content_type => 'text/plain'}
+      expect(RestClient::Utils.get_encoding_from_headers(headers)).
+        to eq nil
+    end
+
+    it 'returns nil on failures' do
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'blah'})).to eq nil
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {})).to eq nil
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'foo; bar=baz'})).to eq nil
+    end
+
+    it 'handles various charsets' do
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'text/plain; charset=UTF-8'})).to eq 'UTF-8'
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'application/json; charset=ISO-8859-1'})).
+        to eq 'ISO-8859-1'
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'text/html; charset=windows-1251'})).
+        to eq 'windows-1251'
+
+      expect(RestClient::Utils.get_encoding_from_headers(
+        {:content_type => 'text/html; charset="UTF-16"'})).
+        to eq 'UTF-16'
+    end
+  end
+
+  describe '.cgi_parse_header' do
+    it 'parses headers' do
+      expect(RestClient::Utils.cgi_parse_header('text/plain')).
+        to eq ['text/plain', {}]
+
+      expect(RestClient::Utils.cgi_parse_header('text/vnd.just.made.this.up ; ')).
+        to eq ['text/vnd.just.made.this.up', {}]
+
+      expect(RestClient::Utils.cgi_parse_header('text/plain;charset=us-ascii')).
+        to eq ['text/plain', {'charset' => 'us-ascii'}]
+
+      expect(RestClient::Utils.cgi_parse_header('text/plain ; charset="us-ascii"')).
+        to eq ['text/plain', {'charset' => 'us-ascii'}]
+
+      expect(RestClient::Utils.cgi_parse_header(
+        'text/plain ; charset="us-ascii"; another=opt')).
+        to eq ['text/plain', {'charset' => 'us-ascii', 'another' => 'opt'}]
+
+      expect(RestClient::Utils.cgi_parse_header(
+        'attachment; filename="silly.txt"')).
+        to eq ['attachment', {'filename' => 'silly.txt'}]
+
+      expect(RestClient::Utils.cgi_parse_header(
+        'attachment; filename="strange;name"')).
+        to eq ['attachment', {'filename' => 'strange;name'}]
+
+      expect(RestClient::Utils.cgi_parse_header(
+        'attachment; filename="strange;name";size=123;')).to eq \
+        ['attachment', {'filename' => 'strange;name', 'size' => '123'}]
+
+      expect(RestClient::Utils.cgi_parse_header(
+        'form-data; name="files"; filename="fo\\"o;bar"')).to eq \
+        ['form-data', {'name' => 'files', 'filename' => 'fo"o;bar'}]
+    end
+  end
+
+  describe '.encode_query_string' do
+    it 'handles simple hashes' do
+      {
+        {foo: 123, bar: 456} => 'foo=123&bar=456',
+        {'foo' => 123, 'bar' => 456} => 'foo=123&bar=456',
+        {foo: 'abc', bar: 'one two'} => 'foo=abc&bar=one+two',
+        {escaped: '1+2=3'} => 'escaped=1%2B2%3D3',
+        {'escaped + key' => 'foo'} => 'escaped+%2B+key=foo',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles simple arrays' do
+      {
+        {foo: [1, 2, 3]} => 'foo[]=1&foo[]=2&foo[]=3',
+        {foo: %w{a b c}, bar: [1, 2, 3]} => 'foo[]=a&foo[]=b&foo[]=c&bar[]=1&bar[]=2&bar[]=3',
+        {foo: ['one two', 3]} => 'foo[]=one+two&foo[]=3',
+        {'a+b' => [1,2,3]} => 'a%2Bb[]=1&a%2Bb[]=2&a%2Bb[]=3',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles nested hashes' do
+      {
+        {outer: {foo: 123, bar: 456}} => 'outer[foo]=123&outer[bar]=456',
+        {outer: {foo: [1, 2, 3], bar: 'baz'}} => 'outer[foo][]=1&outer[foo][]=2&outer[foo][]=3&outer[bar]=baz',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles null and empty values' do
+      {
+        {string: '', empty: nil, list: [], hash: {}, falsey: false } =>
+          'string=&empty&list&hash&falsey=false',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles nested nulls' do
+      {
+        {foo: {string: '', empty: nil}} => 'foo[string]=&foo[empty]',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles deep nesting' do
+      {
+        {coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]} => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles multiple fields with the same name using ParamsArray' do
+      {
+        RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]) => 'foo=1&foo=2&foo=3',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+
+    it 'handles nested ParamsArrays' do
+      {
+        {foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])} => 'foo[a]=1&foo[a]=2',
+        RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]) => 'foo[a]=1&foo[a]=2',
+      }.each_pair do |input, expected|
+        expect(RestClient::Utils.encode_query_string(input)).to eq expected
+      end
+    end
+  end
+end
diff --git a/spec/unit/windows/root_certs_spec.rb b/spec/unit/windows/root_certs_spec.rb
index 12d68ef..6229333 100644
--- a/spec/unit/windows/root_certs_spec.rb
+++ b/spec/unit/windows/root_certs_spec.rb
@@ -1,11 +1,11 @@
-require 'spec_helper'
+require_relative '../_lib'
 
 describe 'RestClient::Windows::RootCerts',
          :if => RestClient::Platform.windows? do
   let(:x509_store) { RestClient::Windows::RootCerts.instance.to_a }
 
   it 'should return at least one X509 certificate' do
-    expect(x509_store.to_a).to have_at_least(1).items
+    expect(x509_store.to_a.size).to be >= 1
   end
 
   it 'should return an X509 certificate with a subject' do
@@ -16,7 +16,7 @@ describe 'RestClient::Windows::RootCerts',
 
   it 'should return X509 certificate objects' do
     x509_store.each do |cert|
-      cert.should be_a(OpenSSL::X509::Certificate)
+      expect(cert).to be_a(OpenSSL::X509::Certificate)
     end
   end
 end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-rest-client.git



More information about the Pkg-ruby-extras-commits mailing list