From 2a9fd28176e3d27698520d2d5a8cc0d8b3367732 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 20 Jul 2015 12:36:13 -0700 Subject: [PATCH] Expose HTTP status code & body in errors --- lib/google/api_client/auth/key_utils.rb | 93 ++++++++++++++++++++++ lib/google/apis/core/api_command.rb | 17 ++-- lib/google/apis/core/http_command.rb | 24 ++++-- lib/google/apis/errors.rb | 11 ++- spec/google/apis/core/http_command_spec.rb | 20 +++++ 5 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 lib/google/api_client/auth/key_utils.rb diff --git a/lib/google/api_client/auth/key_utils.rb b/lib/google/api_client/auth/key_utils.rb new file mode 100644 index 000000000..6b6e0cfe5 --- /dev/null +++ b/lib/google/api_client/auth/key_utils.rb @@ -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 diff --git a/lib/google/apis/core/api_command.rb b/lib/google/apis/core/api_command.rb index 9ee2604f0..455bc90d7 100644 --- a/lib/google/apis/core/api_command.rb +++ b/lib/google/apis/core/api_command.rb @@ -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 diff --git a/lib/google/apis/core/http_command.rb b/lib/google/apis/core/http_command.rb index e0b652066..21cf4187c 100644 --- a/lib/google/apis/core/http_command.rb +++ b/lib/google/apis/core/http_command.rb @@ -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 diff --git a/lib/google/apis/errors.rb b/lib/google/apis/errors.rb index 33e20bb9a..96c5a4056 100644 --- a/lib/google/apis/errors.rb +++ b/lib/google/apis/errors.rb @@ -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 diff --git a/spec/google/apis/core/http_command_spec.rb b/spec/google/apis/core/http_command_spec.rb index d9b14cc7d..09c25c266 100644 --- a/spec/google/apis/core/http_command_spec.rb +++ b/spec/google/apis/core/http_command_spec.rb @@ -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(