Skip to content

Commit

Permalink
Faraday as http client (#330)
Browse files Browse the repository at this point in the history
* switch from rest_client to faraday as http client

* add backward compatibility with the previous response version
  • Loading branch information
mgrishko authored Jan 24, 2025
1 parent 6fb8409 commit 53a8edd
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 59 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in mailgun.gemspec
gemspec

gem 'mime-types'
gem 'json', '~> 2.1', platform: :mri_19
2 changes: 1 addition & 1 deletion lib/mailgun.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require 'tempfile'
require 'rest_client'
require 'faraday'
require 'yaml'
require 'json'

Expand Down
68 changes: 38 additions & 30 deletions lib/mailgun/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,26 @@ def initialize(api_key = Mailgun.api_key,
timeout = nil,
proxy_url = Mailgun.proxy_url)

rest_client_params = {
user: 'api',
password: api_key,
user_agent: "mailgun-sdk-ruby/#{Mailgun::VERSION}"
endpoint = endpoint_generator(api_host, api_version, ssl)

request_options = {
url: endpoint,
proxy: Mailgun.proxy_url,
ssl: {verify: ssl},
headers: {
'User-Agent' => "mailgun-sdk-ruby/#{Mailgun::VERSION}",
'Accept' =>'*/*'
}
}
rest_client_params[:timeout] = timeout if timeout
request_options.merge!(request: {timeout: timeout}) if timeout

@http_client = Faraday.new(request_options) do |conn|
conn.request :authorization, :basic, 'api', api_key
conn.request :url_encoded
conn.response :raise_error, include_request: true
conn.adapter Faraday.default_adapter
end

endpoint = endpoint_generator(api_host, api_version, ssl)
RestClient.proxy = proxy_url
@http_client = RestClient::Resource.new(endpoint, rest_client_params)
@test_mode = test_mode
@api_version = api_version
end
Expand All @@ -49,17 +59,17 @@ def disable_test_mode!

# Change API key
def set_api_key(api_key)
@http_client.options[:password] = api_key
@http_client.set_basic_auth('api', api_key)
end

# Add subaccount id to headers
def set_subaccount(subaccount_id)
@http_client.options[:headers] = { SUBACCOUNT_HEADER => subaccount_id }
@http_client.headers = @http_client.headers.merge!({ SUBACCOUNT_HEADER => subaccount_id })
end

# Reset subaccount for primary usage
def reset_subaccount
@http_client.options[:headers].delete(SUBACCOUNT_HEADER)
@http_client.headers.delete(SUBACCOUNT_HEADER)
end

# Client is in test mode?
Expand Down Expand Up @@ -95,7 +105,7 @@ def send_message(working_domain, data)
return Response.from_hash(
{
:body => "{\"id\": \"test-mode-mail-#{SecureRandom.uuid}@localhost\", \"message\": \"Queued. Thank you.\"}",
:code => 200,
:status => 200,
}
)
end
Expand Down Expand Up @@ -130,29 +140,27 @@ def send_message(working_domain, data)
# @param [Hash] headers Additional headers to pass to the resource.
# @return [Mailgun::Response] A Mailgun::Response object.
def post(resource_path, data, headers = {})
response = @http_client[resource_path].post(data, headers)
response = @http_client.post(resource_path, data, headers)
Response.new(response)
rescue => err
raise communication_error err
end

# Generic Mailgun GET Handler
#
# @param [String] resource_path This is the API resource you wish to interact
# with. Be sure to include your domain, where necessary.
# @param [Hash] params This should be a standard Hash
# containing required parameters for the requested resource.
# @param [String] accept Acceptable Content-Type of the response body.
# @return [Mailgun::Response] A Mailgun::Response object.
def get(resource_path, params = nil, accept = '*/*')
if params
response = @http_client[resource_path].get(params: params, accept: accept)
else
response = @http_client[resource_path].get(accept: accept)
end
# @param [String] resource_path The API resource path to request, including the domain if required.
# @param [Hash] params Optional request parameters, including query parameters and headers.
# - `:headers` [Hash] (optional) Custom headers for the request.
# @param [String] accept The expected Content-Type of the response. Defaults to '*/*'.
# @return [Mailgun::Response] A response object containing the API response data.
# @raise [CommunicationError] If the request fails, raises a communication error.
def get(resource_path, params = {}, accept = '*/*')
headers = (params[:headers] || {}).merge(accept: accept)
response = @http_client.get(resource_path, params, headers)

Response.new(response)
rescue => err
raise communication_error err
raise communication_error(err)
end

# Generic Mailgun PUT Handler
Expand All @@ -163,7 +171,7 @@ def get(resource_path, params = nil, accept = '*/*')
# containing required parameters for the requested resource.
# @return [Mailgun::Response] A Mailgun::Response object.
def put(resource_path, data)
response = @http_client[resource_path].put(data)
response = @http_client.put(resource_path, data)
Response.new(response)
rescue => err
raise communication_error err
Expand All @@ -176,9 +184,9 @@ def put(resource_path, data)
# @return [Mailgun::Response] A Mailgun::Response object.
def delete(resource_path, params = nil)
if params
response = @http_client[resource_path].delete(params: params)
response = @http_client.delete(resource_path, params: params)
else
response = @http_client[resource_path].delete
response = @http_client.delete(resource_path)
end
Response.new(response)
rescue => err
Expand Down Expand Up @@ -227,7 +235,7 @@ def endpoint_generator(api_host, api_version, ssl)
# @param [StandardException] e upstream exception object
def communication_error(e)
if e.respond_to?(:response) && e.response
return case e.response.code
return case e.response_status
when Unauthorized::CODE
Unauthorized.new(e.message, e.response)
when BadRequest::CODE
Expand Down
14 changes: 7 additions & 7 deletions lib/mailgun/exceptions/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class ParseError < Error; end
# Public: Class for managing communications (eg http) response errors
# Inherits from Mailgun::Error
class CommunicationError < Error
# Public: gets HTTP status code
attr_reader :code
# Public: gets HTTP status status
attr_reader :status

# Public: fallback if there is no response code on the object
# Public: fallback if there is no response status on the object
NOCODE = 000
FORBIDDEN = 'Forbidden'

Expand All @@ -43,17 +43,17 @@ class CommunicationError < Error
#
def initialize(message = nil, response = nil)
@response = response
@code = if response.nil?
@status = if response.nil?
NOCODE
else
response.code
response.status
end

begin
json = JSON.parse(response.body)
api_message = json['message'] || json['Error'] || json['error']
rescue JSON::ParserError
api_message = response.body
api_message = response.response_body
rescue NoMethodError
api_message = "Unknown API error"
rescue
Expand All @@ -65,7 +65,7 @@ def initialize(message = nil, response = nil)

super(message, response)
rescue NoMethodError, JSON::ParserError
@code = NOCODE
@status = NOCODE
super(message, response)
end
end
Expand Down
21 changes: 11 additions & 10 deletions lib/mailgun/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ module Mailgun
#
# See the Github documentation for full examples.
class Response
# All responses have a payload and a code corresponding to http, though
# All responses have a payload and a status corresponding to http, though
# slightly different
attr_accessor :body, :code
attr_accessor :body, :status, :code

ResponseHash = Struct.new(:body, :code)
ResponseHash = Struct.new(:body, :status)
def self.from_hash(h)
# Create a "fake" response object with the data passed from h
self.new ResponseHash.new(h[:body], h[:code])
self.new ResponseHash.new(h[:body], h[:status])
end

def initialize(response)
@body = response.body
@code = response.code
@status = response.status
@code = response.status
end

# Return response as Ruby Hash
Expand Down Expand Up @@ -57,12 +58,12 @@ def to_yaml!
rescue => err
raise ParseError.new(err), err
end
# Returns true if response code is 2xx
#
# @return [Boolean] A boolean that binarizes the response code result.

# Returns true if response status is 2xx
#
# @return [Boolean] A boolean that binarizes the response status result.
def success?
(200..299).include?(code)
(200..299).include?(status)
end
end
end
2 changes: 1 addition & 1 deletion lib/mailgun/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# It's the version. Yeay!
module Mailgun
VERSION = '1.2.16'
VERSION = '1.3.0'
end
3 changes: 2 additions & 1 deletion mailgun.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler', '>= 1.16.2'
spec.add_development_dependency 'rspec', '~> 3.8.0'
spec.add_development_dependency 'rake', '~> 12.3.2'
spec.add_development_dependency 'mime-types'
spec.add_development_dependency 'webmock', '~> 3.7'
spec.add_development_dependency 'pry', '~> 0.11.3'
spec.add_development_dependency 'vcr', '~> 3.0.3'
spec.add_development_dependency 'simplecov', '~> 0.16.1'
spec.add_development_dependency 'rails'
spec.add_dependency 'rest-client', '>= 2.0.2'
spec.add_dependency 'faraday', "~> 2.1"

end
10 changes: 5 additions & 5 deletions spec/integration/mailgun_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
:text => 'INTEGRATION TESTING'
})
rescue Mailgun::Unauthorized => err
expect(err.message).to eq('401 Unauthorized - Invalid Domain or API key: Forbidden')
expect(err.message).to eq('the server responded with status 401 - Invalid Domain or API key')
else
fail
end
Expand All @@ -75,7 +75,7 @@
:text => 'INTEGRATION TESTING'
})
rescue Mailgun::BadRequest => err
expect(err.message).to eq('400 Bad Request: to parameter is not a valid address. please check documentation')
expect(err.message).to eq('the server responded with status 400')
else
fail
end
Expand All @@ -99,7 +99,7 @@
:text => 'INTEGRATION TESTING'
})
rescue Mailgun::CommunicationError => err
expect(err.message).to include('403 Forbidden')
expect(err.message).to include('403')
else
fail
end
Expand Down Expand Up @@ -192,7 +192,7 @@
expect(result.body).to include("message")
expect(result.body).to include("id")
end

