lib/ievkitdeprecated/error.rb
module Ievkitdeprecated
# Custom error class for rescuing from all Iev errors
class Error < StandardError
# Returns the appropriate Ievkitdeprecated::Error subclass based
# on status and response message
#
# @param [Hash] response HTTP response
# @return [Ievkitdeprecated::Error]
def self.from_response(response)
status = response[:status].to_i
body = response[:body].to_s
headers = response[:response_headers]
if klass = case status
when 400 then Ievkitdeprecated::BadRequest
when 401 then error_for_401(headers)
when 403 then error_for_403(body)
when 404 then Ievkitdeprecated::NotFound
when 405 then Ievkitdeprecated::MethodNotAllowed
when 406 then Ievkitdeprecated::NotAcceptable
when 409 then Ievkitdeprecated::Conflict
when 415 then Ievkitdeprecated::UnsupportedMediaType
when 422 then Ievkitdeprecated::UnprocessableEntity
when 400..499 then Ievkitdeprecated::ClientError
when 500 then Ievkitdeprecated::InternalServerError
when 501 then Ievkitdeprecated::NotImplemented
when 502 then Ievkitdeprecated::BadGateway
when 503 then Ievkitdeprecated::ServiceUnavailable
when 500..599 then Ievkitdeprecated::ServerError
end
klass.new(response)
end
end
def initialize(response=nil)
@response = response
super(build_error_message)
end
# Documentation URL returned by the API for some errors
#
# @return [String]
def documentation_url
data[:documentation_url] if data.is_a? Hash
end
# Returns most appropriate error for 401 HTTP status code
# @private
def self.error_for_401(headers)
if Ievkitdeprecated::OneTimePasswordRequired.required_header(headers)
Ievkitdeprecated::OneTimePasswordRequired
else
Ievkitdeprecated::Unauthorized
end
end
# Returns most appropriate error for 403 HTTP status code
# @private
def self.error_for_403(body)
if body =~ /rate limit exceeded/i
Ievkitdeprecated::TooManyRequests
elsif body =~ /login attempts exceeded/i
Ievkitdeprecated::TooManyLoginAttempts
elsif body =~ /abuse/i
Ievkitdeprecated::AbuseDetected
elsif body =~ /repository access blocked/i
Ievkitdeprecated::RepositoryUnavailable
else
Ievkitdeprecated::Forbidden
end
end
# Array of validation errors
# @return [Array<Hash>] Error info
def errors
if data && data.is_a?(Hash)
data[:errors] || []
else
[]
end
end
def locale_for_error
if self.class.eql? Ievkitdeprecated::NotFound
'iev.exception.unknown_job'
else
'iev.exception.default'
end
end
private
def data
@data ||=
if (body = @response[:body]) && !body.empty?
if body.is_a?(String) &&
@response[:response_headers] &&
@response[:response_headers][:content_type] =~ /json/
Sawyer::Agent.serializer.decode(body)
else
body
end
else
nil
end
end
def response_message
case data
when Hash
data[:message]
when String
data
end
end
def response_error
"Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
end
def response_error_summary
return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
summary = "\nError summary:\n"
summary << data[:errors].map do |hash|
hash.map { |k,v| " #{k}: #{v}" }
end.join("\n")
summary
end
def build_error_message
return nil if @response.nil?
message = "#{@response[:method].to_s.upcase} "
message << redact_url(@response[:url].to_s) + ": "
message << "#{@response[:status]} - "
message << "#{response_message}" unless response_message.nil?
message << "#{response_error}" unless response_error.nil?
message << "#{response_error_summary}" unless response_error_summary.nil?
message << " // See: #{documentation_url}" unless documentation_url.nil?
message
end
def redact_url(url_string)
%w[client_secret access_token].each do |token|
url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
end
url_string
end
end
# Raised on errors in the 400-499 range
class ClientError < Error; end
# Raised when Iev returns a 400 HTTP status code
class BadRequest < ClientError; end
# Raised when Iev returns a 401 HTTP status code
class Unauthorized < ClientError; end
# Raised when Iev returns a 401 HTTP status code
# and headers include "X-Iev-OTP"
class OneTimePasswordRequired < ClientError
#@private
OTP_DELIVERY_PATTERN = /required; (\w+)/i
#@private
def self.required_header(headers)
OTP_DELIVERY_PATTERN.match headers['X-Iev-OTP'].to_s
end
# Delivery method for the user's OTP
#
# @return [String]
def password_delivery
@password_delivery ||= delivery_method_from_header
end
private
def delivery_method_from_header
if match = self.class.required_header(@response[:response_headers])
match[1]
end
end
end
# Raised when Iev returns a 403 HTTP status code
class Forbidden < ClientError; end
# Raised when Iev returns a 403 HTTP status code
# and body matches 'rate limit exceeded'
class TooManyRequests < Forbidden; end
# Raised when Iev returns a 403 HTTP status code
# and body matches 'login attempts exceeded'
class TooManyLoginAttempts < Forbidden; end
# Raised when Iev returns a 403 HTTP status code
# and body matches 'abuse'
class AbuseDetected < Forbidden; end
# Raised when Iev returns a 403 HTTP status code
# and body matches 'repository access blocked'
class RepositoryUnavailable < Forbidden; end
# Raised when Iev returns a 404 HTTP status code
class NotFound < ClientError; end
# Raised when Iev returns a 405 HTTP status code
class MethodNotAllowed < ClientError; end
# Raised when Iev returns a 406 HTTP status code
class NotAcceptable < ClientError; end
# Raised when Iev returns a 409 HTTP status code
class Conflict < ClientError; end
# Raised when Iev returns a 414 HTTP status code
class UnsupportedMediaType < ClientError; end
# Raised when Iev returns a 422 HTTP status code
class UnprocessableEntity < ClientError; end
# Raised on errors in the 500-599 range
class ServerError < Error; end
# Raised when Iev returns a 500 HTTP status code
class InternalServerError < ServerError; end
# Raised when Iev returns a 501 HTTP status code
class NotImplemented < ServerError; end
# Raised when Iev returns a 502 HTTP status code
class BadGateway < ServerError; end
# Raised when Iev returns a 503 HTTP status code
class ServiceUnavailable < ServerError; end
# Raised when client fails to provide valid Content-Type
class MissingContentType < ArgumentError; end
# Raised when a method requires an application client_id
# and secret but none is provided
class ApplicationCredentialsRequired < StandardError; end
end