Skip to content

Commit

Permalink
feat: parse details information from REST errors (#815)
Browse files Browse the repository at this point in the history
  • Loading branch information
viacheslav-rostovtsev authored Sep 14, 2022
1 parent f097484 commit c464fc5
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 41 deletions.
1 change: 1 addition & 0 deletions gapic-common/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Style/Documentation:
Exclude:
- "lib/gapic/grpc/service_stub/rpc_call.rb"
- "lib/gapic/grpc/status_details.rb"

Style/CaseEquality:
Exclude:
- "lib/gapic/config.rb"
85 changes: 74 additions & 11 deletions gapic-common/lib/gapic/rest/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,30 @@ module Gapic
module Rest
# Gapic REST exception class
class Error < ::Gapic::Common::Error
# @return [Integer] the http status code for the error
# @return [Integer, nil] the http status code for the error
attr_reader :status_code
# @return [Object, nil] the text representation of status as parsed from the response body
attr_reader :status
# @return [Object, nil] the details as parsed from the response body
attr_reader :details
# @return [Object, nil] the headers of the REST error
attr_reader :headers
# The Cloud error wrapper expect to see a `header` property
alias header headers

##
# @param message [String, nil] error message
# @param status_code [Integer, nil] HTTP status code of this error
# @param status [String, nil] The text representation of status as parsed from the response body
# @param details [Object, nil] Details data of this error
# @param headers [Object, nil] Http headers data of this error
#
def initialize message, status_code
@status_code = status_code
def initialize message, status_code, status: nil, details: nil, headers: nil
super message
@status_code = status_code
@status = status
@details = details
@headers = headers
end

class << self
Expand All @@ -37,36 +51,85 @@ class << self
# it tries to parse and set a detailed message and an error code from
# from the Google Cloud's response body
#
# @param err [Faraday::Error] the Faraday error to wrap
#
# @return [ Gapic::Rest::Error]
def wrap_faraday_error err
message = err.message
status_code = err.response_status
status = nil
details = nil
headers = err.response_headers

if err.response_body
msg, code = try_parse_from_body err.response_body
msg, code, status, details = try_parse_from_body err.response_body
message = "An error has occurred when making a REST request: #{msg}" unless msg.nil?
status_code = code unless code.nil?
end

Gapic::Rest::Error.new message, status_code
Gapic::Rest::Error.new message, status_code, status: status, details: details, headers: headers
end

private

##
# @private
# Tries to get the error information from the JSON bodies
#
# @param body_str [String]
# @return [Array(String, String)]
# @return [Array(String, String, String, String)]
def try_parse_from_body body_str
body = JSON.parse body_str
return [nil, nil] unless body && body["error"].is_a?(Hash)

message = body["error"]["message"]
code = body["error"]["code"]
unless body.is_a?(::Hash) && body&.key?("error") && body["error"].is_a?(::Hash)
return [nil, nil, nil, nil]
end
error = body["error"]

message = error["message"] if error.key? "message"
code = error["code"] if error.key? "code"
status = error["status"] if error.key? "status"

details = parse_details error["details"] if error.key? "details"

[message, code]
[message, code, status, details]
rescue JSON::ParserError
[nil, nil]
[nil, nil, nil, nil]
end

##
# @private
# Parses the details data, trying to extract the Protobuf.Any objects
# from it, if it's an array of hashes. Otherwise returns it as is.
#
# @param details [Object, nil] the details object
#
# @return [Object, nil]
def parse_details details
# For rest errors details will contain json representations of `Protobuf.Any`
# decoded into hashes. If it's not an array, of its elements are not hashes,
# it's some other case
return details unless details.is_a? ::Array

details.map do |detail_instance|
next detail_instance unless detail_instance.is_a? ::Hash
# Next, parse detail_instance into a Proto message.
# There are three possible issues for the JSON->Any->message parsing
# - json decoding fails
# - the json belongs to a proto message type we don't know about
# - any unpacking fails
# If we hit any of these three issues we'll just return the original hash
begin
any = ::Google::Protobuf::Any.decode_json detail_instance.to_json
klass = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(any.type_name)&.msgclass
next detail_instance if klass.nil?
unpack = any.unpack klass
next detail_instance if unpack.nil?
unpack
rescue ::Google::Protobuf::ParseError
detail_instance
end
end.compact
end
end
end
Expand Down
Loading

0 comments on commit c464fc5

Please sign in to comment.