it 'receives success response code' do
@mg_obj.enable_test_mode!

Expand All @@ -205,7 +205,7 @@

result = @mg_obj.send_message(@domain, data)
result.to_h!

expect(result.success?).to be(true)
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/connection/test_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def response_generator(resource_endpoint)
return Response.from_hash({ body: JSON.generate({"message" => "Queued. Thank you.", "id" => id}) })
end
if resource_endpoint == "bounces"
return Response.from_hash({ body: JSON.generate({"total_count" => 1, "items" => {"created_at" => "Fri, 21 Oct 2011 11:02:55 GMT", "code" => 550, "address" => "[email protected]", "error" => "Message was not accepted -- invalid mailbox. Local mailbox [email protected] is unavailable: user not found"}}) })
return Response.from_hash({ body: JSON.generate({"total_count" => 1, "items" => {"created_at" => "Fri, 21 Oct 2011 11:02:55 GMT", "status" => 550, "address" => "[email protected]", "error" => "Message was not accepted -- invalid mailbox. Local mailbox [email protected] is unavailable: user not found"}}) })
end
if resource_endpoint == "lists"
return Response.from_hash({ body: JSON.generate({"member" => {"vars" => {"age" => 26}, "name" => "Foo Bar", "subscribed" => false, "address" => "[email protected]"}, "message" => "Mailing list member has been updated"}) })
Expand Down
6 changes: 3 additions & 3 deletions spec/unit/exceptions/exceptions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
context "when the Response body doesn't have a `message` property" do
it "doesn't raise an error" do
expect do
described_class.new('Boom!', Mailgun::Response.from_hash({ code: 401, body: '{}' }))
described_class.new('Boom!', Mailgun::Response.from_hash({ status: 401, body: '{}' }))
end.not_to raise_error
end

context "when the Response body has an `Error` property" do
it "uses the `Error` property as the API message" do
subject = described_class.new('Boom!', Mailgun::Response.from_hash({ code: 401, body: '{"Error":"unauthorized"}' }))
subject = described_class.new('Boom!', Mailgun::Response.from_hash({ status: 401, body: '{"Error":"unauthorized"}' }))

expect(subject.message).to eq("Boom!: unauthorized")
end
end

context "when the Response body has an `error` property" do
it "uses the `Error` property as the API message" do
subject = described_class.new('Boom!', Mailgun::Response.from_hash({ code: 401, body: '{"error":"not found"}' }))
subject = described_class.new('Boom!', Mailgun::Response.from_hash({ status: 401, body: '{"error":"not found"}' }))

expect(subject.message).to eq("Boom!: not found")
end
Expand Down

0 comments on commit 53a8edd

Please sign in to comment.