google-api-ruby-client/lib/google/apis/core/http_command.rb

333 lines
12 KiB
Ruby

# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'addressable/uri'
require 'addressable/template'
require 'google/apis/options'
require 'google/apis/errors'
require 'retriable'
require 'hurley'
require 'hurley/addressable'
require 'hurley_patches'
require 'google/apis/core/logging'
require 'pp'
module Google
module Apis
module Core
# Command for HTTP request/response.
class HttpCommand
include Logging
RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError]
# Request options
# @return [Google::Apis::RequestOptions]
attr_accessor :options
# HTTP request URL
# @return [String, Addressable::URI]
attr_accessor :url
# HTTP headers
# @return [Hurley::Header]
attr_accessor :header
# Request body
# @return [#read]
attr_accessor :body
# HTTP method
# @return [symbol]
attr_accessor :method
# HTTP Client
# @return [Hurley::Client]
attr_accessor :connection
# Query params
# @return [Hash]
attr_accessor :query
# Path params for URL Template
# @return [Hash]
attr_accessor :params
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
# @param [String, #read] body
# Request body
def initialize(method, url, body: nil)
self.options = Google::Apis::RequestOptions.default.dup
self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method
self.header = Hurley::Header.new
self.body = body
self.query = {}
self.params = {}
end
# Execute the command, retrying as necessary
#
# @param [Hurley::Client] client
# HTTP client
# @yield [result, err] Result or error if block supplied
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute(client)
prepare!
begin
Retriable.retriable tries: options.retries + 1,
base_interval: 1,
multiplier: 2,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError],
on_retry: proc { |*| refresh_authorization } do
execute_once(client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
ensure
release!
end
# Refresh the authorization authorization after a 401 error
#
# @private
# @return [void]
def refresh_authorization
# Handled implicitly by auth lib, here in case need to override
logger.debug('Retrying after authentication failure')
end
# Check if attached credentials can be automatically refreshed
# @return [Boolean]
def authorization_refreshable?
options.authorization.respond_to?(:apply!)
end
# Prepare the request (e.g. calculate headers, serialize data, etc) before sending
#
# @private
# @return [void]
def prepare!
header.update(options.header) if options && options.header
self.url = url.expand(params) if url.is_a?(Addressable::Template)
url.query_values = query.merge(url.query_values || {})
if allow_form_encoding?
@form_encoded = true
self.body = Addressable::URI.form_encode(url.query_values(Array))
self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
self.url.query_values = {}
else
@form_encoded = false
end
end
# Release any resources used by this command
# @private
# @return [void]
def release!
end
# Check the response and either decode body or raise error
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hurley::Header] header
# Response headers
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body)
check_status(status, header, body)
decode_response_body(header[:content_type], body)
end
# Check the response and raise error if needed
#
# @param [Fixnum] status
# HTTP status code of response
# @param
# @param [Hurley::Header] header
# HTTP response headers
# @param [String] body
# HTTP response body
# @param [String] message
# Error message text
# @return [void]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def check_status(status, header = nil, body = nil, message = nil)
# TODO: 304 Not Modified depends on context...
case status
when 200...300
nil
when 301, 302, 303, 307
message ||= sprintf('Redirect to %s', header[:location])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401
message ||= 'Unauthorized'
raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
when 304, 400, 402...500
message ||= 'Invalid request'
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
when 500...600
message ||= 'Server error'
raise Google::Apis::ServerError.new(message, status_code: status, header: header, body: body)
else
logger.warn(sprintf('Encountered unexpected status code %s', status))
message ||= 'Unknown error'
raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
end
end
# Process the actual response body. Intended to be overridden by subclasses
#
# @param [String] _content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
def decode_response_body(_content_type, body)
body
end
# Process a success response
# @param [Object] result
# Result object
# @return [Object] result if no block given
# @yield [result, nil] if block given
def success(result, &block)
logger.debug { sprintf('Success - %s', PP.pp(result, '')) }
block.call(result, nil) if block_given?
result
end
# Process an error response
# @param [StandardError] err
# Error object
# @param [Boolean] rethrow
# True if error should be raised again after handling
# @return [void]
# @yield [nil, err] if block given
# @raise [StandardError] if no block
def error(err, rethrow: false, &block)
logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
err = Google::Apis::TransmissionError.new(err) if err.is_a?(Hurley::ClientError) || err.is_a?(SocketError)
block.call(nil, err) if block_given?
fail err if rethrow || block.nil?
end
# Execute the command once.
#
# @private
# @param [Hurley::Client] client
# HTTP client
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client)
body.rewind if body.respond_to?(:rewind)
begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
response = client.send(method, url, body) do |req|
# Temporary workaround for Hurley bug where the connection preference
# is ignored and it uses nested anyway
unless form_encoded?
req.url.query_class = Hurley::Query::Flat
query.each do | k, v|
req.url.query[k] = normalize_query_value(v)
end
end
# End workaround
apply_request_options(req)
end
logger.debug { response.status_code }
logger.debug { response.inspect }
response = process_response(response.status_code, response.header, response.body)
success(response)
rescue => e
logger.debug { sprintf('Caught error %s', e) }
error(e, rethrow: true)
end
end
# Update the request with any specified options.
# @param [Hurley::Request] req
# HTTP request
# @return [void]
def apply_request_options(req)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req.header)
elsif options.authorization.is_a?(String)
req.header[:authorization] = sprintf('Bearer %s', options.authorization)
end
req.header.update(header)
req.options.timeout = options.timeout_sec
req.options.open_timeout = options.open_timeout_sec
end
def allow_form_encoding?
[:post, :put].include?(method) && body.nil?
end
private
def form_encoded?
@form_encoded
end
def normalize_query_value(v)
case v
when Array
v.map { |v2| normalize_query_value(v2) }
when nil
nil
else
v.to_s
end
end
end
end
end
end