lib/recurly/client.rb in recurly-3.4.1 vs lib/recurly/client.rb in recurly-3.5.0
- old
+ new
@@ -1,9 +1,10 @@
require "logger"
require "erb"
require "net/https"
require "base64"
+require "securerandom"
require_relative "./schema/json_parser"
require_relative "./schema/file_parser"
module Recurly
class Client
@@ -16,10 +17,12 @@
"application/pdf",
]
JSON_CONTENT_TYPE = "application/json"
MAX_RETRIES = 3
+ BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a
+
# Initialize a client. It requires an API key.
#
# @example
# API_KEY = '83749879bbde395b5fe0cc1a5abf8e5'
# client = Recurly::Client.new(api_key: API_KEY)
@@ -51,51 +54,49 @@
# execute block with this client if given
yield(self) if block_given?
end
- def next_page(pager)
- 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
+ # Used by the operations.rb file to interpolate paths
+ attr_reader :site_id
+
def pager(path, **options)
- path = scope_by_site(path, **options)
Pager.new(
client: self,
path: path,
options: options,
)
end
+ def head(path, **options)
+ request = Net::HTTP::Head.new build_url(path, options)
+ set_headers(request, options[:headers])
+ http_response = run_request(request, options)
+ handle_response! request, http_response
+ end
+
def get(path, **options)
- path = scope_by_site(path, **options)
- request = Net::HTTP::Get.new path
+ request = Net::HTTP::Get.new build_url(path, options)
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 = Net::HTTP::Post.new path
+ request = Net::HTTP::Post.new build_url(path, options)
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 = Net::HTTP::Put.new path
+ request = Net::HTTP::Put.new build_url(path, options)
request.set_content_type(JSON_CONTENT_TYPE)
set_headers(request, options[:headers])
if request_data
request_class.new(request_data).validate!
json_body = JSON.dump(request_data)
@@ -105,22 +106,16 @@
http_response = run_request(request, options)
handle_response! request, http_response
end
def delete(path, **options)
- path = scope_by_site(path, **options)
- request = Net::HTTP::Delete.new path
+ request = Net::HTTP::Delete.new build_url(path, options)
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
- attr_reader :site_id
-
private
# @return [Logger]
attr_reader :logger
@@ -137,11 +132,19 @@
retries = 0
begin
http.start unless http.started?
- http.request(request)
+ response = http.request(request)
+
+ # GETs are safe to retry after a server error, requests with an Idempotency-Key will return the prior response
+ if response.kind_of?(Net::HTTPServerError) && request.is_a?(Net::HTTP::Get)
+ retries += 1
+ response = http.request(request) if retries < MAX_RETRIES
+ end
+
+ response
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
@@ -165,14 +168,27 @@
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}"
+ unless request.is_a?(Net::HTTP::Get) || request.is_a?(Net::HTTP::Head)
+ request["Idempotency-Key"] ||= generate_idempotency_key
+ end
+
# TODO this is undocumented until we finalize it
additional_headers.each { |header, v| request[header] = v } if additional_headers
end
+ # from https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/core_ext/securerandom.rb
+ def generate_idempotency_key(n = 16)
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
+ idx = byte % 64
+ idx = SecureRandom.random_number(36) if idx >= 36
+ BASE36_ALPHABET[idx]
+ end.join
+ 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?
@@ -249,21 +265,26 @@
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}"
+ def build_url(path, options)
+ path = scope_by_site(path, options)
+ if options.any?
+ "#{path}?#{URI.encode_www_form(options)}"
else
path
end
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
+ def scope_by_site(path, **options)
+ if site = site_id || options[:site_id]
+ # Ensure that we are only including the site_id once because the Pager operations
+ # will use the cursor returned from the API which may already have these components
+ path.start_with?("/sites/#{site}") ? path : "/sites/#{site}#{path}"
+ else
+ path
+ end
end
def set_options(options)
@log_level = options[:log_level] || Logger::WARN
@logger = Logger.new(STDOUT)