Class: Stripe::APIRequestor

Inherits:
Object
  • Object
show all
Extended by:
Gem::Deprecate
Defined in:
lib/stripe/api_requestor.rb

Overview

APIRequestor 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.

Defined Under Namespace

Classes: RequestLogContext, StripeRequestMetrics, SystemProfiler, ThreadContext

Constant Summary collapse

CONNECTION_MANAGER_GC_LAST_USED_EXPIRY =

Time (in seconds) that a connection manager has not been used before it’s eligible for garbage collection.

120
CONNECTION_MANAGER_GC_PERIOD =

How often to check (in seconds) for connection managers that haven’t been used in a long time and which should be garbage collected.

60
ERROR_MESSAGE_CONNECTION =
"Unexpected error communicating when trying to connect to " \
"Stripe (%s). You may be seeing this message because your DNS is not " \
"working or you don't have an internet connection.  To check, try " \
"running `host stripe.com` from the command line."
ERROR_MESSAGE_SSL =
"Could not establish a secure connection to Stripe (%s), you " \
"may need to upgrade your OpenSSL version. To check, try running " \
"`openssl s_client -connect api.stripe.com:443` from the command " \
"line."
ERROR_MESSAGE_TIMEOUT_SUFFIX =

Common error suffix sared by both connect and read timeout messages.

