Lots of broken tests, but working implementation!

This commit is contained in:
Brandur 2017-01-20 15:34:44 -08:00
parent 41f58e3cde
commit de08a9b986
4 changed files with 325 additions and 326 deletions

View File

@ -76,7 +76,7 @@ module Stripe
@max_network_retry_delay = 2
@initial_network_retry_delay = 0.5
@ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
@ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
@ca_store = nil
@verify_ssl_certs = true
@ -90,10 +90,6 @@ module Stripe
attr_reader :max_network_retry_delay, :initial_network_retry_delay
end
def self.api_url(url='', api_base_url=nil)
(api_base_url || @api_base) + url
end
# The location of a file containing a bundle of CA certificates. By default
# the library will use an included bundle that can successfully validate
# Stripe certificates.
@ -125,91 +121,6 @@ module Stripe
end
end
# A default Faraday connection to be used when one isn't configured. This
# object should never be mutated, and instead instantiating your own
# connection and wrapping it in a StripeClient object should be preferred.
def self.default_conn
@default_conn ||= Faraday.new do |conn|
conn.use Faraday::Request::UrlEncoded
conn.use Faraday::Response::RaiseError
conn.adapter Faraday.default_adapter
end
end
# TODO: Fix parameters.
def self.request(conn, method, url, api_key, params, headers, api_base_url)
api_base_url = api_base_url || @api_base
unless api_key ||= @api_key
raise AuthenticationError.new('No API key provided. ' \
'Set your API key using "Stripe.api_key = <API-KEY>". ' \
'You can generate API keys from the Stripe web interface. ' \
'See https://stripe.com/api for details, or email support@stripe.com ' \
'if you have any questions.')
end
if api_key =~ /\s/
raise AuthenticationError.new('Your API key is invalid, as it contains ' \
'whitespace. (HINT: You can double-check your API key from the ' \
'Stripe web interface. See https://stripe.com/api for details, or ' \
'email support@stripe.com if you have any questions.)')
end
params = Util.objects_to_ids(params)
url = api_url(url, api_base_url)
case method.to_s.downcase.to_sym
when :get, :head, :delete
# Make params into GET parameters
url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
payload = nil
else
if headers[:content_type] && headers[:content_type] == "multipart/form-data"
payload = params
else
payload = Util.encode_parameters(params)
end
end
if verify_ssl_certs
conn.ssl.verify = true
conn.ssl.cert_store = ca_store
else
conn.ssl.verify = false
unless @verify_ssl_warned
@verify_ssl_warned = true
$stderr.puts("WARNING: Running without SSL cert verification. " \
"You should never do this in production. " \
"Execute 'Stripe.verify_ssl_certs = true' to enable verification.")
end
end
http_resp = execute_request_with_rescues(api_base_url, 0) do
conn.run_request(
method,
url,
payload,
# TODO: Convert RestClient-style parameters.
request_headers(api_key, method).update(headers)
) do |req|
req.options.open_timeout = open_timeout
req.options.timeout = read_timeout
end
end
begin
resp = StripeResponse.from_faraday_response(http_resp)
rescue JSON::ParserError
raise general_api_error(http_resp.code, http_resp.body)
end
# Allows StripeClient#request to return a response object to a caller.
StripeClient.set_last_response(resp)
[resp, api_key]
end
def self.max_network_retries
@max_network_retries
end
@ -220,210 +131,6 @@ module Stripe
private
def self.execute_request_with_rescues(api_base_url, retry_count, &block)
begin
resp = block.call
# We rescue all exceptions from a request so that we have an easy spot to
# implement our retry logic across the board. We'll re-raise if it's a type
# of exception that we didn't expect to handle.
rescue => e
if should_retry?(e, retry_count)
retry_count = retry_count + 1
sleep sleep_time(retry_count)
retry
end
case e
when Faraday::ClientError
if e.response
handle_api_error(e.response)
else
handle_network_error(e, retry_count, api_base_url)
end
# Only handle errors when we know we can do so, and re-raise otherwise.
# This should be pretty infrequent.
else
raise
end
end
resp
end
def self.general_api_error(status, body)
APIError.new("Invalid response object from API: #{body.inspect} " +
"(HTTP response code was #{status})", status, body)
end
def self.get_uname
if File.exist?('/proc/version')
File.read('/proc/version').strip
else
case RbConfig::CONFIG['host_os']
when /linux|darwin|bsd|sunos|solaris|cygwin/i
get_uname_from_system
when /mswin|mingw/i
get_uname_from_system_ver
else
"unknown platform"
end
end
end
def self.get_uname_from_system
(`uname -a 2>/dev/null` || '').strip
rescue Errno::ENOENT
"uname executable not found"
rescue Errno::ENOMEM # couldn't create subprocess
"uname lookup failed"
end
def self.get_uname_from_system_ver
(`ver` || '').strip
rescue Errno::ENOENT
"ver executable not found"
rescue Errno::ENOMEM # couldn't create subprocess
"uname lookup failed"
end
def self.handle_api_error(http_resp)
begin
resp = StripeResponse.from_faraday_hash(http_resp)
error = resp.data[:error]
raise StripeError.new unless error && error.is_a?(Hash)
rescue JSON::ParserError, StripeError
raise general_api_error(http_resp[:status], http_resp[:body])
end
case resp.http_status
when 400, 404
error = InvalidRequestError.new(
error[:message], error[:param],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 401
error = AuthenticationError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 402
error = CardError.new(
error[:message], error[:param], error[:code],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 403
error = PermissionError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 429
error = RateLimitError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
else
error = APIError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
end
error.response = resp
raise(error)
end
def self.handle_network_error(e, retry_count, api_base_url=nil)
case e
when Faraday::ConnectionFailed
message = "Unexpected error communicating when trying to connect to Stripe. " \
"You may be seeing this message because your DNS is not working. " \
"To check, try running 'host stripe.com' from the command line."
when Faraday::SSLError
message = "Could not establish a secure connection to Stripe, you may " \
"need to upgrade your OpenSSL version. To check, try running " \
"'openssl s_client -connect api.stripe.com:443' from the " \
"command line."
when Faraday::TimeoutError
api_base_url = @api_base unless api_base_url
message = "Could not connect to Stripe (#{api_base_url}). " \
"Please check your internet connection and try again. " \
"If this problem persists, you should check Stripe's service status at " \
"https://twitter.com/stripestatus, or let us know at support@stripe.com."
else
message = "Unexpected error communicating with Stripe. " \
"If this problem persists, let us know at support@stripe.com."
end
if retry_count > 0
message += " Request was retried #{retry_count} times."
end
raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
end
def self.request_headers(api_key, method)
headers = {
'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}",
'Authorization' => "Bearer #{api_key}",
'Content-Type' => 'application/x-www-form-urlencoded'
}
# It is only safe to retry network failures on post and delete
# requests if we add an Idempotency-Key header
if [:post, :delete].include?(method) && self.max_network_retries > 0
headers['Idempotency-Key'] ||= SecureRandom.uuid
end
headers['Stripe-Version'] = api_version if api_version
headers['Stripe-Account'] = stripe_account if stripe_account
begin
headers.update('X-Stripe-Client-User-Agent' => JSON.generate(user_agent))
rescue => e
headers.update('X-Stripe-Client-Raw-User-Agent' => user_agent.inspect,
:error => "#{e} (#{e.class})")
end
end
# Checks if an error is a problem that we should retry on. This includes both
# socket errors that may represent an intermittent problem and some special
# HTTP statuses.
def self.should_retry?(e, retry_count)
return false if retry_count >= self.max_network_retries
# Retry on timeout-related problems (either on open or read).
return true if e.is_a?(Faraday::TimeoutError)
# Destination refused the connection, the connection was reset, or a
# variety of other connection failures. This could occur from a single
# saturated server, so retry in case it's intermittent.
return true if e.is_a?(Faraday::ConnectionFailed)
if e.is_a?(Faraday::ClientError) && e.response
# 409 conflict
return true if e.response[:status] == 409
end
false
end
def self.sleep_time(retry_count)
# Apply exponential backoff with initial_network_retry_delay on the number
# of attempts so far as inputs. Do not allow the number to exceed
# max_network_retry_delay.
sleep_seconds = [initial_network_retry_delay * (2 ** (retry_count - 1)), max_network_retry_delay].min
# Apply some jitter by randomizing the value in the range of (sleep_seconds
# / 2) to (sleep_seconds).
sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
# But never sleep less than the base sleep seconds.
sleep_seconds = [initial_network_retry_delay, sleep_seconds].max
sleep_seconds
end
# DEPRECATED. Use `Util#encode_parameters` instead.
def self.uri_encode(params)
Util.encode_parameters(params)
@ -432,21 +139,4 @@ module Stripe
extend Gem::Deprecate
deprecate :uri_encode, "Stripe::Util#encode_parameters", 2016, 01
end
def self.user_agent
@uname ||= get_uname
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
{
:bindings_version => Stripe::VERSION,
:lang => 'ruby',
:lang_version => lang_version,
:platform => RUBY_PLATFORM,
:engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
:publisher => 'stripe',
:uname => @uname,
:hostname => Socket.gethostname,
}
end
end

View File

@ -2,19 +2,20 @@ module Stripe
module APIOperations
module Request
module ClassMethods
OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :conn, :stripe_account, :stripe_version]
OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :client, :stripe_account, :stripe_version]
def request(method, url, params={}, opts={})
opts = Util.normalize_opts(opts)
opts[:conn] ||= Stripe.default_conn
opts[:client] ||= StripeClient.active_client
headers = opts.clone
api_key = headers.delete(:api_key)
api_base = headers.delete(:api_base)
conn = headers.delete(:conn)
client = headers.delete(:client)
# Assume all remaining opts must be headers
resp, opts[:api_key] = Stripe.request(conn, method, url, api_key, params, headers, api_base)
resp, opts[:api_key] = client.execute_request(
method, url, api_key, params, headers, api_base)
# Hash#select returns an array before 1.9
opts_to_persist = {}

