lib/recurly/client.rb in recurly-3.3.1 vs lib/recurly/client.rb in recurly-3.4.0

- old
+ new

@@ -1,20 +1,24 @@ -require "faraday" require "logger" require "erb" +require "net/https" +require "base64" require_relative "./schema/json_parser" require_relative "./schema/file_parser" -require_relative "./client/adapter" module Recurly class Client require_relative "./client/operations" - BASE_URL = "https://v3.recurly.com/" + BASE_HOST = "v3.recurly.com" + BASE_PORT = 443 + CA_FILE = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt") BINARY_TYPES = [ "application/pdf", ] + JSON_CONTENT_TYPE = "application/json" + MAX_RETRIES = 3 # Initialize a client. It requires an API key. # # @example # API_KEY = '83749879bbde395b5fe0cc1a5abf8e5' @@ -40,21 +44,23 @@ # @param api_key [String] The private API key # @param site_id [String] The site you wish to be scoped to. # @param subdomain [String] Optional subdomain for the site you wish to be scoped to. Providing this makes all the `site_id` parameters optional. def initialize(api_key:, site_id: nil, subdomain: nil, **options) set_site_id(site_id, subdomain) + set_api_key(api_key) set_options(options) - set_faraday_connection(api_key) # execute block with this client if given yield(self) if block_given? end def next_page(pager) - req = HTTP::Request.new(:get, pager.next, nil) - faraday_resp = run_request(req, headers) - handle_response! req, faraday_resp + path = extract_path(pager.next) + request = Net::HTTP::Get.new path + set_headers(request) + http_response = run_request(request) + handle_response! request, http_response end protected def pager(path, **options) @@ -66,48 +72,48 @@ ) end def get(path, **options) path = scope_by_site(path, **options) - request = HTTP::Request.new(:get, path, nil) - faraday_resp = run_request(request, headers) - handle_response! request, faraday_resp - rescue Faraday::ClientError => ex - raise_network_error!(ex) + request = Net::HTTP::Get.new path + set_headers(request, options[:headers]) + http_response = run_request(request, options) + handle_response! request, http_response end def post(path, request_data, request_class, **options) request_class.new(request_data).validate! path = scope_by_site(path, **options) - request = HTTP::Request.new(:post, path, JSON.dump(request_data)) - faraday_resp = run_request(request, headers) - handle_response! request, faraday_resp - rescue Faraday::ClientError => ex - raise_network_error!(ex) + request = Net::HTTP::Post.new path + request.set_content_type(JSON_CONTENT_TYPE) + set_headers(request, options[:headers]) + request.body = JSON.dump(request_data) + http_response = run_request(request, options) + handle_response! request, http_response end def put(path, request_data = nil, request_class = nil, **options) path = scope_by_site(path, **options) - request = HTTP::Request.new(:put, path) + request = Net::HTTP::Put.new path + request.set_content_type(JSON_CONTENT_TYPE) + set_headers(request, options[:headers]) if request_data request_class.new(request_data).validate! - logger.info("PUT BODY #{JSON.dump(request_data)}") - request.body = JSON.dump(request_data) + json_body = JSON.dump(request_data) + logger.info("PUT BODY #{json_body}") + request.body = json_body end - faraday_resp = run_request(request, headers) - handle_response! request, faraday_resp - rescue Faraday::ClientError => ex - raise_network_error!(ex) + http_response = run_request(request, options) + handle_response! request, http_response end def delete(path, **options) path = scope_by_site(path, **options) - request = HTTP::Request.new(:delete, path, nil) - faraday_resp = run_request(request, headers) - handle_response! request, faraday_resp - rescue Faraday::ClientError => ex - raise_network_error!(ex) + request = Net::HTTP::Delete.new path + set_headers(request, options[:headers]) + http_response = run_request(request, options) + handle_response! request, http_response end protected # Used by the operations.rb file to interpolate paths @@ -116,67 +122,107 @@ private # @return [Logger] attr_reader :logger - def run_request(request, headers) - read_headers @conn.run_request(request.method, request.path, request.body, headers) + @connection_pool = Recurly::ConnectionPool.new + + class << self + # @return [Recurly::ConnectionPool] + attr_accessor :connection_pool end - def handle_response!(request, faraday_resp) - response = HTTP::Response.new(faraday_resp, request) - raise_api_error!(response) unless (200...300).include?(response.status) + def run_request(request, options = {}) + self.class.connection_pool.with_connection do |http| + set_http_options(http, options) + + retries = 0 + + begin + http.start unless http.started? + http.request(request) + rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED, + Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex + retries += 1 + if retries < MAX_RETRIES + retry + end + + if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT) + raise Recurly::Errors::TimeoutError, "Request timed out" + end + + raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}" + rescue Net::ReadTimeout, Timeout::Error + raise Recurly::Errors::TimeoutError, "Request timed out" + rescue OpenSSL::SSL::SSLError => ex + raise Recurly::Errors::SSLError, ex.message + rescue StandardError => ex + raise Recurly::Errors::NetworkError, ex.message + end + end + end + + def set_headers(request, additional_headers = {}) + request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb + request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp + request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}" + + # TODO this is undocumented until we finalize it + additional_headers.each { |header, v| request[header] = v } if additional_headers + end + + def set_http_options(http, options) + http.open_timeout = options[:open_timeout] || 20 + http.read_timeout = options[:read_timeout] || 60 + + http.set_debug_output(logger) if @log_level <= Logger::INFO && !http.started? + end + + def handle_response!(request, http_response) + response = HTTP::Response.new(http_response, request) + raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess) resource = if response.body - if BINARY_TYPES.include?(response.content_type) + if http_response.content_type.include?(JSON_CONTENT_TYPE) + JSONParser.parse(self, response.body) + elsif BINARY_TYPES.include?(http_response.content_type) FileParser.parse(response.body) else - JSONParser.parse(self, response.body) + raise Recurly::Errors::InvalidResponseError, "Unexpected content type: #{http_response.content_type}" end else Resources::Empty.new end # Keep this interface "private" resource.instance_variable_set(:@response, response) resource end - def raise_network_error!(ex) - error_class = case ex - when Faraday::TimeoutError - Errors::TimeoutError - when Faraday::ConnectionFailed - Errors::ConnectionFailedError - when Faraday::SSLError - Errors::SSLError - else - Errors::NetworkError - end + def raise_api_error!(http_response, response) + if response.content_type.include?(JSON_CONTENT_TYPE) + error = JSONParser.parse(self, response.body) + error_class = Errors::APIError.error_class(error.type) + raise error_class.new(response, error) + end - raise error_class, ex.message - end + error_class = Errors::APIError.from_response(http_response) - def raise_api_error!(response) - error = JSONParser.parse(self, response.body) - error_class = Errors::APIError.error_class(error.type) - raise error_class.new(response, error) + if error_class <= Recurly::Errors::APIError + error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}") + raise error_class.new(response, error) + else + raise error_class, "#{http_response.code}: #{http_response.message}" + end end def read_headers(response) if !@_ignore_deprecation_warning && response.headers["Recurly-Deprecated"]&.upcase == "TRUE" puts "[recurly-client-ruby] WARNING: Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}" end response end - def headers - { - "Accept" => "application/vnd.recurly.#{api_version}", # got this method from operations.rb - "Content-Type" => "application/json", - "User-Agent" => "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}", - }.merge(@extra_headers) - end - def interpolate_path(path, **options) options.each do |k, v| # Check to see that we are passing the correct data types # This prevents a confusing error if the user passes in a non-primitive by mistake unless [String, Symbol, Integer, Float].include?(v.class) @@ -199,44 +245,30 @@ elsif subdomain @site_id = "subdomain-#{subdomain}" end end + def set_api_key(api_key) + @api_key = api_key + end + def scope_by_site(path, **options) if site = site_id || options[:site_id] "/sites/#{site}#{path}" else path end end - def set_faraday_connection(api_key) - options = { - url: BASE_URL, - request: { timeout: 60, open_timeout: 50 }, - ssl: { verify: true }, - } - # Let's not use the bundled cert in production yet - # but we will use these certs for any other staging or dev environment - unless BASE_URL.end_with?(".recurly.com") - options[:ssl][:ca_file] = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt") - end - - @conn = Faraday.new(options) do |faraday| - if [Logger::DEBUG, Logger::INFO].include?(@log_level) - faraday.response :logger - end - faraday.basic_auth(api_key, "") - configure_net_adapter(faraday) - end + # Returns just the path and parameters so we can safely reuse the connection + def extract_path(uri_or_path) + uri = URI(uri_or_path) + uri.kind_of?(URI::HTTP) ? uri.request_uri : uri_or_path end def set_options(options) @log_level = options[:log_level] || Logger::WARN @logger = Logger.new(STDOUT) @logger.level = @log_level - - # TODO this is undocumented until we finalize it - @extra_headers = options[:headers] || {} end end end