class Stripe::StripeClient

StripeClient executes requests against the Stripe API and allows a user to recover both a resource a call returns as well as a response object that contains information on the HTTP call.

Attributes

conn[RW]

Public Class Methods

active_client() click to toggle source
# File lib/stripe/stripe_client.rb, line 18
def self.active_client
  Thread.current[:stripe_client] || default_client
end
default_client() click to toggle source
# File lib/stripe/stripe_client.rb, line 22
def self.default_client
  Thread.current[:stripe_client_default_client] ||=
    StripeClient.new(default_conn)
end
default_conn() click to toggle source

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.

# File lib/stripe/stripe_client.rb, line 30
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
new(conn = nil) click to toggle source

Initializes a new StripeClient. Expects a Faraday connection object, and uses a default connection unless one is passed.

# File lib/stripe/stripe_client.rb, line 12
def initialize(conn = nil)
  self.conn = conn || self.class.default_conn
  @system_profiler = SystemProfiler.new
  @last_request_metrics = nil
end
should_retry?(error, num_retries) click to toggle source

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.

# File lib/stripe/stripe_client.rb, line 73
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
sleep_time(num_retries) click to toggle source
# File lib/stripe/stripe_client.rb, line 92
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

Public Instance Methods

execute_request(method, path, api_base: nil, api_key: nil, headers: {}, params: {}) click to toggle source
# File lib/stripe/stripe_client.rb, line 129
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
request() { || ... } click to toggle source

Executes the API call within the given block. Usage looks like:

client = StripeClient.new
charge, resp = client.request { Charge.create }
# File lib/stripe/stripe_client.rb, line 116
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

Private Instance Methods

api_url(url = "", api_base = nil) click to toggle source
# File lib/stripe/stripe_client.rb, line 240
        def api_url(url = "", api_base = nil)
  (api_base || Stripe.api_base) + url
end
check_api_key!(api_key) click to toggle source
# File lib/stripe/stripe_client.rb, line 244
        def check_api_key!(api_key)
  unless api_key
    raise AuthenticationError, "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

  return unless api_key =~ /\s/

  raise AuthenticationError, "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
execute_request_with_rescues(api_base, context) { || ... } click to toggle source
# File lib/stripe/stripe_client.rb, line 261
        def execute_request_with_rescues(api_base, context)
  num_retries = 0
  begin
    request_start = Time.now
    log_request(context, num_retries)
    resp = yield
    context = context.dup_from_response(resp)
    log_response(context, request_start, resp.status, resp.body)

    if Stripe.enable_telemetry? && context.request_id
      request_duration_ms = ((Time.now - request_start) * 1000).to_int
      @last_request_metrics =
        StripeRequestMetrics.new(context.request_id, request_duration_ms)
    end

  # 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 StandardError => e
    # If we modify context we copy it into a new variable so as not to
    # taint the original on a retry.
    error_context = context

    if e.respond_to?(:response) && e.response
      error_context = context.dup_from_response(e.response)
      log_response(error_context, request_start,
                   e.response[:status], e.response[:body])
    else
      log_response_error(error_context, request_start, e)
    end

    if self.class.should_retry?(e, num_retries)
      num_retries += 1
      sleep self.class.sleep_time(num_retries)
      retry
    end

    case e
    when Faraday::ClientError
      if e.response
        handle_error_response(e.response, error_context)
      else
        handle_network_error(e, error_context, num_retries, api_base)
      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
format_app_info(info) click to toggle source

Formats a plugin “app info” hash into a string that we can tack onto the end of a User-Agent string where it'll be fairly prominent in places like the Dashboard. Note that this formatting has been implemented to match other libraries, and shouldn't be changed without universal consensus.

# File lib/stripe/stripe_client.rb, line 326
        def format_app_info(info)
  str = info[:name]
  str = "#{str}/#{info[:version]}" unless info[:version].nil?
  str = "#{str} (#{info[:url]})" unless info[:url].nil?
  str
end
general_api_error(status, body) click to toggle source
# File lib/stripe/stripe_client.rb, line 316
        def general_api_error(status, body)
  APIError.new("Invalid response object from API: #{body.inspect} " \
               "(HTTP response code was #{status})",
               http_status: status, http_body: body)
end
handle_error_response(http_resp, context) click to toggle source
# File lib/stripe/stripe_client.rb, line 333
        def handle_error_response(http_resp, context)
  begin
    resp = StripeResponse.from_faraday_hash(http_resp)
    error_data = resp.data[:error]

    raise StripeError, "Indeterminate error" unless error_data
  rescue JSON::ParserError, StripeError
    raise general_api_error(http_resp[:status], http_resp[:body])
  end

  error = if error_data.is_a?(String)
            specific_oauth_error(resp, error_data, context)
          else
            specific_api_error(resp, error_data, context)
          end

  error.response = resp
  raise(error)
