Merge pull request #20 from simplymeasured/feature/make-autorefresh-of-token-optional
[arvados.git] / lib / google / api_client.rb
index ceb18edb8e090ca35f442aa5a1ad54358ca57459..4c9ee03c7a9a7b1657f5f0418f884512b5f3326a 100644 (file)
 # limitations under the License.
 
 
-require 'httpadapter'
-require 'json'
+require 'faraday'
+require 'faraday/utils'
+require 'multi_json'
+require 'compat/multi_json'
 require 'stringio'
 
+require 'google/api_client/version'
+require 'google/api_client/logging'
 require 'google/api_client/errors'
 require 'google/api_client/environment'
 require 'google/api_client/discovery'
+require 'google/api_client/request'
 require 'google/api_client/reference'
 require 'google/api_client/result'
+require 'google/api_client/media'
+require 'google/api_client/service_account'
+require 'google/api_client/batch'
+require 'google/api_client/railtie' if defined?(Rails)
 
 module Google
-  # TODO(bobaman): Document all this stuff.
-
 
   ##
   # This class manages APIs communication.
   class APIClient
+    include Google::APIClient::Logging
+    
     ##
     # Creates a new Google API client.
     #
@@ -43,48 +52,62 @@ module Google
     #     <li><code>:oauth_1</code></li>
     #     <li><code>:oauth_2</code></li>
     #   </ul>
-    # @option options [String] :host ("www.googleapis.com")
-    #   The API hostname used by the client.  This rarely needs to be changed.
+    # @option options [Boolean] :auto_refresh_token (true)
+    #   The setting that controls whether or not the api client attempts to
+    #   refresh authorization when a 401 is hit in #execute. If the token does 
+    #   not support it, this option is ignored.
     # @option options [String] :application_name
-    #   The name and version of the application using the client. This should
-    #   be given in the form `"{name}/{version}"`.
+    #   The name of the application using the client.
+    # @option options [String] :application_version
+    #   The version number of the application using the client.
     # @option options [String] :user_agent
     #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
     #   The user agent used by the client.  Most developers will want to
     #   leave this value alone and use the `:application_name` option instead.
+    # @option options [String] :host ("www.googleapis.com")
+    #   The API hostname used by the client. This rarely needs to be changed.
+    # @option options [String] :port (443)
+    #   The port number used by the client. This rarely needs to be changed.
+    # @option options [String] :discovery_path ("/discovery/v1")
+    #   The discovery base path. This rarely needs to be changed.
     def initialize(options={})
+      logger.debug { "#{self.class} - Initializing client with options #{options}" }
+      
       # Normalize key to String to allow indifferent access.
       options = options.inject({}) do |accu, (key, value)|
-        accu[key.to_s] = value
+        accu[key.to_sym] = value
         accu
       end
       # Almost all API usage will have a host of 'www.googleapis.com'.
-      self.host = options["host"] || 'www.googleapis.com'
+      self.host = options[:host] || 'www.googleapis.com'
+      self.port = options[:port] || 443
+      self.discovery_path = options[:discovery_path] || '/discovery/v1'
+
       # Most developers will want to leave this value alone and use the
       # application_name option.
