class Stripe::StripeClient
contains information on the HTTP call.
recover both a resource a call returns as well as a response object that
StripeClient executes requests against the Stripe API and allows a user to
def self.active_client
def self.active_client Thread.current[:stripe_client] || default_client end
def self.default_client
def self.default_client Thread.current[:stripe_client_default_client] ||= StripeClient.new(default_conn) end
def self.default_conn
object should never be mutated, and instead instantiating your own
A default Faraday connection to be used when one isn't configured. This
def self.default_conn # We're going to keep connections around so that we can take advantage # of connection re-use, so make sure that we have a separate connection # object per thread. Thread.current[:stripe_client_default_conn] ||= begin conn = Faraday.new do |builder| builder.use Faraday::Request::Multipart builder.use Faraday::Request::UrlEncoded builder.use Faraday::Response::RaiseError # Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby, # so fall back to default there. if Gem.win_platform? || RUBY_PLATFORM == "java" builder.adapter :net_http else builder.adapter :net_http_persistent end end conn.proxy = Stripe.proxy if Stripe.proxy 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 warn("WARNING: Running without SSL cert verification. " \ "You should never do this in production. " \ "Execute `Stripe.verify_ssl_certs = true` to enable " \ "verification.") end end conn end end
def self.should_retry?(error, num_retries)
both socket errors that may represent an intermittent problem and some
Checks if an error is a problem that we should retry on. This includes
def self.should_retry?(error, num_retries) return false if num_retries >= Stripe.max_network_retries # Retry on timeout-related problems (either on open or read). return true if error.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 error.is_a?(Faraday::ConnectionFailed) if error.is_a?(Faraday::ClientError) && error.response # 409 conflict return true if error.response[:status] == 409 end false end
def self.sleep_time(num_retries)
def self.sleep_time(num_retries) # Apply exponential backoff with initial_network_retry_delay on the # number of num_retries so far as inputs. Do not allow the number to # exceed max_network_retry_delay. sleep_seconds = [ Stripe.initial_network_retry_delay * (2**(num_retries - 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 *= (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 api_url(url = "", api_base = nil)
def api_url(url = "", api_base = nil) ase || Stripe.api_base) + url
def check_api_key!(api_key)
def check_api_key!(api_key) api_key e AuthenticationError, "No API key provided. " \ et your API key using "Stripe.api_key = <API-KEY>". ' \ ou can generate API keys from the Stripe web interface. " \ ee https://stripe.com/api for details, or email " \ upport@stripe.com if you have any questions." unless api_key =~ /\s/ AuthenticationError, "Your API key is invalid, as it contains " \ tespace. (HINT: You can double-check your API key from the " \ ipe web interface. See https://stripe.com/api for details, or " \ il support@stripe.com if you have any questions.)"
def execute_request(method, path,
def execute_request(method, path, api_base: nil, api_key: nil, headers: {}, params: {}) api_base ||= Stripe.api_base api_key ||= Stripe.api_key params = Util.objects_to_ids(params) check_api_key!(api_key) body = nil query_params = nil case method.to_s.downcase.to_sym when :get, :head, :delete query_params = params else body = params end # This works around an edge case where we end up with both query # parameters in `query_params` and query parameters that are appended # onto the end of the given path. In this case, Faraday will silently # discard the URL's parameters which may break a request. # # Here we decode any parameters that were added onto the end of a path # and add them to `query_params` so that all parameters end up in one # place and all of them are correctly included in the final request. u = URI.parse(path) unless u.query.nil? query_params ||= {} query_params = Hash[URI.decode_www_form(u.query)].merge(query_params) # Reset the path minus any query parameters that were specified. path = u.path end headers = request_headers(api_key, method) .update(Util.normalize_headers(headers)) params_encoder = FaradayStripeEncoder.new url = api_url(path, api_base) # stores information on the request we're about to make so that we don't # have to pass as many parameters around for logging. context = RequestLogContext.new context.account = headers["Stripe-Account"] context.api_key = api_key context.api_version = headers["Stripe-Version"] context.body = body ? params_encoder.encode(body) : nil context.idempotency_key = headers["Idempotency-Key"] context.method = method context.path = path context.query_params = if query_params params_encoder.encode(query_params) end # note that both request body and query params will be passed through # `FaradayStripeEncoder` http_resp = execute_request_with_rescues(api_base, context) do conn.run_request(method, url, body, headers) do |req| req.options.open_timeout = Stripe.open_timeout req.options.params_encoder = params_encoder req.options.timeout = Stripe.read_timeout req.params = query_params unless query_params.nil? end end begin resp = StripeResponse.from_faraday_response(http_resp) rescue JSON::ParserError raise general_api_error(http_resp.status, http_resp.body) end # Allows StripeClient#request to return a response object to a caller. @last_response = resp [resp, api_key] end
def execute_request_with_rescues(api_base, context)
def execute_request_with_rescues(api_base, context) tries = 0 est_start = Time.now request(context, num_retries) = yield ext = context.dup_from_response(resp) response(context, request_start, resp.status, resp.body) tripe.enable_telemetry? && context.request_id quest_duration_ms = ((Time.now - request_start) * 1000).to_int ast_request_metrics = StripeRequestMetrics.new(context.request_id, request_duration_ms) escue all exceptions from a request so that we have an easy spot to ement our retry logic across the board. We'll re-raise if it's a of exception that we didn't expect to handle. StandardError => e we modify context we copy it into a new variable so as not to int the original on a retry. r_context = context .respond_to?(:response) && e.response ror_context = context.dup_from_response(e.response) g_response(error_context, request_start, e.response[:status], e.response[:body]) g_response_error(error_context, request_start, e) elf.class.should_retry?(e, num_retries) m_retries += 1 eep self.class.sleep_time(num_retries) try e Faraday::ClientError e.response handle_error_response(e.response, error_context) se handle_network_error(e, error_context, num_retries, api_base) d ly handle errors when we know we can do so, and re-raise otherwise. is should be pretty infrequent. ise
def format_app_info(info)
the Dashboard. Note that this formatting has been implemented to match
end of a User-Agent string where it'll be fairly prominent in places like
Formats a plugin "app info" hash into a string that we can tack onto the
def format_app_info(info) info[:name] "#{str}/#{info[:version]}" unless info[:version].nil? "#{str} (#{info[:url]})" unless info[:url].nil?
def general_api_error(status, body)
def general_api_error(status, body) or.new("Invalid response object from API: #{body.inspect} " \ "(HTTP response code was #{status})", http_status: status, http_body: body)
def handle_error_response(http_resp, context)
def handle_error_response(http_resp, context) = StripeResponse.from_faraday_hash(http_resp) r_data = resp.data[:error] e StripeError, "Indeterminate error" unless error_data JSON::ParserError, StripeError e general_api_error(http_resp[:status], http_resp[:body]) = if error_data.is_a?(String) specific_oauth_error(resp, error_data, context) else specific_api_error(resp, error_data, context) end response = resp error)
def handle_network_error(error, context, num_retries,
def handle_network_error(error, context, num_retries, api_base = nil) og_error("Stripe network error", error_message: error.message, idempotency_key: context.idempotency_key, request_id: context.request_id) rror araday::ConnectionFailed age = "Unexpected error communicating when trying to connect to " \ tripe. You may be seeing this message because your DNS is not" \ orking. To check, try running `host stripe.com` from the " \ ommand line." araday::SSLError age = "Could not establish a secure connection to Stripe, you " \ ay need to upgrade your OpenSSL version. To check, try running " \ openssl s_client -connect api.stripe.com:443` from the command " \ ine." araday::TimeoutError base ||= Stripe.api_base age = "Could not connect to Stripe (#{api_base}). " \ lease check your internet connection and try again. " \ f this problem persists, you should check Stripe's service " \ tatus at https://status.stripe.com, or let us know at " \ upport@stripe.com." age = "Unexpected error communicating with Stripe. " \ f this problem persists, let us know at support@stripe.com." e += " Request was retried #{num_retries} times." if num_retries > 0 APIConnectionError, message + "\n\n(Network error: #{error.message})"
def initialize(conn = nil)
Initializes a new StripeClient. Expects a Faraday connection object, and
def initialize(conn = nil) self.conn = conn || self.class.default_conn @system_profiler = SystemProfiler.new @last_request_metrics = nil end
def log_request(context, num_retries)
def log_request(context, num_retries) og_info("Request to Stripe API", account: context.account, api_version: context.api_version, idempotency_key: context.idempotency_key, method: context.method, num_retries: num_retries, path: context.path) og_debug("Request details", body: context.body, idempotency_key: context.idempotency_key, query_params: context.query_params)
def log_response(context, request_start, status, body)
def log_response(context, request_start, status, body) og_info("Response from Stripe API", account: context.account, api_version: context.api_version, elapsed: Time.now - request_start, idempotency_key: context.idempotency_key, method: context.method, path: context.path, request_id: context.request_id, status: status) og_debug("Response details", body: body, idempotency_key: context.idempotency_key, request_id: context.request_id) unless context.request_id og_debug("Dashboard link for request", idempotency_key: context.idempotency_key, request_id: context.request_id, url: Util.request_id_dashboard_url(context.request_id, context.api_key))
def log_response_error(context, request_start, error)
def log_response_error(context, request_start, error) og_error("Request error", elapsed: Time.now - request_start, error_message: error.message, idempotency_key: context.idempotency_key, method: context.method, path: context.path)
def request
charge, resp = client.request { Charge.create }
client = StripeClient.new
Executes the API call within the given block. Usage looks like:
def request @last_response = nil old_stripe_client = Thread.current[:stripe_client] Thread.current[:stripe_client] = self begin res = yield [res, @last_response] ensure Thread.current[:stripe_client] = old_stripe_client end end
def request_headers(api_key, method)
def request_headers(api_key, method) gent = "Stripe/v1 RubyBindings/#{Stripe::VERSION}" Stripe.app_info.nil? _agent += " " + format_app_info(Stripe.app_info) s = { r-Agent" => user_agent, horization" => "Bearer #{api_key}", tent-Type" => "application/x-www-form-urlencoded", ipe.enable_telemetry? && !@last_request_metrics.nil? ers["X-Stripe-Client-Telemetry"] = JSON.generate( st_request_metrics: @last_request_metrics.payload s only safe to retry network failures on post and delete ests if we add an Idempotency-Key header post delete].include?(method) && Stripe.max_network_retries > 0 ers["Idempotency-Key"] ||= SecureRandom.uuid s["Stripe-Version"] = Stripe.api_version if Stripe.api_version s["Stripe-Account"] = Stripe.stripe_account if Stripe.stripe_account gent = @system_profiler.user_agent ers.update( -Stripe-Client-User-Agent" => JSON.generate(user_agent) StandardError => e ers.update( -Stripe-Client-Raw-User-Agent" => user_agent.inspect, rror => "#{e} (#{e.class})" s
def specific_api_error(resp, error_data, context)
def specific_api_error(resp, error_data, context) og_error("Stripe API error", status: resp.http_status, error_code: error_data[:code], error_message: error_data[:message], error_param: error_data[:param], error_type: error_data[:type], idempotency_key: context.idempotency_key, request_id: context.request_id) standard set of arguments that can be used to initialize most of exceptions. { _body: resp.http_body, _headers: resp.http_headers, _status: resp.http_status, _body: resp.data, : error_data[:code], esp.http_status 00, 404 error_data[:type] "idempotency_error" empotencyError.new(error_data[:message], opts) validRequestError.new( error_data[:message], error_data[:param], opts 01 enticationError.new(error_data[:message], opts) 02 DO: modify CardError constructor to make code a keyword argument so we don't have to delete it from opts .delete(:code) Error.new( ror_data[:message], error_data[:param], error_data[:code], ts 03 issionError.new(error_data[:message], opts) 29 LimitError.new(error_data[:message], opts) rror.new(error_data[:message], opts)
def specific_oauth_error(resp, error_code, context)
Attempts to look at a response's error code and return an OAuth error if
def specific_oauth_error(resp, error_code, context) ption = resp.data[:error_description] || error_code og_error("Stripe OAuth error", status: resp.http_status, error_code: error_code, error_description: description, idempotency_key: context.idempotency_key, request_id: context.request_id) [error_code, description, { _status: resp.http_status, http_body: resp.http_body, _body: resp.data, http_headers: resp.http_headers, rror_code invalid_client" h::InvalidClientError.new(*args) invalid_grant" h::InvalidGrantError.new(*args) invalid_request" h::InvalidRequestError.new(*args) invalid_scope" h::InvalidScopeError.new(*args) unsupported_grant_type" h::UnsupportedGrantTypeError.new(*args) unsupported_response_type" h::UnsupportedResponseTypeError.new(*args) 'd prefer that all errors are typed, but we create a generic uthError in case we run into a code that we don't recognize. h::OAuthError.new(*args)