Expose HTTP status code & body in errors

This commit is contained in:
Steven Bazyl 2015-07-20 12:36:13 -07:00
parent 810cfa4b5d
commit 2a9fd28176
5 changed files with 151 additions and 14 deletions

View File

@ -0,0 +1,93 @@
# Copyright 2010 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.
module Google
class APIClient
##
# Helper for loading keys from the PKCS12 files downloaded when
# setting up service accounts at the APIs Console.
#
module KeyUtils
##
# Loads a key from PKCS12 file, assuming a single private key
# is present.
#
# @param [String] keyfile
# Path of the PKCS12 file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_from_pkcs12(keyfile, passphrase)
load_key(keyfile, passphrase) do |content, pass_phrase|
OpenSSL::PKCS12.new(content, pass_phrase).key
end
end
##
# Loads a key from a PEM file.
#
# @param [String] keyfile
# Path of the PEM file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
#
def self.load_from_pem(keyfile, passphrase)
load_key(keyfile, passphrase) do | content, pass_phrase|
OpenSSL::PKey::RSA.new(content, pass_phrase)
end
end
private
##
# Helper for loading keys from file or memory. Accepts a block
# to handle the specific file format.
#
# @param [String] keyfile
# Path of thefile to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @yield [String, String]
# Key file & passphrase to extract key from
# @yieldparam [String] keyfile
# Contents of the file
# @yieldparam [String] passphrase
# Passphrase to unlock key
# @yieldreturn [OpenSSL::PKey]
# Private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_key(keyfile, passphrase, &block)
begin
begin
content = File.open(keyfile, 'rb') { |io| io.read }
rescue
content = keyfile
end
block.call(content, passphrase)
rescue OpenSSL::OpenSSLError
raise ArgumentError.new("Invalid keyfile or passphrase")
end
end
end
end
end

View File

@ -79,23 +79,30 @@ module Google
#
# @param [Fixnum] status
# HTTP status code of response
# @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, body = nil)
def check_status(status, header = nil, body = nil, message = nil)
case status
when 400, 402...500
error = parse_error(body)
if error
logger.debug { error }
fail Google::Apis::RateLimitError, error if RATE_LIMIT_ERRORS.include?(error['reason'])
message = error['reason'] if error.has_key?('reason')
raise Google::Apis::RateLimitError.new(message,
status_code: status,
header: header,
body: body) if RATE_LIMIT_ERRORS.include?(message)
end
super(status, error)
super(status, header, body, message)
else
super(status, body)
super(status, header, body, message)
end
end

View File

@ -159,7 +159,7 @@ module Google
# @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, body)
check_status(status, header, body)
decode_response_body(header[:content_type], body)
end
@ -167,28 +167,38 @@ module Google
#
# @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, body = nil)
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
fail Google::Apis::RedirectError, header[:location]
message ||= sprintf('Redirect to %s', header[:location])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401
fail Google::Apis::AuthorizationError, body
message ||= 'Unauthorized'
raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
when 304, 400, 402...500
fail Google::Apis::ClientError, body
message ||= 'Invalid request'
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
when 500...600
fail Google::Apis::ServerError, body
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))
fail Google::Apis::TransmissionError, body
message ||= 'Unknown error'
raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
end
end

View File

@ -16,7 +16,11 @@ module Google
module Apis
# Base error, capable of wrapping another
class Error < StandardError
def initialize(err)
attr_reader :status_code
attr_reader :header
attr_reader :body
def initialize(err, status_code: nil, header: nil, body: nil)
@cause = nil
if err.respond_to?(:backtrace)
@ -25,6 +29,9 @@ module Google
else
super(err.to_s)
end
@status_code = status_code
@header = header.dup unless header.nil?
@body = body
end
def backtrace
@ -35,7 +42,7 @@ module Google
end
end
end
# An error which is raised when there is an unexpected response or other
# transport error that prevents an operation from succeeding.
class TransmissionError < Error

View File

@ -123,7 +123,27 @@ RSpec.describe Google::Apis::Core::HttpCommand do
command.options.retries = 1
expect { command.execute(client) }.to raise_error(Google::Apis::ServerError)
end
context('with retries exceeded') do
before(:example) do
command.options.retries = 1
end
let(:err) do
begin
command.execute(client)
rescue Google::Apis::Error => e
e
end
end
it 'should raise error with HTTP status code' do
expect(err.status_code).to eq 500
end
end
context('with callbacks') do
it 'should return the response body after retries' do
expect { |b| command.execute(client, &b) }.to yield_successive_args(