module Alula class AlulaError < StandardError attr_reader :http_status, :raw_response, :error, :message def initialize(error) if error.class == String @message = error return end @http_status = error.http_status @raw_response = error @error = error.data['error'] @message = error.data['error_description'] || error.data['message'] end def ok? false end def self.for_response(response) if !response.data['error'].nil? && !response.data['error'].empty? self.error_for_response(response) elsif !response.data['errors'].nil? && !response.data['errors'].empty? self.errors_for_response(response) elsif response.data.match(/^/) self.critical_error_for_response(response.data.scan(/
(.*)<\/pre>/)) elsif response.data.match(/502 Bad Gateway/) self.gateway_error_for_response(response.data.scan(/502 Bad Gateway/)) else message = "Unable to derive error from response: #{response.inspect}" Alula.logger.error message raise UnknownApiError.new(message) end rescue NoMethodError message = "Unable to derive error from response: #{response.inspect}" Alula.logger.error message raise UnknownApiError.new(message) end private # Handle HTML-based errors from the API def self.critical_error_for_response(error_text) InvalidRequestError.new(error_text.first.first) end # Handle Gateway errors from the API def self.gateway_error_for_response(error_text) ApiGatewayError.new(error_text.first) end # Figure out what error should be raised def self.error_for_response(response) case response.data['error'] when 'RateLimit' Alula.logger.error response RateLimitError.new(response) when 'invalid_token' InvalidTokenError.new(response) when 'insufficient_scope' InsufficientScopeError.new(response) when 'server_error' Alula.logger.error response ServerError.new(response) else # # RPC errors are identified by jsonrpc in the body. return ProcedureError.new(response) if response.data['jsonrpc'] # Error messages coming from the API are inconsistent. # Sometimes they are in the msg key, sometimes in the message key # Example: https://github.com/alula-net/alula-connect/pull/3417#issue-1985813211 # Here we have some errors that are actually known but at least one of them is not properly formatted by the API message = response.data.dig('error', 'data', 'msg') || response.data.dig('error', 'data', 'message') || response.data.dig('error', 'description') case message when 'dealer does not exist' NotFoundError.new(message) when 'Insufficient User Scope' InsufficientScopeError.new(message) when 'Not Found' NotFoundError.new(message) else Alula.logger.error response raise NotImplementedError, "Unable to derive error for #{response.data['error'].to_json}" end end end # Some responses have an error array. We won't be able to raise on all errors but we can raise on the first one def self.errors_for_response(response) error = response.data['errors'].first case error['title'] when 'RateLimit' RateLimitError.new(response) when 'Forbidden' ForbiddenError.new(error['detail']) when 'Not Found' NotFoundError.new(error['detail']) when 'Bad Request' Alula.logger.error response BadRequestError.new(error['detail']) when 'API Error Unknown' Alula.logger.error response UnknownError.new(error['detail']) when 'Insufficient Scope' InsufficientScopeError.new(error['detail']) else # # RPC errors are identified by jsonrpc in the body. return ProcedureError.new(response) if response.data['jsonrpc'] # See comment in the error_for_response method message = error.dig('data', 'msg') || error.dig('data', 'message') || error['description'] case message when 'dealer does not exist' NotFoundError.new(message) when 'Insufficient User Scope' InsufficientScopeError.new(message) when 'Not Found' NotFoundError.new(message) else Alula.logger.error response raise NotImplementedError, "Unable to derive error for #{response.data['errors'].to_json}" end end end end class UnknownApiError < AlulaError end class ApiGatewayError < AlulaError end class RateLimitError < AlulaError end class NotFoundError < AlulaError end class ForbiddenError < AlulaError end class InvalidRequestError < AlulaError end class InvalidTokenError < AlulaError end class InsufficientScopeError < AlulaError end class BadRequestError < AlulaError end class NotConfiguredError < AlulaError end class InvalidFilterFieldError < AlulaError end class InvalidSortFieldError < AlulaError end class InvalidRelationshipError < AlulaError end class InvalidRoleError < AlulaError end class ServerError < AlulaError end class UnknownError < AlulaError end class ProcedureError < AlulaError attr_reader :code, :full_messages def initialize(response) # Take 1 error from the request # TODO: Multiple errors are possible, we probably want to shlep that up error = response.data['error'] || response.data['errors'].first @http_status = response.http_status @raw_response = response @error = error['message'] @full_messages = error.dig('data', 'message')&.split(', ') || [error['message']] @message = error['message'] @code = error['code'] end # # Provides interface mirroring to success responses def ok? false end end class ValidationError < AlulaError attr_reader :errors def initialize(errors) @errors = errors end end end