View File

@ -8,14 +8,25 @@ module Stripe
# Initializes a new StripeClient. Expects a Faraday connection object, and
# uses a default connection unless one is passed.
def initialize(conn = nil)
self.conn = conn || Stripe.default_conn
self.conn = conn || self.class.default_conn
end
# Sets the last StripeResponse object to have come back from an API call.
# This is expected to be called by the Stripe module.
def self.set_last_response(resp)
if Thread.current[:stripe_client]
Thread.current[:stripe_last_response] = resp
def self.active_client
Thread.current[:stripe_client] || default_client
end
def self.default_client
@default_client ||= StripeClient.new(default_conn)
end
# A default Faraday connection to be used when one isn't configured. This
# object should never be mutated, and instead instantiating your own
# connection and wrapping it in a StripeClient object should be preferred.
def self.default_conn
@default_conn ||= Faraday.new do |conn|
conn.use Faraday::Request::UrlEncoded
conn.use Faraday::Response::RaiseError
conn.adapter Faraday.default_adapter
end
end
@ -25,19 +36,315 @@ module Stripe
# charge, resp = client.request { Charge.create }
#
def request(&block)
@last_response = nil
old_stripe_client = Thread.current[:stripe_client]
old_stripe_last_response = Thread.current[:stripe_last_response]
Thread.current[:stripe_client] = self
Thread.current[:stripe_last_response] = nil
begin
res = block.call
[res, Thread.current[:stripe_last_response]]
[res, @last_response]
ensure
Thread.current[:stripe_client] = old_stripe_client
Thread.current[:stripe_last_response] = old_stripe_last_response
end
end
# TODO: Fix parameters.
def execute_request(method, url, api_key, params, headers, api_base_url)
api_base_url = api_base_url || @api_base
unless api_key ||= Stripe.api_key
raise AuthenticationError.new('No API key provided. ' \
'Set your API key using "Stripe.api_key = <API-KEY>". ' \
'You can generate API keys from the Stripe web interface. ' \
'See https://stripe.com/api for details, or email support@stripe.com ' \
'if you have any questions.')
end
if api_key =~ /\s/
raise AuthenticationError.new('Your API key is invalid, as it contains ' \
'whitespace. (HINT: You can double-check your API key from the ' \
'Stripe web interface. See https://stripe.com/api for details, or ' \
'email support@stripe.com if you have any questions.)')
end
params = Util.objects_to_ids(params)
url = api_url(url, api_base_url)
case method.to_s.downcase.to_sym
when :get, :head, :delete
# Make params into GET parameters
url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
payload = nil
else
if headers[:content_type] && headers[:content_type] == "multipart/form-data"
payload = params
else
payload = Util.encode_parameters(params)
end
end
if Stripe.verify_ssl_certs
conn.ssl.verify = true
conn.ssl.cert_store = Stripe.ca_store
else
conn.ssl.verify = false
unless @verify_ssl_warned
@verify_ssl_warned = true
$stderr.puts("WARNING: Running without SSL cert verification. " \
"You should never do this in production. " \
"Execute 'Stripe.verify_ssl_certs = true' to enable verification.")
end
end
http_resp = execute_request_with_rescues(api_base_url, 0) do
conn.run_request(
method,
url,
payload,
# TODO: Convert RestClient-style parameters.
request_headers(api_key, method).update(headers)
) do |req|
req.options.open_timeout = Stripe.open_timeout
req.options.timeout = Stripe.read_timeout
end
end
begin
resp = StripeResponse.from_faraday_response(http_resp)
rescue JSON::ParserError
raise general_api_error(http_resp.code, http_resp.body)
end
# Allows StripeClient#request to return a response object to a caller.
@last_response = resp
[resp, api_key]
end
private
def api_url(url='', api_base_url=nil)
(api_base_url || Stripe.api_base) + url
end
def execute_request_with_rescues(api_base_url, retry_count, &block)
begin
resp = block.call
# We rescue all exceptions from a request so that we have an easy spot to
# implement our retry logic across the board. We'll re-raise if it's a type
# of exception that we didn't expect to handle.
rescue => e
if should_retry?(e, retry_count)
retry_count = retry_count + 1
sleep sleep_time(retry_count)
retry
end
case e
when Faraday::ClientError
if e.response
handle_api_error(e.response)
else
handle_network_error(e, retry_count, api_base_url)
end
# Only handle errors when we know we can do so, and re-raise otherwise.
# This should be pretty infrequent.
else
raise
end
end
resp
end
def general_api_error(status, body)
APIError.new("Invalid response object from API: #{body.inspect} " +
"(HTTP response code was #{status})", status, body)
end
def get_uname
if File.exist?('/proc/version')
File.read('/proc/version').strip
else
case RbConfig::CONFIG['host_os']
when /linux|darwin|bsd|sunos|solaris|cygwin/i
get_uname_from_system
when /mswin|mingw/i
get_uname_from_system_ver
else
"unknown platform"
end
end
end
def get_uname_from_system
(`uname -a 2>/dev/null` || '').strip
rescue Errno::ENOENT
"uname executable not found"
rescue Errno::ENOMEM # couldn't create subprocess
"uname lookup failed"
end
def get_uname_from_system_ver
(`ver` || '').strip
rescue Errno::ENOENT
"ver executable not found"
rescue Errno::ENOMEM # couldn't create subprocess
"uname lookup failed"
end
def handle_api_error(http_resp)
begin
resp = StripeResponse.from_faraday_hash(http_resp)
error = resp.data[:error]
raise StripeError.new unless error && error.is_a?(Hash)
rescue JSON::ParserError, StripeError
raise general_api_error(http_resp[:status], http_resp[:body])
end
case resp.http_status
when 400, 404
error = InvalidRequestError.new(
error[:message], error[:param],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 401
error = AuthenticationError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 402
error = CardError.new(
error[:message], error[:param], error[:code],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 403
error = PermissionError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
when 429
error = RateLimitError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
else
error = APIError.new(
error[:message],
resp.http_status, resp.http_body, resp.data, resp.http_headers)
end
error.response = resp
raise(error)
end
def handle_network_error(e, retry_count, api_base_url=nil)
case e
when Faraday::ConnectionFailed
message = "Unexpected error communicating when trying to connect to Stripe. " \
"You may be seeing this message because your DNS is not working. " \
"To check, try running 'host stripe.com' from the command line."
when Faraday::SSLError
message = "Could not establish a secure connection to Stripe, you may " \
"need to upgrade your OpenSSL version. To check, try running " \
"'openssl s_client -connect api.stripe.com:443' from the " \
"command line."
when Faraday::TimeoutError
api_base_url = @api_base unless api_base_url
message = "Could not connect to Stripe (#{api_base_url}). " \
"Please check your internet connection and try again. " \
"If this problem persists, you should check Stripe's service status at " \
"https://twitter.com/stripestatus, or let us know at support@stripe.com."
else
message = "Unexpected error communicating with Stripe. " \
"If this problem persists, let us know at support@stripe.com."
end
if retry_count > 0
message += " Request was retried #{retry_count} times."
end
raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
end
# Checks if an error is a problem that we should retry on. This includes both
# socket errors that may represent an intermittent problem and some special
# HTTP statuses.
def should_retry?(e, retry_count)
return false if retry_count >= Stripe.max_network_retries
# Retry on timeout-related problems (either on open or read).
return true if e.is_a?(Faraday::TimeoutError)
# Destination refused the connection, the connection was reset, or a
# variety of other connection failures. This could occur from a single
# saturated server, so retry in case it's intermittent.
return true if e.is_a?(Faraday::ConnectionFailed)
if e.is_a?(Faraday::ClientError) && e.response
# 409 conflict
return true if e.response[:status] == 409
end
false
end
def request_headers(api_key, method)
headers = {
'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}",
'Authorization' => "Bearer #{api_key}",
'Content-Type' => 'application/x-www-form-urlencoded'
}
# It is only safe to retry network failures on post and delete
# requests if we add an Idempotency-Key header
if [:post, :delete].include?(method) && Stripe.max_network_retries > 0
headers['Idempotency-Key'] ||= SecureRandom.uuid
end
headers['Stripe-Version'] = Stripe.api_version if Stripe.api_version
headers['Stripe-Account'] = Stripe.stripe_account if Stripe.stripe_account
begin
headers.update('X-Stripe-Client-User-Agent' => JSON.generate(user_agent))
rescue => e
headers.update('X-Stripe-Client-Raw-User-Agent' => user_agent.inspect,
:error => "#{e} (#{e.class})")
end
end
def sleep_time(retry_count)
# Apply exponential backoff with initial_network_retry_delay on the number
# of attempts so far as inputs. Do not allow the number to exceed
# max_network_retry_delay.
sleep_seconds = [Stripe.initial_network_retry_delay * (2 ** (retry_count - 1)), Stripe.max_network_retry_delay].min
# Apply some jitter by randomizing the value in the range of (sleep_seconds
# / 2) to (sleep_seconds).
sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
# But never sleep less than the base sleep seconds.
sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max
sleep_seconds
end
def user_agent
@uname ||= get_uname
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
{
:bindings_version => Stripe::VERSION,
:lang => 'ruby',
:lang_version => lang_version,
:platform => RUBY_PLATFORM,
:engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
:publisher => 'stripe',
:uname => @uname,
:hostname => Socket.gethostname,
}
end
end
end

View File

@ -22,6 +22,7 @@ module Stripe
assert_equal "request-id", resp.request_id
end
end
context ".from_faraday_response" do
should "converts to StripeResponse" do
body = '{"foo": "bar"}'