"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 " \
"[email protected]."
ERROR_MESSAGE_TIMEOUT_CONNECT =
(
  "Timed out connecting to Stripe (%s). " +
  ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze
ERROR_MESSAGE_TIMEOUT_READ =
(
  "Timed out communicating with Stripe (%s). " +
  ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_arg = {}) ⇒ APIRequestor

Initializes a new APIRequestor



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/stripe/api_requestor.rb', line 17

def initialize(config_arg = {})
  @system_profiler = SystemProfiler.new
  @last_request_metrics = Queue.new

  @config = case config_arg
            when Hash
              StripeConfiguration.new.reverse_duplicate_merge(config_arg)
            when Stripe::StripeConfiguration
              config_arg
            when String
              StripeConfiguration.new.reverse_duplicate_merge(
                { api_key: config_arg }
              )
            else
              raise ArgumentError, "Can't handle argument: #{config_arg}"
            end
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



35
36
37
# File 'lib/stripe/api_requestor.rb', line 35

def config
  @config
end

#optionsObject (readonly)

Returns the value of attribute options.



35
36
37
# File 'lib/stripe/api_requestor.rb', line 35

def options
  @options
end

Class Method Details

.active_requestorObject

Gets a currently active ‘APIRequestor`. Set for the current thread when `APIRequestor#request` is being run so that API operations being executed inside of that block can find the currently active requestor. It’s reset to the original value (hopefully ‘nil`) after the block ends.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.



44
45
46
# File 'lib/stripe/api_requestor.rb', line 44

def self.active_requestor
  current_thread_context.active_requestor || default_requestor
end

.clear_all_connection_managers(config: nil) ⇒ Object

Finishes any active connections by closing their TCP connection and clears them from internal tracking in all connection managers across all threads.

If passed a ‘config` object, only clear connection managers for that particular configuration.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/stripe/api_requestor.rb', line 57

def self.clear_all_connection_managers(config: nil)
  # Just a quick path for when configuration is being set for the first
  # time before any connections have been opened. There is technically some
  # potential for thread raciness here, but not in a practical sense.
  return if @thread_contexts_with_connection_managers.empty?

  @thread_contexts_with_connection_managers_mutex.synchronize do
    pruned_contexts = Set.new

    @thread_contexts_with_connection_managers.each do |thread_context|
      # Note that the thread context itself is not destroyed, but we clear
      # its connection manager and remove our reference to it. If it ever
      # makes a new request we'll give it a new connection manager and
      # it'll go back into `@thread_contexts_with_connection_managers`.
      thread_context.default_connection_managers.reject! do |cm_config, cm|
        if config.nil? || config.key == cm_config
          cm.clear
          true
        end
      end

      pruned_contexts << thread_context if thread_context.default_connection_managers.empty?
    end

    @thread_contexts_with_connection_managers.subtract(pruned_contexts)
  end
end

.current_thread_contextObject

Access data stored for ‘APIRequestor` within the thread’s current context. Returns ‘ThreadContext`.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.



400
401
402
# File 'lib/stripe/api_requestor.rb', line 400

def self.current_thread_context
  Thread.current[:api_requestor__internal_use_only] ||= ThreadContext.new
end

.default_connection_manager(config = Stripe.config) ⇒ Object

A default connection manager for the current thread scoped to the configuration object that may be provided.



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/stripe/api_requestor.rb', line 92

def self.default_connection_manager(config = Stripe.config)
  current_thread_context.default_connection_managers[config.key] ||= begin
    connection_manager = ConnectionManager.new(config)

    @thread_contexts_with_connection_managers_mutex.synchronize do
      maybe_gc_connection_managers
      @thread_contexts_with_connection_managers << current_thread_context
    end

    connection_manager
  end
end

.default_requestorObject

A default requestor for the current thread.



86
87
88
# File 'lib/stripe/api_requestor.rb', line 86

def self.default_requestor
  current_thread_context.default_requestor ||= APIRequestor.new(Stripe.config)
end

.maybe_gc_connection_managersObject

Garbage collects connection managers that haven’t been used in some time, with the idea being that we want to remove old connection managers that belong to dead threads and the like.

Prefixed with ‘maybe_` because garbage collection will only run periodically so that we’re not constantly engaged in busy work. If connection managers live a little passed their useful age it’s not harmful, so it’s not necessary to get them right away.

For testability, returns ‘nil` if it didn’t run and the number of connection managers that were garbage collected otherwise.

IMPORTANT: This method is not thread-safe and expects to be called inside a lock on ‘@thread_contexts_with_connection_managers_mutex`.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/stripe/api_requestor.rb', line 421

def self.maybe_gc_connection_managers
  next_gc_time = @last_connection_manager_gc + CONNECTION_MANAGER_GC_PERIOD
  return nil if next_gc_time > Util.monotonic_time

  last_used_threshold =
    Util.monotonic_time - CONNECTION_MANAGER_GC_LAST_USED_EXPIRY

  pruned_contexts = []
  @thread_contexts_with_connection_managers.each do |thread_context|
    thread_context
      .default_connection_managers
      .each do |config_key, connection_manager|
        next if connection_manager.last_used > last_used_threshold

        connection_manager.clear
        thread_context.default_connection_managers.delete(config_key)
      end
  end

  @thread_contexts_with_connection_managers.each do |thread_context|
    next unless thread_context.default_connection_managers.empty?

    pruned_contexts << thread_context
  end

  @thread_contexts_with_connection_managers -= pruned_contexts
  @last_connection_manager_gc = Util.monotonic_time

  pruned_contexts.count
end

.should_retry?(error, num_retries:, config: Stripe.config) ⇒ Boolean

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.

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/stripe/api_requestor.rb', line 108

def self.should_retry?(error,
                       num_retries:, config: Stripe.config)
  return false if num_retries >= config.max_network_retries

  case error
  when Net::OpenTimeout, Net::ReadTimeout
    # Retry on timeout-related problems (either on open or read).
    true
  when EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET, # rubocop:todo Lint/DuplicateBranch
        Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
    # 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.
    true
  when Stripe::StripeError
    # The API may ask us not to retry (e.g. if doing so would be a no-op),
    # or advise us to retry (e.g. in cases of lock timeouts). Defer to
    # those instructions if given.
    return false if error.http_headers["stripe-should-retry"] == "false"
    return true if error.http_headers["stripe-should-retry"] == "true"

    # 409 Conflict
    return true if error.http_status == 409

    # 429 Too Many Requests
    #
    # There are a few different problems that can lead to a 429. The most
    # common is rate limiting, on which we *don't* want to retry because
    # that'd likely contribute to more contention problems. However, some
    # 429s are lock timeouts, which is when a request conflicted with
    # another request or an internal process on some particular object.
    # These 429s are safe to retry.
    return true if error.http_status == 429 && error.code == "lock_timeout"

    # Retry on 500, 503, and other internal errors.
    #
    # Note that we expect the stripe-should-retry header to be false
    # in most cases when a 500 is returned, since our idempotency framework
    # would typically replay it anyway.
    true if error.http_status >= 500
  else
    false
  end
end

.sleep_time(num_retries, config: Stripe.config) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/stripe/api_requestor.rb', line 153

def self.sleep_time(num_retries, config: Stripe.config)
  # 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 = [
    config.initial_network_retry_delay * (2**(num_retries - 1)),
    config.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.
  [config.initial_network_retry_delay, sleep_seconds].max
end

Instance Method Details

#execute_request(method, path, base_address, params: {}, opts: {}, usage: []) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/stripe/api_requestor.rb', line 197

def execute_request(method, path, base_address,
                    params: {}, opts: {}, usage: [])
  http_resp, req_opts = execute_request_internal(
    method, path, base_address, params, opts, usage
  )
  req_opts = RequestOptions.extract_opts_from_hash(req_opts)

  resp = interpret_response(http_resp)

  # If being called from `APIRequestor#request`, put the last response in
  # thread-local memory so that it can be returned to the user. Don't store
  # anything otherwise so that we don't leak memory.
  store_last_response(object_id, resp)

  api_mode = Util.get_api_mode(path)
  Util.convert_to_stripe_object_with_params(resp.data, params, RequestOptions.persistable(req_opts), resp,
                                            api_mode: api_mode, requestor: self)
end

#execute_request_initialize_from(method, path, base_address, object, params: {}, opts: {}, usage: []) ⇒ Object

Execute request without instantiating a new object if the relevant object’s name matches the class

For internal use only. Does not provide a stable API and may be broken with future non-major changes.



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/stripe/api_requestor.rb', line 220

def execute_request_initialize_from(method, path, base_address, object,
                                    params: {}, opts: {}, usage: [])
  opts = RequestOptions.combine_opts(object.instance_variable_get(:@opts), opts)
  opts = Util.normalize_opts(opts)
  http_resp, req_opts = execute_request_internal(
    method, path, base_address, params, opts, usage
  )
  req_opts = RequestOptions.extract_opts_from_hash(req_opts)

  resp = interpret_response(http_resp)

  # If being called from `APIRequestor#request`, put the last response in
  # thread-local memory so that it can be returned to the user. Don't store
  # anything otherwise so that we don't leak memory.
  store_last_response(object_id, resp)

  if Util.object_name_matches_class?(resp.data[:object], object.class)
    object.send(:initialize_from,
                resp.data, RequestOptions.persistable(req_opts), resp,
                api_mode: :v1, requestor: self)
  else
    Util.convert_to_stripe_object_with_params(resp.data, params,
                                              RequestOptions.persistable(req_opts),
                                              resp, api_mode: :v1, requestor: self)
  end
end

#execute_request_stream(method, path, base_address, params: {}, opts: {}, usage: [], &read_body_chunk_block) ⇒ Object

Executes a request and returns the body as a stream instead of converting it to a StripeObject. This should be used for any request where we expect an arbitrary binary response.

A ‘read_body_chunk` block can be passed, which will be called repeatedly with the body chunks read from the socket.

If a block is passed, a StripeHeadersOnlyResponse is returned as the block is expected to do all the necessary body processing. If no block is passed, then a StripeStreamResponse is returned containing an IO stream with the response body.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/stripe/api_requestor.rb', line 264

def execute_request_stream(method, path,
                           base_address,
                           params: {}, opts: {}, usage: [],
                           &read_body_chunk_block)
  unless block_given?
    raise ArgumentError,
          "execute_request_stream requires a read_body_chunk_block"
  end

  http_resp, api_key = execute_request_internal(
    method, path, base_address, params, opts, usage, &read_body_chunk_block
  )

  # When the read_body_chunk_block is given, we no longer have access to the
  # response body at this point and so return a response object containing
  # only the headers. This is because the body was consumed by the block.
  resp = StripeHeadersOnlyResponse.from_net_http(http_resp)

  [resp, api_key]
end

#interpret_response(http_resp) ⇒ Object



247
248
249
250
251
# File 'lib/stripe/api_requestor.rb', line 247

def interpret_response(http_resp)
  StripeResponse.from_net_http(http_resp)
rescue JSON::ParserError
  raise general_api_error(http_resp.code.to_i, http_resp.body)
end

#last_response_has_key?(object_id) ⇒ Boolean

Returns:

  • (Boolean)


291
292
293
# File 'lib/stripe/api_requestor.rb', line 291

def last_response_has_key?(object_id)
  self.class.current_thread_context.last_responses&.key?(object_id)
end

#requestObject

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

client = APIRequestor.new
charge, resp = client.request { Charge.create }


175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/stripe/api_requestor.rb', line 175

def request
  old_api_requestor = self.class.current_thread_context.active_requestor
  self.class.current_thread_context.active_requestor = self

  if self.class.current_thread_context.last_responses&.key?(object_id)
    raise "calls to APIRequestor#request cannot be nested within a thread"
  end

  self.class.current_thread_context.last_responses ||= {}
  self.class.current_thread_context.last_responses[object_id] = nil

  begin
    res = yield
    [res, self.class.current_thread_context.last_responses[object_id]]
  ensure
    self.class.current_thread_context.active_requestor = old_api_requestor
    self.class.current_thread_context.last_responses.delete(object_id)
  end
end

#store_last_response(object_id, resp) ⇒ Object



285
286
287
288
289
# File 'lib/stripe/api_requestor.rb', line 285

def store_last_response(object_id, resp)
  return unless last_response_has_key?(object_id)

  self.class.current_thread_context.last_responses[object_id] = resp
end