module Stripe
def self.api_error(error, resp, error_obj)
def self.api_error(error, resp, error_obj) APIError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) end
def self.api_url(url='', api_base_url=nil)
def self.api_url(url='', api_base_url=nil) (api_base_url || @api_base) + url end
def self.authentication_error(error, resp, error_obj)
def self.authentication_error(error, resp, error_obj) AuthenticationError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) end
def self.ca_bundle_path
the library will use an included bundle that can successfully validate
The location of a file containing a bundle of CA certificates. By default
def self.ca_bundle_path @ca_bundle_path end
def self.ca_bundle_path=(path)
def self.ca_bundle_path=(path) @ca_bundle_path = path # empty this field so a new store is initialized @ca_store = nil end
def self.ca_store
`Stripe.ca_store`) in their initialization code because it marshals lazily
to leverage this pseudo safety should make a call to this method (i.e.
the most likely point of failure (see issue #382). Any program attempting
when initiating many parallel requests marshaling the certificate store is
This was added to the give the gem "pseudo thread safety" in that it seems
which is used to validate TLS on every request.
A certificate store initialized from the the bundle in #ca_bundle_path and
def self.ca_store @ca_store ||= begin store = OpenSSL::X509::Store.new store.add_file(ca_bundle_path) store end end
def self.card_error(error, resp, error_obj)
def self.card_error(error, resp, error_obj) CardError.new(error[:message], error[:param], error[:code], resp.code, resp.body, error_obj, resp.headers) end
def self.execute_request(opts)
def self.execute_request(opts) RestClient::Request.execute(opts) end
def self.execute_request_with_rescues(request_opts, api_base_url, retry_count = 0)
def self.execute_request_with_rescues(request_opts, api_base_url, retry_count = 0) begin response = execute_request(request_opts) # 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 SocketError response = handle_restclient_error(e, request_opts, retry_count, api_base_url) when RestClient::ExceptionWithResponse if e.response handle_api_error(e.response) else response = handle_restclient_error(e, request_opts, retry_count, api_base_url) end when RestClient::Exception, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError response = handle_restclient_error(e, request_opts, retry_count, api_base_url) # Only handle errors when we know we can do so, and re-raise otherwise. # This should be pretty infrequent. else raise end end response end
def self.general_api_error(rcode, rbody)
def self.general_api_error(rcode, rbody) APIError.new("Invalid response object from API: #{rbody.inspect} " + "(HTTP response code was #{rcode})", rcode, rbody) end
def self.get_uname
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
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
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(resp)
def self.handle_api_error(resp) begin error_obj = JSON.parse(resp.body) error_obj = Util.symbolize_names(error_obj) error = error_obj[:error] raise StripeError.new unless error && error.is_a?(Hash) rescue JSON::ParserError, StripeError raise general_api_error(resp.code, resp.body) end case resp.code when 400, 404 raise invalid_request_error(error, resp, error_obj) when 401 raise authentication_error(error, resp, error_obj) when 402 raise card_error(error, resp, error_obj) when 403 raise permission_error(error, resp, error_obj) when 429 raise rate_limit_error(error, resp, error_obj) else raise api_error(error, resp, error_obj) end end
def self.handle_restclient_error(e, request_opts, retry_count, api_base_url=nil)
def self.handle_restclient_error(e, request_opts, retry_count, api_base_url=nil) api_base_url = @api_base unless api_base_url connection_message = "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." case e when RestClient::RequestTimeout message = "Could not connect to Stripe (#{api_base_url}). #{connection_message}" when RestClient::ServerBrokeConnection message = "The connection to the server (#{api_base_url}) broke before the " \ "request completed. #{connection_message}" when OpenSSL::SSL::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 RestClient::SSLCertificateNotVerified message = "Could not verify Stripe's SSL certificate. " \ "Please make sure that your network is not intercepting certificates. " \ "(Try going to https://api.stripe.com/v1 in your browser.) " \ "If this problem persists, let us know at support@stripe.com." when SocketError 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." 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.invalid_request_error(error, resp, error_obj)
def self.invalid_request_error(error, resp, error_obj) InvalidRequestError.new(error[:message], error[:param], resp.code, resp.body, error_obj, resp.headers) end
def self.max_network_retries
def self.max_network_retries @max_network_retries end
def self.max_network_retries=(val)
def self.max_network_retries=(val) @max_network_retries = val.to_i end
def self.parse(response)
def self.parse(response) begin # Would use :symbolize_names => true, but apparently there is # some library out there that makes symbolize_names not work. response = JSON.parse(response.body) rescue JSON::ParserError raise general_api_error(response.code, response.body) end Util.symbolize_names(response) end
def self.permission_error(error, resp, error_obj)
def self.permission_error(error, resp, error_obj) PermissionError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) end
def self.rate_limit_error(error, resp, error_obj)
def self.rate_limit_error(error, resp, error_obj) RateLimitError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) end
def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil)
def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil) 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 if verify_ssl_certs request_opts = {:verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_cert_store => ca_store} else request_opts = {:verify_ssl => 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 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 request_opts.update(:headers => request_headers(api_key, method).update(headers), :method => method, :open_timeout => open_timeout, :payload => payload, :url => url, :timeout => read_timeout) response = execute_request_with_rescues(request_opts, api_base_url) [parse(response), api_key] end
def self.request_headers(api_key, method)
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
def self.should_retry?(e, retry_count)
def self.should_retry?(e, retry_count) retry_count < self.max_network_retries && RETRY_EXCEPTIONS.any? { |klass| e.is_a?(klass) } end
def self.sleep_time(retry_count)
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
def self.uri_encode(params)
def self.uri_encode(params) Util.encode_parameters(params) end
def self.user_agent
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