-      self.user_agent = options["user_agent"] || (
-        (options["application_name"] || '')
-        'google-api-ruby-client/' + VERSION::STRING +
-        ' ' + ENV::OS_VERSION
+      if options[:application_name]
+        app_name = options[:application_name]
+        app_version = options[:application_version]
+        application_string = "#{app_name}/#{app_version || '0.0.0'}"
+      else
+        logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
+      end
+      self.user_agent = options[:user_agent] || (
+        "#{application_string} " +
+        "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} " +
+         ENV::OS_VERSION
       ).strip
       # The writer method understands a few Symbols and will generate useful
       # default authentication mechanisms.
-      self.authorization = options["authorization"] || :oauth_2
-      self.key = options["key"]
-      self.user_ip = options["user_ip"]
-      # The HTTP adapter controls all of the HTTP traffic the client generates.
-      # By default, Net::HTTP is used, but adding support for other clients
-      # is trivial.
-      if options["http_adapter"]
-        self.http_adapter = options["http_adapter"]
-      else
-        require 'httpadapter/adapters/net_http'
-        # NOTE: Do not rely on this default value, as it may change
-        self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
-      end
+      self.authorization =
+        options.key?(:authorization) ? options[:authorization] : :oauth_2
+      self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
+      self.key = options[:key]
+      self.user_ip = options[:user_ip]
       @discovery_uris = {}
       @discovery_documents = {}
       @discovered_apis = {}
+            
       return self
     end
 
@@ -144,10 +167,17 @@ module Google
       return @authorization
     end
 
+    ##
+    # The setting that controls whether or not the api client attempts to
+    # refresh authorization when a 401 is hit in #execute. 
+    #
+    # @return [Boolean]
+    attr_accessor :auto_refresh_token
+
     ##
     # The application's API key issued by the API console.
     #
-    # @return [String] The API key..
+    # @return [String] The API key.
     attr_accessor :key
 
     ##
@@ -157,58 +187,47 @@ module Google
     attr_accessor :user_ip
 
     ##
-    # Returns the HTTP adapter used by the client.
+    # The user agent used by the client.
     #
-    # @return [HTTPAdapter]
-    #   The HTTP adapter object.  The object must include the
-    #   HTTPAdapter module and conform to its interface.
-    attr_reader :http_adapter
-
-    ##
-    # Returns the HTTP adapter used by the client.
-    #
-    # @return [HTTPAdapter]
-    #   The HTTP adapter object.  The object must include the
-    #   HTTPAdapter module and conform to its interface.
-    def http_adapter=(new_http_adapter)
-      if new_http_adapter.kind_of?(HTTPAdapter)
-        @http_adapter = new_http_adapter
-      else
-        raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
-      end
-    end
+    # @return [String]
+    #   The user agent string used in the User-Agent header.
+    attr_accessor :user_agent
 
     ##
     # The API hostname used by the client.
     #
     # @return [String]
-    #   The API hostname.  Should almost always be 'www.googleapis.com'.
+    #   The API hostname. Should almost always be 'www.googleapis.com'.
     attr_accessor :host
 
     ##
-    # The user agent used by the client.
+    # The port number used by the client.
     #
     # @return [String]
-    #   The user agent string used in the User-Agent header.
-    attr_accessor :user_agent
+    #   The port number. Should almost always be 443.
+    attr_accessor :port
+
+    ##
+    # The base path used by the client for discovery.
+    #
+    # @return [String]
+    #   The base path. Should almost always be '/discovery/v1'.
+    attr_accessor :discovery_path
 
     ##
     # Returns the URI for the directory document.
     #
     # @return [Addressable::URI] The URI of the directory document.
     def directory_uri
-      template = Addressable::Template.new(
-        "https://{host}/discovery/v1/apis"
-      )
-      return template.expand({"host" => self.host})
+      return resolve_uri(self.discovery_path + '/apis')
     end
 
     ##
     # Manually registers a URI as a discovery document for a specific version
     # of an API.
     #
-    # @param [String, Symbol] api The service name.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @param [Addressable::URI] uri The URI of the discovery document.
     def register_discovery_uri(api, version, uri)
       api = api.to_s
@@ -219,31 +238,27 @@ module Google
     ##
     # Returns the URI for the discovery document.
     #
-    # @param [String, Symbol] api The service name.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @return [Addressable::URI] The URI of the discovery document.
     def discovery_uri(api, version=nil)
       api = api.to_s
       version = version || 'v1'
-      return @discovery_uris["#{api}:#{version}"] ||= (begin
-        template = Addressable::Template.new(
-          "https://{host}/discovery/v1/apis/" +
-          "{api}/{version}/rest"
+      return @discovery_uris["#{api}:#{version}"] ||= (
+        resolve_uri(
+          self.discovery_path + '/apis/{api}/{version}/rest',
+          'api' => api,
+          'version' => version
         )
-        template.expand({
-          "host" => self.host,
-          "api" => api,
-          "version" => version
-        })
-      end)
+      )
     end
 
     ##
     # Manually registers a pre-loaded discovery document for a specific version
     # of an API.
     #
-    # @param [String, Symbol] api The service name.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @param [String, StringIO] discovery_document
     #   The contents of the discovery document.
     def register_discovery_document(api, version, discovery_document)
@@ -259,7 +274,7 @@ module Google
           "Expected String or StringIO, got #{discovery_document.class}."
       end
       @discovery_documents["#{api}:#{version}"] =
-        ::JSON.parse(discovery_document)
+        MultiJson.load(discovery_document)
     end
 
     ##
@@ -268,73 +283,31 @@ module Google
     # @return [Hash] The parsed JSON from the directory document.
     def directory_document
       return @directory_document ||= (begin
-        request = self.generate_request(
-          :http_method => 'GET',
+        response = self.execute!(
+          :http_method => :get,
           :uri => self.directory_uri,
           :authenticated => false
         )
-        response = self.transmit(request)
-        status, headers, body = response
-        if status >= 200 && status < 300
-          # TODO(bobaman) Better status code handling?
-          merged_body = body.inject(StringIO.new) do |accu, chunk|
-            accu.write(chunk)
-            accu
-          end
-          ::JSON.parse(merged_body.string)
-        elsif status >= 400 && status < 500
-          _, request_uri, _, _ = request
-          raise ClientError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        elsif status >= 500 && status < 600
-          _, request_uri, _, _ = request
-          raise ServerError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        elsif status > 600
-          _, request_uri, _, _ = request
-          raise TransmissionError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        end
+        response.data
       end)
     end
 
     ##
     # Returns the parsed discovery document.
     #
-    # @param [String, Symbol] api The service name.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @return [Hash] The parsed JSON from the discovery document.
     def discovery_document(api, version=nil)
       api = api.to_s
       version = version || 'v1'
       return @discovery_documents["#{api}:#{version}"] ||= (begin
-        request = self.generate_request(
-          :http_method => 'GET',
+        response = self.execute!(
+          :http_method => :get,
           :uri => self.discovery_uri(api, version),
           :authenticated => false
         )
-        response = self.transmit(request)
-        status, headers, body = response
-        if status >= 200 && status < 300
-          # TODO(bobaman) Better status code handling?
-          merged_body = body.inject(StringIO.new) do |accu, chunk|
-            accu.write(chunk)
-            accu
-          end
-          ::JSON.parse(merged_body.string)
-        elsif status >= 400 && status < 500
-          _, request_uri, _, _ = request
-          raise ClientError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        elsif status >= 500 && status < 600
-          _, request_uri, _, _ = request
-          raise ServerError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        elsif status > 600
-          _, request_uri, _, _ = request
-          raise TransmissionError,
-            "Could not retrieve discovery document at: #{request_uri}"
-        end
+        response.data
       end)
     end
 
@@ -361,8 +334,8 @@ module Google
     ##
     # Returns the service object for a given service name and service version.
     #
-    # @param [String, Symbol] api The service name.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     #
     # @return [Google::APIClient::API] The service object.
     def discovered_api(api, version=nil)
@@ -390,7 +363,8 @@ module Google
     # Returns the method object for a given RPC name and service version.
     #
     # @param [String, Symbol] rpc_name The RPC name of the desired method.
-    # @param [String] version The desired version of the service.
+    # @param [String, Symbol] api The API the method is within.
+    # @param [String] version The desired version of the API.
     #
     # @return [Google::APIClient::Method] The method object.
     def discovered_method(rpc_name, api, version=nil)
@@ -425,165 +399,179 @@ module Google
           "Expected String or Symbol, got #{api.class}."
       end
       api = api.to_s
-      # TODO(bobaman): Update to use directory API.
       return self.discovered_apis.detect do |a|
         a.name == api && a.preferred == true
       end
     end
 
+    ##
+    # Verifies an ID token against a server certificate. Used to ensure that
+    # an ID token supplied by an untrusted client-side mechanism is valid.
+    # Raises an error if the token is invalid or missing.
+    def verify_id_token!
+      require 'jwt'
+      require 'openssl'
+      @certificates ||= {}
+      if !self.authorization.respond_to?(:id_token)
+        raise ArgumentError, (
+          "Current authorization mechanism does not support ID tokens: " +
+          "#{self.authorization.class.to_s}"
+        )
+      elsif !self.authorization.id_token
+        raise ArgumentError, (
+          "Could not verify ID token, ID token missing. " +
+          "Scopes were: #{self.authorization.scope.inspect}"
+        )
+      else
+        check_cached_certs = lambda do
+          valid = false
+          for key, cert in @certificates
+            begin
+              self.authorization.decoded_id_token(cert.public_key)
+              valid = true
+            rescue JWT::DecodeError, Signet::UnsafeOperationError
+              # Expected exception. Ignore, ID token has not been validated.
+            end
+          end
+          valid
+        end
+        if check_cached_certs.call()
+          return true
+        end
+        response = self.execute!(
+          :http_method => :get,
+          :uri => 'https://www.googleapis.com/oauth2/v1/certs',
+          :authenticated => false
+        )
+        @certificates.merge!(
+          Hash[MultiJson.load(response.body).map do |key, cert|
+            [key, OpenSSL::X509::Certificate.new(cert)]
+          end]
+        )
+        if check_cached_certs.call()
+          return true
+        else
+          raise InvalidIDTokenError,
+            "Could not verify ID token against any available certificate."
+        end
+      end
+      return nil
+    end
+
     ##
     # Generates a request.
     #
-    # @param [Google::APIClient::Method, String] api_method
+    # @option options [Google::APIClient::Method] :api_method
     #   The method object or the RPC name of the method being executed.
-    # @param [Hash, Array] parameters
+    # @option options [Hash, Array] :parameters
     #   The parameters to send to the method.
-    # @param [String] body The body of the request.
-    # @param [Hash, Array] headers The HTTP headers for the request.
-    # @param [Hash] options
-    #   The configuration parameters for the request.
-    #   - <code>:version</code> — 
-    #     The service version.  Only used if <code>api_method</code> is a
-    #     <code>String</code>.  Defaults to <code>'v1'</code>.
-    #   - <code>:authorization</code> — 
-    #     The authorization mechanism for the response.  Used only if
-    #     <code>:authenticated</code> is <code>true</code>.
-    #   - <code>:authenticated</code> — 
-    #     <code>true</code> if the request must be signed or otherwise
-    #     authenticated, <code>false</code>
-    #     otherwise.  Defaults to <code>true</code> if an authorization
-    #     mechanism has been set, <code>false</code> otherwise.
-    #
-    # @return [Array] The generated request.
+    # @option options [Hash, Array] :headers The HTTP headers for the request.
+    # @option options [String] :body The body of the request.
+    # @option options [String] :version ("v1")
+    #   The service version. Only used if `api_method` is a `String`.
+    # @option options [#generate_authenticated_request] :authorization
+    #   The authorization mechanism for the response. Used only if
+    #   `:authenticated` is `true`.
+    # @option options [TrueClass, FalseClass] :authenticated (true)
+    #   `true` if the request must be signed or somehow
+    #   authenticated, `false` otherwise.
+    #
+    # @return [Google::APIClient::Reference] The generated request.
     #
     # @example
     #   request = client.generate_request(
-    #     :api_method => 'chili.activities.list',
+    #     :api_method => 'plus.activities.list',
     #     :parameters =>
-    #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #       {'collection' => 'public', 'userId' => 'me'}
     #   )
-    #   method, uri, headers, body = request
     def generate_request(options={})
-      # Note: The merge method on a Hash object will coerce an API Reference
-      # object into a Hash and merge with the default options.
-      options={
-        :version => 'v1',
-        :authorization => self.authorization,
-        :key => self.key,
-        :user_ip => self.user_ip
+      options = {
+        :api_client => self
       }.merge(options)
-      # The Reference object is going to need this to do method ID lookups.
-      options[:client] = self
-      # The default value for the :authenticated option depends on whether an
-      # authorization mechanism has been set.
-      if options[:authorization]
-        options = {:authenticated => true}.merge(options)
-      else
-        options = {:authenticated => false}.merge(options)
-      end
-      reference = Google::APIClient::Reference.new(options)
-      request = reference.to_request
-      if options[:authenticated]
-        request = self.generate_authenticated_request(:request => request)
-      end
-      return request
+      return Google::APIClient::Request.new(options)
     end
 
     ##
-    # Signs a request using the current authorization mechanism.
+    # Executes a request, wrapping it in a Result object.
     #
-    # @param [Hash] options The options to pass through.
+    # @param [Google::APIClient::Request, Hash, Array] params
+    #   Either a Google::APIClient::Request, a Hash, or an Array.
     #
-    # @return [Array] The signed or otherwise authenticated request.
-    def generate_authenticated_request(options={})
-      return authorization.generate_authenticated_request(options)
-    end
-
-    ##
-    # Transmits the request using the current HTTP adapter.
-    #
-    # @param [Array] request The request to transmit.
-    # @param [#transmit] adapter The HTTP adapter.
-    #
-    # @return [Array] The response from the server.
-    def transmit(request, adapter=self.http_adapter)
-      if self.user_agent != nil
-        # If there's no User-Agent header, set one.
-        method, uri, headers, body = request
-        unless headers.kind_of?(Enumerable)
-          # We need to use some Enumerable methods, relying on the presence of
-          # the #each method.
-          class <<headers
-            include Enumerable
-          end
-        end
-        if self.user_agent.kind_of?(String)
-          unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
-            headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
-          end
-        elsif self.user_agent != nil
-          raise TypeError,
-            "Expected User-Agent to be String, got #{self.user_agent.class}"
-        end
-      end
-      adapter.transmit([method, uri, headers, body])
-    end
-
-    ##
-    # Executes a request, wrapping it in a Result object.
+    #   If a Google::APIClient::Request, no other parameters are expected.
     #
-    # @param [Google::APIClient::Method, String] api_method
-    #   The method object or the RPC name of the method being executed.
-    # @param [Hash, Array] parameters
-    #   The parameters to send to the method.
-    # @param [String] body The body of the request.
-    # @param [Hash, Array] headers The HTTP headers for the request.
-    # @param [Hash] options
-    #   The configuration parameters for the request.
-    #   - <code>:version</code> — 
-    #     The service version.  Only used if <code>api_method</code> is a
-    #     <code>String</code>.  Defaults to <code>'v1'</code>.
-    #   - <code>:adapter</code> — 
-    #     The HTTP adapter.
-    #   - <code>:authorization</code> — 
-    #     The authorization mechanism for the response.  Used only if
-    #     <code>:authenticated</code> is <code>true</code>.
-    #   - <code>:authenticated</code> — 
-    #     <code>true</code> if the request must be signed or otherwise
-    #     authenticated, <code>false</code>
-    #     otherwise.  Defaults to <code>true</code>.
-    #
-    # @return [Array] The response from the API.
+    #   If a Hash, the below parameters are handled. If an Array, the
+    #   parameters are assumed to be in the below order:
+    #
+    #   - (Google::APIClient::Method) api_method:
+    #     The method object or the RPC name of the method being executed.
+    #   - (Hash, Array) parameters:
+    #     The parameters to send to the method.
+    #   - (String) body: The body of the request.
+    #   - (Hash, Array) headers: The HTTP headers for the request.
+    #   - (Hash) options: A set of options for the request, of which:
+    #     - (#generate_authenticated_request) :authorization (default: true) -
+    #       The authorization mechanism for the response. Used only if
+    #       `:authenticated` is `true`.
+    #     - (TrueClass, FalseClass) :authenticated (default: true) -
+    #       `true` if the request must be signed or somehow
+    #       authenticated, `false` otherwise.
+    #
+    # @return [Google::APIClient::Result] The result from the API, nil if batch.
     #
     # @example
-    #   request = client.generate_request(
-    #     :api_method => 'chili.activities.list',
-    #     :parameters =>
-    #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #   result = client.execute(batch_request)
+    #
+    # @example
+    #   plus = client.discovered_api('plus')
+    #   result = client.execute(
+    #     :api_method => plus.activities.list,
+    #     :parameters => {'collection' => 'public', 'userId' => 'me'}
     #   )
+    #
+    # @see Google::APIClient#generate_request
     def execute(*params)
-      # This block of code allows us to accept multiple parameter passing
-      # styles, and maintaining some backwards compatibility.
-      #
-      # Note: I'm extremely tempted to deprecate this style of execute call.
-      if params.last.respond_to?(:to_hash) && params.size == 1
-        options = params.pop
-      else
+      if params.last.kind_of?(Google::APIClient::Request) &&
+          params.size == 1
+        request = params.pop
         options = {}
+      else
+        # This block of code allows us to accept multiple parameter passing
+        # styles, and maintaining some backwards compatibility.
+        #
+        # Note: I'm extremely tempted to deprecate this style of execute call.
+        if params.last.respond_to?(:to_hash) && params.size == 1
+          options = params.pop
+        else
+          options = {}
+        end
+
+        options[:api_method] = params.shift if params.size > 0
+        options[:parameters] = params.shift if params.size > 0
+        options[:body] = params.shift if params.size > 0
+        options[:headers] = params.shift if params.size > 0
+        options.update(params.shift) if params.size > 0
+        request = self.generate_request(options)
       end
-      options[:api_method] = params.shift if params.size > 0
-      options[:parameters] = params.shift if params.size > 0
-      options[:merged_body] = params.shift if params.size > 0
-      options[:headers] = params.shift if params.size > 0
-      options[:client] = self
-
-      reference = Google::APIClient::Reference.new(options)
-      request = self.generate_request(reference)
-      response = self.transmit(
-        request,
-        options[:adapter] || self.http_adapter
-      )
-      return Google::APIClient::Result.new(reference, request, response)
+      
+      request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
+      request.parameters['key'] ||= self.key unless self.key.nil?
+      request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
+
+      connection = options[:connection] || Faraday.default_connection
+      request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
+
+      result = request.send(connection)
+      if result.status == 401 && authorization.respond_to?(:refresh_token) && auto_refresh_token
+        begin
+          logger.debug("Attempting refresh of access token & retry of request")
+          authorization.fetch_access_token!
+          result = request.send(connection)
+        rescue Signet::AuthorizationError
+           # Ignore since we want the original error
+        end
+      end
+      
+      return result
     end
 
     ##
@@ -593,27 +581,52 @@ module Google
     # @see Google::APIClient#execute
     def execute!(*params)
       result = self.execute(*params)
-      status, _, _ = result.response
-      if result.data.respond_to?(:error) &&
-          result.data.error.respond_to?(:message)
-        # You're going to get a terrible error message if the response isn't
-        # parsed successfully as an error.
-        error_message = result.data.error.message
-      elsif result.data['error'] && result.data['error']['message']
-        error_message = result.data['error']['message']
-      end
-      if status >= 400 && status < 500
-        raise ClientError,
-          error_message || "A client error has occurred."
-      elsif status >= 500 && status < 600
-        raise ServerError,
-          error_message || "A server error has occurred."
-      elsif status > 600
-        raise TransmissionError,
-          error_message || "A transmission error has occurred."
+      if result.error?
+        error_message = result.error_message
+        case result.response.status
+          when 400...500
+            exception_type = ClientError
+            error_message ||= "A client error has occurred."
+          when 500...600
+            exception_type = ServerError
+            error_message ||= "A server error has occurred."
+          else
+            exception_type = TransmissionError
+            error_message ||= "A transmission error has occurred."
+        end
+        raise exception_type, error_message
       end
       return result
     end
+    
+    protected
+    
+    ##
+    # Resolves a URI template against the client's configured base.
+    #
+    # @api private
+    # @param [String, Addressable::URI, Addressable::Template] template
+    #   The template to resolve.
+    # @param [Hash] mapping The mapping that corresponds to the template.
+    # @return [Addressable::URI] The expanded URI.
+    def resolve_uri(template, mapping={})
+      @base_uri ||= Addressable::URI.new(
+        :scheme => 'https',
+        :host => self.host,
+        :port => self.port
+      ).normalize
+      template = if template.kind_of?(Addressable::Template)
+        template.pattern
+      elsif template.respond_to?(:to_str)
+        template.to_str
+      else
+        raise TypeError,
+          "Expected String, Addressable::URI, or Addressable::Template, " +
+          "got #{template.class}."
+      end
+      return Addressable::Template.new(@base_uri + template).expand(mapping)
+    end
+    
   end
 end