end
handle_network_error(error, context, num_retries, api_base = nil) click to toggle source
# File lib/stripe/stripe_client.rb, line 440
        def handle_network_error(error, context, num_retries,
                                 api_base = nil)
  Util.log_error("Stripe network error",
                 error_message: error.message,
                 idempotency_key: context.idempotency_key,
                 request_id: context.request_id)

  case error
  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 ||= Stripe.api_base
    message = "Could not connect to Stripe (#{api_base}). " \
      "Please check your internet connection and try again. " \
      "If this problem persists, you should check Stripe's service " \
      "status at https://status.stripe.com, 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

  message += " Request was retried #{num_retries} times." if num_retries > 0

  raise APIConnectionError,
        message + "\n\n(Network error: #{error.message})"
end
log_request(context, num_retries) click to toggle source
# File lib/stripe/stripe_client.rb, line 522
        def log_request(context, num_retries)
  Util.log_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)
  Util.log_debug("Request details",
                 body: context.body,
                 idempotency_key: context.idempotency_key,
                 query_params: context.query_params)
end
log_response(context, request_start, status, body) click to toggle source
# File lib/stripe/stripe_client.rb, line 536
        def log_response(context, request_start, status, body)
  Util.log_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)
  Util.log_debug("Response details",
                 body: body,
                 idempotency_key: context.idempotency_key,
                 request_id: context.request_id)

  return unless context.request_id

  Util.log_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))
end
log_response_error(context, request_start, error) click to toggle source
# File lib/stripe/stripe_client.rb, line 560
        def log_response_error(context, request_start, error)
  Util.log_error("Request error",
                 elapsed: Time.now - request_start,
                 error_message: error.message,
                 idempotency_key: context.idempotency_key,
                 method: context.method,
                 path: context.path)
end
request_headers(api_key, method) click to toggle source
# File lib/stripe/stripe_client.rb, line 480
        def request_headers(api_key, method)
  user_agent = "Stripe/v1 RubyBindings/#{Stripe::VERSION}"
  unless Stripe.app_info.nil?
    user_agent += " " + format_app_info(Stripe.app_info)
  end

  headers = {
    "User-Agent" => user_agent,
    "Authorization" => "Bearer #{api_key}",
    "Content-Type" => "application/x-www-form-urlencoded",
  }

  if Stripe.enable_telemetry? && !@last_request_metrics.nil?
    headers["X-Stripe-Client-Telemetry"] = JSON.generate(
      last_request_metrics: @last_request_metrics.payload
    )
  end

  # It is only safe to retry network failures on post and delete
  # requests if we add an Idempotency-Key header
  if %i[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

  user_agent = @system_profiler.user_agent
  begin
    headers.update(
      "X-Stripe-Client-User-Agent" => JSON.generate(user_agent)
    )
  rescue StandardError => e
    headers.update(
      "X-Stripe-Client-Raw-User-Agent" => user_agent.inspect,
      :error => "#{e} (#{e.class})"
    )
  end

  headers
end
specific_api_error(resp, error_data, context) click to toggle source
# File lib/stripe/stripe_client.rb, line 353
        def specific_api_error(resp, error_data, context)
  Util.log_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)

  # The standard set of arguments that can be used to initialize most of
  # the exceptions.
  opts = {
    http_body: resp.http_body,
    http_headers: resp.http_headers,
    http_status: resp.http_status,
    json_body: resp.data,
    code: error_data[:code],
  }

  case resp.http_status
  when 400, 404
    case error_data[:type]
    when "idempotency_error"
      IdempotencyError.new(error_data[:message], opts)
    else
      InvalidRequestError.new(
        error_data[:message], error_data[:param],
        opts
      )
    end
  when 401
    AuthenticationError.new(error_data[:message], opts)
  when 402
    # TODO: modify CardError constructor to make code a keyword argument
    #       so we don't have to delete it from opts
    opts.delete(:code)
    CardError.new(
      error_data[:message], error_data[:param], error_data[:code],
      opts
    )
  when 403
    PermissionError.new(error_data[:message], opts)
  when 429
    RateLimitError.new(error_data[:message], opts)
  else
    APIError.new(error_data[:message], opts)
  end
end
specific_oauth_error(resp, error_code, context) click to toggle source

Attempts to look at a response's error code and return an OAuth error if one matches. Will return `nil` if the code isn't recognized.

# File lib/stripe/stripe_client.rb, line 405
        def specific_oauth_error(resp, error_code, context)
  description = resp.data[:error_description] || error_code

  Util.log_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)

  args = [error_code, description, {
    http_status: resp.http_status, http_body: resp.http_body,
    json_body: resp.data, http_headers: resp.http_headers,
  },]

  case error_code
  when "invalid_client"
    OAuth::InvalidClientError.new(*args)
  when "invalid_grant"
    OAuth::InvalidGrantError.new(*args)
  when "invalid_request"
    OAuth::InvalidRequestError.new(*args)
  when "invalid_scope"
    OAuth::InvalidScopeError.new(*args)
  when "unsupported_grant_type"
    OAuth::UnsupportedGrantTypeError.new(*args)
  when "unsupported_response_type"
    OAuth::UnsupportedResponseTypeError.new(*args)
  else
    # We'd prefer that all errors are typed, but we create a generic
    # OAuthError in case we run into a code that we don't recognize.
    OAuth::OAuthError.new(*args)
  end
end