stripe 5.1.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +0 -6
- data/CHANGELOG.md +15 -0
- data/Gemfile +0 -1
- data/VERSION +1 -1
- data/lib/stripe/api_operations/nested_resource.rb +28 -26
- data/lib/stripe/connection_manager.rb +36 -18
- data/lib/stripe/stripe_client.rb +114 -39
- data/lib/stripe/util.rb +12 -0
- data/lib/stripe/version.rb +1 -1
- data/test/stripe/connection_manager_test.rb +25 -0
- data/test/stripe/stripe_client_test.rb +188 -54
- data/test/test_helper.rb +0 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04bf14074bb94dbb41edf733c9240f770026a9e51647359d4d1d55820c1f3c46
|
4
|
+
data.tar.gz: e5a8433b4284faf095da5ad4ebd102127b005a52b56b7651dadfa579a06188f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e905a1f7fa40887d5c8bd78c428f1d377b42ead6586cf147f6334fafc6264d81ccbdb504e6ef2c8da3e473f40ad18b275f372cc70eea147435252d813ac948b8
|
7
|
+
data.tar.gz: 3d03fb7104356a787b85287455e74157761e92b159db33414685af565fd99e7ff596e9cd1f4ce7dd0e00f44e2c342618fc65f52ad91ed7ccedabd42db6d990c4
|
data/.rubocop.yml
CHANGED
@@ -19,8 +19,17 @@ Layout/IndentFirstHashElement:
|
|
19
19
|
Layout/IndentHeredoc:
|
20
20
|
Enabled: false
|
21
21
|
|
22
|
+
Metrics/BlockLength:
|
23
|
+
Max: 40
|
24
|
+
Exclude:
|
25
|
+
# `context` in tests are blocks and get quite large, so exclude the test
|
26
|
+
# directory from having to adhere to this rule.
|
27
|
+
- "test/**/*.rb"
|
28
|
+
|
22
29
|
Metrics/ClassLength:
|
23
30
|
Exclude:
|
31
|
+
# Test classes get quite large, so exclude the test directory from having
|
32
|
+
# to adhere to this rule.
|
24
33
|
- "test/**/*.rb"
|
25
34
|
|
26
35
|
Metrics/LineLength:
|
data/.rubocop_todo.yml
CHANGED
@@ -10,12 +10,6 @@
|
|
10
10
|
Metrics/AbcSize:
|
11
11
|
Max: 51
|
12
12
|
|
13
|
-
# Offense count: 33
|
14
|
-
# Configuration parameters: CountComments, ExcludedMethods.
|
15
|
-
# ExcludedMethods: refine
|
16
|
-
Metrics/BlockLength:
|
17
|
-
Max: 509
|
18
|
-
|
19
13
|
# Offense count: 12
|
20
14
|
# Configuration parameters: CountComments.
|
21
15
|
Metrics/ClassLength:
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 5.5.0 - 2019-10-03
|
4
|
+
* [#859](https://github.com/stripe/stripe-ruby/pull/859) User-friendly messages and retries for `EOFError`, `Errno::ECONNRESET`, `Errno::ETIMEDOUT`, and `Errno::EHOSTUNREACH` network errors
|
5
|
+
|
6
|
+
## 5.4.1 - 2019-10-01
|
7
|
+
* [#858](https://github.com/stripe/stripe-ruby/pull/858) Drop Timecop dependency
|
8
|
+
|
9
|
+
## 5.4.0 - 2019-10-01
|
10
|
+
* [#857](https://github.com/stripe/stripe-ruby/pull/857) Move to monotonic time for duration calculations
|
11
|
+
|
12
|
+
## 5.3.0 - 2019-10-01
|
13
|
+
* [#853](https://github.com/stripe/stripe-ruby/pull/853) Support `Stripe-Should-Retry` header
|
14
|
+
|
15
|
+
## 5.2.0 - 2019-09-19
|
16
|
+
* [#851](https://github.com/stripe/stripe-ruby/pull/851) Introduce system for garbage collecting connection managers
|
17
|
+
|
3
18
|
## 5.1.1 - 2019-09-04
|
4
19
|
* [#845](https://github.com/stripe/stripe-ruby/pull/845) Transfer the request_id from the http_headers to error.
|
5
20
|
|
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
5.
|
1
|
+
5.5.0
|
@@ -6,7 +6,7 @@ module Stripe
|
|
6
6
|
# that it's possible to do so from a static context (i.e. without a
|
7
7
|
# pre-existing collection of subresources on the parent).
|
8
8
|
#
|
9
|
-
# For
|
9
|
+
# For example, a transfer gains the static methods for reversals so that the
|
10
10
|
# methods `.create_reversal`, `.retrieve_reversal`, `.update_reversal`,
|
11
11
|
# etc. all become available.
|
12
12
|
module NestedResource
|
@@ -14,9 +14,11 @@ module Stripe
|
|
14
14
|
resource_plural: nil)
|
15
15
|
resource_plural ||= "#{resource}s"
|
16
16
|
path ||= resource_plural
|
17
|
+
|
17
18
|
raise ArgumentError, "operations array required" if operations.nil?
|
18
19
|
|
19
20
|
resource_url_method = :"#{resource}s_url"
|
21
|
+
|
20
22
|
define_singleton_method(resource_url_method) do |id, nested_id = nil|
|
21
23
|
url = "#{resource_url}/#{CGI.escape(id)}/#{CGI.escape(path)}"
|
22
24
|
url += "/#{CGI.escape(nested_id)}" unless nested_id.nil?
|
@@ -27,39 +29,39 @@ module Stripe
|
|
27
29
|
case operation
|
28
30
|
when :create
|
29
31
|
define_singleton_method(:"create_#{resource}") \
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
do |id, params = {}, opts = {}|
|
33
|
+
url = send(resource_url_method, id)
|
34
|
+
resp, opts = request(:post, url, params, opts)
|
35
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
36
|
+
end
|
35
37
|
when :retrieve
|
36
38
|
define_singleton_method(:"retrieve_#{resource}") \
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
do |id, nested_id, opts = {}|
|
40
|
+
url = send(resource_url_method, id, nested_id)
|
41
|
+
resp, opts = request(:get, url, {}, opts)
|
42
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
43
|
+
end
|
42
44
|
when :update
|
43
45
|
define_singleton_method(:"update_#{resource}") \
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
do |id, nested_id, params = {}, opts = {}|
|
47
|
+
url = send(resource_url_method, id, nested_id)
|
48
|
+
resp, opts = request(:post, url, params, opts)
|
49
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
50
|
+
end
|
49
51
|
when :delete
|
50
52
|
define_singleton_method(:"delete_#{resource}") \
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
do |id, nested_id, params = {}, opts = {}|
|
54
|
+
url = send(resource_url_method, id, nested_id)
|
55
|
+
resp, opts = request(:delete, url, params, opts)
|
56
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
57
|
+
end
|
56
58
|
when :list
|
57
59
|
define_singleton_method(:"list_#{resource_plural}") \
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
do |id, params = {}, opts = {}|
|
61
|
+
url = send(resource_url_method, id)
|
62
|
+
resp, opts = request(:get, url, params, opts)
|
63
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
64
|
+
end
|
63
65
|
else
|
64
66
|
raise ArgumentError, "Unknown operation: #{operation.inspect}"
|
65
67
|
end
|
@@ -9,24 +9,36 @@ module Stripe
|
|
9
9
|
#
|
10
10
|
# Note that this class in itself is *not* thread safe. We expect it to be
|
11
11
|
# instantiated once per thread.
|
12
|
-
#
|
13
|
-
# Note also that this class doesn't currently clean up after itself because
|
14
|
-
# it expects to only ever have a few connections (unless `.clear` is called
|
15
|
-
# manually). It'd be possible to tank memory by constantly changing the value
|
16
|
-
# of `Stripe.api_base` or the like. A possible improvement might be to detect
|
17
|
-
# and prune old connections whenever a request is executed.
|
18
12
|
class ConnectionManager
|
13
|
+
# Timestamp (in seconds procured from the system's monotonic clock)
|
14
|
+
# indicating when the connection manager last made a request. This is used
|
15
|
+
# by `StripeClient` to determine whether a connection manager should be
|
16
|
+
# garbage collected or not.
|
17
|
+
attr_reader :last_used
|
18
|
+
|
19
19
|
def initialize
|
20
20
|
@active_connections = {}
|
21
|
+
@last_used = Util.monotonic_time
|
22
|
+
|
23
|
+
# A connection manager may be accessed across threads as one thread makes
|
24
|
+
# requests on it while another is trying to clear it (either because it's
|
25
|
+
# trying to garbage collect the manager or trying to clear it because a
|
26
|
+
# configuration setting has changed). That's probably thread-safe already
|
27
|
+
# because of Ruby's GIL, but just in case the library's running on JRuby
|
28
|
+
# or the like, use a mutex to synchronize access in this connection
|
29
|
+
# manager.
|
30
|
+
@mutex = Mutex.new
|
21
31
|
end
|
22
32
|
|
23
33
|
# Finishes any active connections by closing their TCP connection and
|
24
34
|
# clears them from internal tracking.
|
25
35
|
def clear
|
26
|
-
@
|
27
|
-
connection
|
36
|
+
@mutex.synchronize do
|
37
|
+
@active_connections.each do |_, connection|
|
38
|
+
connection.finish
|
39
|
+
end
|
40
|
+
@active_connections = {}
|
28
41
|
end
|
29
|
-
@active_connections = {}
|
30
42
|
end
|
31
43
|
|
32
44
|
# Gets a connection for a given URI. This is for internal use only as it's
|
@@ -35,17 +47,19 @@ module Stripe
|
|
35
47
|
#
|
36
48
|
# `uri` is expected to be a string.
|
37
49
|
def connection_for(uri)
|
38
|
-
|
39
|
-
|
50
|
+
@mutex.synchronize do
|
51
|
+
u = URI.parse(uri)
|
52
|
+
connection = @active_connections[[u.host, u.port]]
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
54
|
+
if connection.nil?
|
55
|
+
connection = create_connection(u)
|
56
|
+
connection.start
|
44
57
|
|
45
|
-
|
46
|
-
|
58
|
+
@active_connections[[u.host, u.port]] = connection
|
59
|
+
end
|
47
60
|
|
48
|
-
|
61
|
+
connection
|
62
|
+
end
|
49
63
|
end
|
50
64
|
|
51
65
|
# Executes an HTTP request to the given URI with the given method. Also
|
@@ -65,6 +79,8 @@ module Stripe
|
|
65
79
|
raise ArgumentError, "query should be a string" \
|
66
80
|
if query && !query.is_a?(String)
|
67
81
|
|
82
|
+
@last_used = Util.monotonic_time
|
83
|
+
|
68
84
|
connection = connection_for(uri)
|
69
85
|
|
70
86
|
u = URI.parse(uri)
|
@@ -74,7 +90,9 @@ module Stripe
|
|
74
90
|
u.path
|
75
91
|
end
|
76
92
|
|
77
|
-
|
93
|
+
@mutex.synchronize do
|
94
|
+
connection.send_request(method.to_s.upcase, path, body, headers)
|
95
|
+
end
|
78
96
|
end
|
79
97
|
|
80
98
|
#
|
data/lib/stripe/stripe_client.rb
CHANGED
@@ -5,18 +5,17 @@ module Stripe
|
|
5
5
|
# recover both a resource a call returns as well as a response object that
|
6
6
|
# contains information on the HTTP call.
|
7
7
|
class StripeClient
|
8
|
-
# A set of all known
|
8
|
+
# A set of all known thread contexts across all threads and a mutex to
|
9
9
|
# synchronize global access to them.
|
10
|
-
@
|
11
|
-
@
|
10
|
+
@thread_contexts_with_connection_managers = []
|
11
|
+
@thread_contexts_with_connection_managers_mutex = Mutex.new
|
12
|
+
@last_connection_manager_gc = Util.monotonic_time
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
#
|
16
|
-
#
|
17
|
-
def initialize(
|
18
|
-
self.connection_manager = connection_manager ||
|
19
|
-
self.class.default_connection_manager
|
14
|
+
# Initializes a new `StripeClient`.
|
15
|
+
#
|
16
|
+
# Takes a connection manager object for backwards compatibility only, and
|
17
|
+
# that use is DEPRECATED.
|
18
|
+
def initialize(_connection_manager = nil)
|
20
19
|
@system_profiler = SystemProfiler.new
|
21
20
|
@last_request_metrics = nil
|
22
21
|
end
|
@@ -42,17 +41,24 @@ module Stripe
|
|
42
41
|
# Just a quick path for when configuration is being set for the first
|
43
42
|
# time before any connections have been opened. There is technically some
|
44
43
|
# potential for thread raciness here, but not in a practical sense.
|
45
|
-
return if @
|
46
|
-
|
47
|
-
@
|
48
|
-
@
|
44
|
+
return if @thread_contexts_with_connection_managers.empty?
|
45
|
+
|
46
|
+
@thread_contexts_with_connection_managers_mutex.synchronize do
|
47
|
+
@thread_contexts_with_connection_managers.each do |thread_context|
|
48
|
+
# Note that the thread context itself is not destroyed, but we clear
|
49
|
+
# its connection manager and remove our reference to it. If it ever
|
50
|
+
# makes a new request we'll give it a new connection manager and
|
51
|
+
# it'll go back into `@thread_contexts_with_connection_managers`.
|
52
|
+
thread_context.default_connection_manager.clear
|
53
|
+
thread_context.default_connection_manager = nil
|
54
|
+
end
|
55
|
+
@thread_contexts_with_connection_managers.clear
|
49
56
|
end
|
50
57
|
end
|
51
58
|
|
52
59
|
# A default client for the current thread.
|
53
60
|
def self.default_client
|
54
|
-
current_thread_context.default_client ||=
|
55
|
-
StripeClient.new(default_connection_manager)
|
61
|
+
current_thread_context.default_client ||= StripeClient.new
|
56
62
|
end
|
57
63
|
|
58
64
|
# A default connection manager for the current thread.
|
@@ -60,8 +66,9 @@ module Stripe
|
|
60
66
|
current_thread_context.default_connection_manager ||= begin
|
61
67
|
connection_manager = ConnectionManager.new
|
62
68
|
|
63
|
-
@
|
64
|
-
|
69
|
+
@thread_contexts_with_connection_managers_mutex.synchronize do
|
70
|
+
maybe_gc_connection_managers
|
71
|
+
@thread_contexts_with_connection_managers << current_thread_context
|
65
72
|
end
|
66
73
|
|
67
74
|
connection_manager
|
@@ -74,17 +81,23 @@ module Stripe
|
|
74
81
|
def self.should_retry?(error, method:, num_retries:)
|
75
82
|
return false if num_retries >= Stripe.max_network_retries
|
76
83
|
|
77
|
-
|
78
|
-
|
79
|
-
|
84
|
+
case error
|
85
|
+
when Net::OpenTimeout, Net::ReadTimeout
|
86
|
+
# Retry on timeout-related problems (either on open or read).
|
87
|
+
true
|
88
|
+
when EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
89
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
|
90
|
+
# Destination refused the connection, the connection was reset, or a
|
91
|
+
# variety of other connection failures. This could occur from a single
|
92
|
+
# saturated server, so retry in case it's intermittent.
|
93
|
+
true
|
94
|
+
when Stripe::StripeError
|
95
|
+
# The API may ask us not to retry (e.g. if doing so would be a no-op),
|
96
|
+
# or advise us to retry (e.g. in cases of lock timeouts). Defer to
|
97
|
+
# those instructions if given.
|
98
|
+
return false if error.http_headers["stripe-should-retry"] == "false"
|
99
|
+
return true if error.http_headers["stripe-should-retry"] == "true"
|
80
100
|
|
81
|
-
# Destination refused the connection, the connection was reset, or a
|
82
|
-
# variety of other connection failures. This could occur from a single
|
83
|
-
# saturated server, so retry in case it's intermittent.
|
84
|
-
return true if error.is_a?(Errno::ECONNREFUSED)
|
85
|
-
return true if error.is_a?(SocketError)
|
86
|
-
|
87
|
-
if error.is_a?(Stripe::StripeError)
|
88
101
|
# 409 Conflict
|
89
102
|
return true if error.http_status == 409
|
90
103
|
|
@@ -106,10 +119,10 @@ module Stripe
|
|
106
119
|
return true if error.http_status == 500 && method != :post
|
107
120
|
|
108
121
|
# 503 Service Unavailable
|
109
|
-
|
122
|
+
error.http_status == 503
|
123
|
+
else
|
124
|
+
false
|
110
125
|
end
|
111
|
-
|
112
|
-
false
|
113
126
|
end
|
114
127
|
|
115
128
|
def self.sleep_time(num_retries)
|
@@ -131,6 +144,15 @@ module Stripe
|
|
131
144
|
sleep_seconds
|
132
145
|
end
|
133
146
|
|
147
|
+
# Gets the connection manager in use for the current `StripeClient`.
|
148
|
+
#
|
149
|
+
# This method is DEPRECATED and for backwards compatibility only.
|
150
|
+
def connection_manager
|
151
|
+
self.class.default_connection_manager
|
152
|
+
end
|
153
|
+
extend Gem::Deprecate
|
154
|
+
deprecate :connection_manager, :none, 2020, 9
|
155
|
+
|
134
156
|
# Executes the API call within the given block. Usage looks like:
|
135
157
|
#
|
136
158
|
# client = StripeClient.new
|
@@ -207,10 +229,10 @@ module Stripe
|
|
207
229
|
context.query = query
|
208
230
|
|
209
231
|
http_resp = execute_request_with_rescues(method, api_base, context) do
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
232
|
+
self.class.default_connection_manager.execute_request(method, url,
|
233
|
+
body: body,
|
234
|
+
headers: headers,
|
235
|
+
query: query)
|
214
236
|
end
|
215
237
|
|
216
238
|
begin
|
@@ -233,6 +255,14 @@ module Stripe
|
|
233
255
|
# private
|
234
256
|
#
|
235
257
|
|
258
|
+
# Time (in seconds) that a connection manager has not been used before it's
|
259
|
+
# eligible for garbage collection.
|
260
|
+
CONNECTION_MANAGER_GC_LAST_USED_EXPIRY = 120
|
261
|
+
|
262
|
+
# How often to check (in seconds) for connection managers that haven't been
|
263
|
+
# used in a long time and which should be garbage collected.
|
264
|
+
CONNECTION_MANAGER_GC_PERIOD = 60
|
265
|
+
|
236
266
|
ERROR_MESSAGE_CONNECTION =
|
237
267
|
"Unexpected error communicating when trying to connect to " \
|
238
268
|
"Stripe (%s). You may be seeing this message because your DNS is not " \
|
@@ -266,7 +296,11 @@ module Stripe
|
|
266
296
|
# The original error message is also appended onto the final exception for
|
267
297
|
# full transparency.
|
268
298
|
NETWORK_ERROR_MESSAGES_MAP = {
|
299
|
+
EOFError => ERROR_MESSAGE_CONNECTION,
|
269
300
|
Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
|
301
|
+
Errno::ECONNRESET => ERROR_MESSAGE_CONNECTION,
|
302
|
+
Errno::EHOSTUNREACH => ERROR_MESSAGE_CONNECTION,
|
303
|
+
Errno::ETIMEDOUT => ERROR_MESSAGE_TIMEOUT_CONNECT,
|
270
304
|
SocketError => ERROR_MESSAGE_CONNECTION,
|
271
305
|
|
272
306
|
Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
|
@@ -320,6 +354,46 @@ module Stripe
|
|
320
354
|
Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
|
321
355
|
end
|
322
356
|
|
357
|
+
# Garbage collects connection managers that haven't been used in some time,
|
358
|
+
# with the idea being that we want to remove old connection managers that
|
359
|
+
# belong to dead threads and the like.
|
360
|
+
#
|
361
|
+
# Prefixed with `maybe_` because garbage collection will only run
|
362
|
+
# periodically so that we're not constantly engaged in busy work. If
|
363
|
+
# connection managers live a little passed their useful age it's not
|
364
|
+
# harmful, so it's not necessary to get them right away.
|
365
|
+
#
|
366
|
+
# For testability, returns `nil` if it didn't run and the number of
|
367
|
+
# connection managers that were garbage collected otherwise.
|
368
|
+
#
|
369
|
+
# IMPORTANT: This method is not thread-safe and expects to be called inside
|
370
|
+
# a lock on `@thread_contexts_with_connection_managers_mutex`.
|
371
|
+
#
|
372
|
+
# For internal use only. Does not provide a stable API and may be broken
|
373
|
+
# with future non-major changes.
|
374
|
+
def self.maybe_gc_connection_managers
|
375
|
+
next_gc_time = @last_connection_manager_gc + CONNECTION_MANAGER_GC_PERIOD
|
376
|
+
return nil if next_gc_time > Util.monotonic_time
|
377
|
+
|
378
|
+
last_used_threshold =
|
379
|
+
Util.monotonic_time - CONNECTION_MANAGER_GC_LAST_USED_EXPIRY
|
380
|
+
|
381
|
+
pruned_thread_contexts = []
|
382
|
+
@thread_contexts_with_connection_managers.each do |thread_context|
|
383
|
+
connection_manager = thread_context.default_connection_manager
|
384
|
+
next if connection_manager.last_used > last_used_threshold
|
385
|
+
|
386
|
+
connection_manager.clear
|
387
|
+
thread_context.default_connection_manager = nil
|
388
|
+
pruned_thread_contexts << thread_context
|
389
|
+
end
|
390
|
+
|
391
|
+
@thread_contexts_with_connection_managers -= pruned_thread_contexts
|
392
|
+
@last_connection_manager_gc = Util.monotonic_time
|
393
|
+
|
394
|
+
pruned_thread_contexts.count
|
395
|
+
end
|
396
|
+
|
323
397
|
private def api_url(url = "", api_base = nil)
|
324
398
|
(api_base || Stripe.api_base) + url
|
325
399
|
end
|
@@ -375,7 +449,7 @@ module Stripe
|
|
375
449
|
private def execute_request_with_rescues(method, api_base, context)
|
376
450
|
num_retries = 0
|
377
451
|
begin
|
378
|
-
request_start =
|
452
|
+
request_start = Util.monotonic_time
|
379
453
|
log_request(context, num_retries)
|
380
454
|
resp = yield
|
381
455
|
context = context.dup_from_response_headers(resp)
|
@@ -385,7 +459,8 @@ module Stripe
|
|
385
459
|
log_response(context, request_start, resp.code.to_i, resp.body)
|
386
460
|
|
387
461
|
if Stripe.enable_telemetry? && context.request_id
|
388
|
-
request_duration_ms =
|
462
|
+
request_duration_ms =
|
463
|
+
((Util.monotonic_time - request_start) * 1000).to_int
|
389
464
|
@last_request_metrics =
|
390
465
|
StripeRequestMetrics.new(context.request_id, request_duration_ms)
|
391
466
|
end
|
@@ -656,7 +731,7 @@ module Stripe
|
|
656
731
|
Util.log_info("Response from Stripe API",
|
657
732
|
account: context.account,
|
658
733
|
api_version: context.api_version,
|
659
|
-
elapsed:
|
734
|
+
elapsed: Util.monotonic_time - request_start,
|
660
735
|
idempotency_key: context.idempotency_key,
|
661
736
|
method: context.method,
|
662
737
|
path: context.path,
|
@@ -678,7 +753,7 @@ module Stripe
|
|
678
753
|
|
679
754
|
private def log_response_error(context, request_start, error)
|
680
755
|
Util.log_error("Request error",
|
681
|
-
elapsed:
|
756
|
+
elapsed: Util.monotonic_time - request_start,
|
682
757
|
error_message: error.message,
|
683
758
|
idempotency_key: context.idempotency_key,
|
684
759
|
method: context.method,
|
data/lib/stripe/util.rb
CHANGED
@@ -172,6 +172,18 @@ module Stripe
|
|
172
172
|
result
|
173
173
|
end
|
174
174
|
|
175
|
+
# `Time.now` can be unstable in cases like an administrator manually
|
176
|
+
# updating its value or a reconcilation via NTP. For this reason, prefer
|
177
|
+
# the use of the system's monotonic clock especially where comparing times
|
178
|
+
# to calculate an elapsed duration.
|
179
|
+
#
|
180
|
+
# Shortcut for getting monotonic time, mostly for purposes of line length
|
181
|
+
# and test stubbing. Returns time in seconds since the event used for
|
182
|
+
# monotonic reference purposes by the platform (e.g. system boot time).
|
183
|
+
def self.monotonic_time
|
184
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
185
|
+
end
|
186
|
+
|
175
187
|
def self.normalize_id(id)
|
176
188
|
if id.is_a?(Hash) # overloaded id
|
177
189
|
params_hash = id.dup
|
data/lib/stripe/version.rb
CHANGED
@@ -8,6 +8,14 @@ module Stripe
|
|
8
8
|
@manager = Stripe::ConnectionManager.new
|
9
9
|
end
|
10
10
|
|
11
|
+
context "#initialize" do
|
12
|
+
should "set #last_used to current time" do
|
13
|
+
t = 123.0
|
14
|
+
Util.stubs(:monotonic_time).returns(t)
|
15
|
+
assert_equal t, Stripe::ConnectionManager.new.last_used
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
11
19
|
context "#clear" do
|
12
20
|
should "clear any active connections" do
|
13
21
|
stub_request(:post, "#{Stripe.api_base}/path")
|
@@ -133,6 +141,23 @@ module Stripe
|
|
133
141
|
end
|
134
142
|
assert_equal e.message, "query should be a string"
|
135
143
|
end
|
144
|
+
|
145
|
+
should "set #last_used to current time" do
|
146
|
+
stub_request(:post, "#{Stripe.api_base}/path")
|
147
|
+
.to_return(body: JSON.generate(object: "account"))
|
148
|
+
|
149
|
+
t = 123.0
|
150
|
+
Util.stubs(:monotonic_time).returns(t)
|
151
|
+
|
152
|
+
manager = Stripe::ConnectionManager.new
|
153
|
+
|
154
|
+
# `#last_used` is also set by the constructor, so make sure we get a
|
155
|
+
# new value for it.
|
156
|
+
Util.stubs(:monotonic_time).returns(t + 1.0)
|
157
|
+
|
158
|
+
manager.execute_request(:post, "#{Stripe.api_base}/path")
|
159
|
+
assert_equal t + 1.0, manager.last_used
|
160
|
+
end
|
136
161
|
end
|
137
162
|
end
|
138
163
|
end
|
@@ -17,6 +17,76 @@ module Stripe
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
context ".maybe_gc_connection_managers" do
|
21
|
+
should "garbage collect old connection managers when appropriate" do
|
22
|
+
stub_request(:post, "#{Stripe.api_base}/v1/path")
|
23
|
+
.to_return(body: JSON.generate(object: "account"))
|
24
|
+
|
25
|
+
# Make sure we start with a blank slate (state may have been left in
|
26
|
+
# place by other tests).
|
27
|
+
StripeClient.clear_all_connection_managers
|
28
|
+
|
29
|
+
# Establish a base time.
|
30
|
+
t = 0.0
|
31
|
+
|
32
|
+
# And pretend that `StripeClient` was just initialized for the first
|
33
|
+
# time. (Don't access instance variables like this, but it's tricky to
|
34
|
+
# test properly otherwise.)
|
35
|
+
StripeClient.instance_variable_set(:@last_connection_manager_gc, t)
|
36
|
+
|
37
|
+
#
|
38
|
+
# t
|
39
|
+
#
|
40
|
+
Util.stubs(:monotonic_time).returns(t)
|
41
|
+
|
42
|
+
# Execute an initial request to ensure that a connection manager was
|
43
|
+
# created.
|
44
|
+
client = StripeClient.new
|
45
|
+
client.execute_request(:post, "/v1/path")
|
46
|
+
|
47
|
+
# The GC shouldn't run yet (a `nil` return indicates that GC didn't run).
|
48
|
+
assert_equal nil, StripeClient.maybe_gc_connection_managers
|
49
|
+
|
50
|
+
#
|
51
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1
|
52
|
+
#
|
53
|
+
# Move time to just *before* garbage collection is eligible to run.
|
54
|
+
# Nothing should happen.
|
55
|
+
#
|
56
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1)
|
57
|
+
|
58
|
+
assert_equal nil, StripeClient.maybe_gc_connection_managers
|
59
|
+
|
60
|
+
#
|
61
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1
|
62
|
+
#
|
63
|
+
# Move time to just *after* garbage collection is eligible to run.
|
64
|
+
# Garbage collection will run, but because the connection manager is
|
65
|
+
# not passed its expiry age, it will not be collected. Zero is returned
|
66
|
+
# to indicate so.
|
67
|
+
#
|
68
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1)
|
69
|
+
|
70
|
+
assert_equal 0, StripeClient.maybe_gc_connection_managers
|
71
|
+
|
72
|
+
#
|
73
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1
|
74
|
+
#
|
75
|
+
# Move us far enough into the future that we're passed the horizons for
|
76
|
+
# both a GC run as well as well as the expiry age of a connection
|
77
|
+
# manager. That means the GC will run and collect the connection
|
78
|
+
# manager that we created above.
|
79
|
+
#
|
80
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1)
|
81
|
+
|
82
|
+
assert_equal 1, StripeClient.maybe_gc_connection_managers
|
83
|
+
|
84
|
+
# And as an additional check, the connection manager of the current
|
85
|
+
# thread context should have been set to `nil` as it was GCed.
|
86
|
+
assert_nil StripeClient.current_thread_context.default_connection_manager
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
20
90
|
context ".clear_all_connection_managers" do
|
21
91
|
should "clear connection managers across all threads" do
|
22
92
|
stub_request(:post, "#{Stripe.api_base}/path")
|
@@ -102,6 +172,26 @@ module Stripe
|
|
102
172
|
method: :post, num_retries: 0)
|
103
173
|
end
|
104
174
|
|
175
|
+
should "retry on EOFError" do
|
176
|
+
assert StripeClient.should_retry?(EOFError.new,
|
177
|
+
method: :post, num_retries: 0)
|
178
|
+
end
|
179
|
+
|
180
|
+
should "retry on Errno::ECONNRESET" do
|
181
|
+
assert StripeClient.should_retry?(Errno::ECONNRESET.new,
|
182
|
+
method: :post, num_retries: 0)
|
183
|
+
end
|
184
|
+
|
185
|
+
should "retry on Errno::ETIMEDOUT" do
|
186
|
+
assert StripeClient.should_retry?(Errno::ETIMEDOUT.new,
|
187
|
+
method: :post, num_retries: 0)
|
188
|
+
end
|
189
|
+
|
190
|
+
should "retry on Errno::EHOSTUNREACH" do
|
191
|
+
assert StripeClient.should_retry?(Errno::EHOSTUNREACH.new,
|
192
|
+
method: :post, num_retries: 0)
|
193
|
+
end
|
194
|
+
|
105
195
|
should "retry on Net::OpenTimeout" do
|
106
196
|
assert StripeClient.should_retry?(Net::OpenTimeout.new,
|
107
197
|
method: :post, num_retries: 0)
|
@@ -117,6 +207,28 @@ module Stripe
|
|
117
207
|
method: :post, num_retries: 0)
|
118
208
|
end
|
119
209
|
|
210
|
+
should "retry when the `Stripe-Should-Retry` header is `true`" do
|
211
|
+
headers = StripeResponse::Headers.new(
|
212
|
+
"Stripe-Should-Retry" => ["true"]
|
213
|
+
)
|
214
|
+
|
215
|
+
# Note we send status 400 here, which would normally not be retried.
|
216
|
+
assert StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
|
217
|
+
http_status: 400),
|
218
|
+
method: :post, num_retries: 0)
|
219
|
+
end
|
220
|
+
|
221
|
+
should "not retry when the `Stripe-Should-Retry` header is `false`" do
|
222
|
+
headers = StripeResponse::Headers.new(
|
223
|
+
"Stripe-Should-Retry" => ["false"]
|
224
|
+
)
|
225
|
+
|
226
|
+
# Note we send status 409 here, which would normally be retried.
|
227
|
+
refute StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
|
228
|
+
http_status: 409),
|
229
|
+
method: :post, num_retries: 0)
|
230
|
+
end
|
231
|
+
|
120
232
|
should "retry on a 409 Conflict" do
|
121
233
|
assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 409),
|
122
234
|
method: :post, num_retries: 0)
|
@@ -199,20 +311,6 @@ module Stripe
|
|
199
311
|
end
|
200
312
|
end
|
201
313
|
|
202
|
-
context "#initialize" do
|
203
|
-
should "set Stripe.default_connection_manager" do
|
204
|
-
client = StripeClient.new
|
205
|
-
assert_equal StripeClient.default_connection_manager,
|
206
|
-
client.connection_manager
|
207
|
-
end
|
208
|
-
|
209
|
-
should "set a different connection if one was specified" do
|
210
|
-
connection_manager = ConnectionManager.new
|
211
|
-
client = StripeClient.new(connection_manager)
|
212
|
-
assert_equal connection_manager, client.connection_manager
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
314
|
context "#execute_request" do
|
217
315
|
context "headers" do
|
218
316
|
should "support literal headers" do
|
@@ -239,16 +337,11 @@ module Stripe
|
|
239
337
|
context "logging" do
|
240
338
|
setup do
|
241
339
|
# Freeze time for the purposes of the `elapsed` parameter that we
|
242
|
-
# emit for responses.
|
243
|
-
#
|
244
|
-
#
|
245
|
-
#
|
246
|
-
|
247
|
-
Timecop.freeze(Time.local(1990))
|
248
|
-
end
|
249
|
-
|
250
|
-
teardown do
|
251
|
-
Timecop.return
|
340
|
+
# emit for responses. Mocha's `anything` parameter can't match inside
|
341
|
+
# of a hash and is therefore not useful for this purpose. If we
|
342
|
+
# switch over to rspec-mocks at some point, we can probably remove
|
343
|
+
# this.
|
344
|
+
Util.stubs(:monotonic_time).returns(0.0)
|
252
345
|
end
|
253
346
|
|
254
347
|
should "produce appropriate logging" do
|
@@ -470,10 +563,10 @@ module Stripe
|
|
470
563
|
.to_return(body: "", status: 500)
|
471
564
|
|
472
565
|
client = StripeClient.new
|
566
|
+
|
473
567
|
e = assert_raises Stripe::APIError do
|
474
568
|
client.execute_request(:post, "/v1/charges")
|
475
569
|
end
|
476
|
-
|
477
570
|
assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message
|
478
571
|
end
|
479
572
|
|
@@ -482,22 +575,36 @@ module Stripe
|
|
482
575
|
.to_return(body: "", status: 200)
|
483
576
|
|
484
577
|
client = StripeClient.new
|
578
|
+
|
485
579
|
e = assert_raises Stripe::APIError do
|
486
580
|
client.execute_request(:post, "/v1/charges")
|
487
581
|
end
|
488
|
-
|
489
582
|
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
|
490
583
|
end
|
491
584
|
|
585
|
+
should "feed a request ID through to the error object" do
|
586
|
+
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
587
|
+
.to_return(body: JSON.generate(make_missing_id_error),
|
588
|
+
headers: { "Request-ID": "req_123" },
|
589
|
+
status: 400)
|
590
|
+
|
591
|
+
client = StripeClient.new
|
592
|
+
|
593
|
+
e = assert_raises Stripe::InvalidRequestError do
|
594
|
+
client.execute_request(:post, "/v1/charges")
|
595
|
+
end
|
596
|
+
assert_equal("req_123", e.request_id)
|
597
|
+
end
|
598
|
+
|
492
599
|
should "handle low level error" do
|
493
600
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
494
601
|
.to_raise(Errno::ECONNREFUSED.new)
|
495
602
|
|
496
603
|
client = StripeClient.new
|
604
|
+
|
497
605
|
e = assert_raises Stripe::APIConnectionError do
|
498
606
|
client.execute_request(:post, "/v1/charges")
|
499
607
|
end
|
500
|
-
|
501
608
|
assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base +
|
502
609
|
"\n\n(Network error: Connection refused)",
|
503
610
|
e.message
|
@@ -508,10 +615,10 @@ module Stripe
|
|
508
615
|
.to_return(body: JSON.generate(bar: "foo"), status: 500)
|
509
616
|
|
510
617
|
client = StripeClient.new
|
618
|
+
|
511
619
|
e = assert_raises Stripe::APIError do
|
512
620
|
client.execute_request(:post, "/v1/charges")
|
513
621
|
end
|
514
|
-
|
515
622
|
assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message
|
516
623
|
end
|
517
624
|
|
@@ -521,6 +628,7 @@ module Stripe
|
|
521
628
|
|
522
629
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
523
630
|
.to_return(body: JSON.generate(data), status: 400)
|
631
|
+
|
524
632
|
client = StripeClient.new
|
525
633
|
|
526
634
|
e = assert_raises Stripe::IdempotencyError do
|
@@ -533,75 +641,81 @@ module Stripe
|
|
533
641
|
should "raise InvalidRequestError on other 400s" do
|
534
642
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
535
643
|
.to_return(body: JSON.generate(make_missing_id_error), status: 400)
|
644
|
+
|
536
645
|
client = StripeClient.new
|
537
|
-
|
646
|
+
|
647
|
+
e = assert_raises Stripe::InvalidRequestError do
|
538
648
|
client.execute_request(:post, "/v1/charges")
|
539
|
-
rescue Stripe::InvalidRequestError => e
|
540
|
-
assert_equal(400, e.http_status)
|
541
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
542
649
|
end
|
650
|
+
assert_equal(400, e.http_status)
|
651
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
543
652
|
end
|
544
653
|
|
545
654
|
should "raise AuthenticationError on 401" do
|
546
655
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
547
656
|
.to_return(body: JSON.generate(make_missing_id_error), status: 401)
|
657
|
+
|
548
658
|
client = StripeClient.new
|
549
|
-
|
659
|
+
|
660
|
+
e = assert_raises Stripe::AuthenticationError do
|
550
661
|
client.execute_request(:post, "/v1/charges")
|
551
|
-
rescue Stripe::AuthenticationError => e
|
552
|
-
assert_equal(401, e.http_status)
|
553
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
554
662
|
end
|
663
|
+
assert_equal(401, e.http_status)
|
664
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
555
665
|
end
|
556
666
|
|
557
667
|
should "raise CardError on 402" do
|
558
668
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
559
669
|
.to_return(body: JSON.generate(make_invalid_exp_year_error), status: 402)
|
670
|
+
|
560
671
|
client = StripeClient.new
|
561
|
-
|
672
|
+
|
673
|
+
e = assert_raises Stripe::CardError do
|
562
674
|
client.execute_request(:post, "/v1/charges")
|
563
|
-
rescue Stripe::CardError => e
|
564
|
-
assert_equal(402, e.http_status)
|
565
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
566
|
-
assert_equal("invalid_expiry_year", e.code)
|
567
|
-
assert_equal("exp_year", e.param)
|
568
675
|
end
|
676
|
+
assert_equal(402, e.http_status)
|
677
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
678
|
+
assert_equal("invalid_expiry_year", e.code)
|
679
|
+
assert_equal("exp_year", e.param)
|
569
680
|
end
|
570
681
|
|
571
682
|
should "raise PermissionError on 403" do
|
572
683
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
573
684
|
.to_return(body: JSON.generate(make_missing_id_error), status: 403)
|
685
|
+
|
574
686
|
client = StripeClient.new
|
575
|
-
|
687
|
+
|
688
|
+
e = assert_raises Stripe::PermissionError do
|
576
689
|
client.execute_request(:post, "/v1/charges")
|
577
|
-
rescue Stripe::PermissionError => e
|
578
|
-
assert_equal(403, e.http_status)
|
579
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
580
690
|
end
|
691
|
+
assert_equal(403, e.http_status)
|
692
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
581
693
|
end
|
582
694
|
|
583
695
|
should "raise InvalidRequestError on 404" do
|
584
696
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
585
697
|
.to_return(body: JSON.generate(make_missing_id_error), status: 404)
|
698
|
+
|
586
699
|
client = StripeClient.new
|
587
|
-
|
700
|
+
|
701
|
+
e = assert_raises Stripe::InvalidRequestError do
|
588
702
|
client.execute_request(:post, "/v1/charges")
|
589
|
-
rescue Stripe::InvalidRequestError => e
|
590
|
-
assert_equal(404, e.http_status)
|
591
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
592
703
|
end
|
704
|
+
assert_equal(404, e.http_status)
|
705
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
593
706
|
end
|
594
707
|
|
595
708
|
should "raise RateLimitError on 429" do
|
596
709
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
597
710
|
.to_return(body: JSON.generate(make_rate_limit_error), status: 429)
|
711
|
+
|
598
712
|
client = StripeClient.new
|
599
|
-
|
713
|
+
|
714
|
+
e = assert_raises Stripe::RateLimitError do
|
600
715
|
client.execute_request(:post, "/v1/charges")
|
601
|
-
rescue Stripe::RateLimitError => e
|
602
|
-
assert_equal(429, e.http_status)
|
603
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
604
716
|
end
|
717
|
+
assert_equal(429, e.http_status)
|
718
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
605
719
|
end
|
606
720
|
|
607
721
|
should "raise OAuth::InvalidRequestError when error is a string with value 'invalid_request'" do
|
@@ -610,6 +724,7 @@ module Stripe
|
|
610
724
|
error_description: "No grant type specified"), status: 400)
|
611
725
|
|
612
726
|
client = StripeClient.new
|
727
|
+
|
613
728
|
opts = { api_base: Stripe.connect_base }
|
614
729
|
e = assert_raises Stripe::OAuth::InvalidRequestError do
|
615
730
|
client.execute_request(:post, "/oauth/token", opts)
|
@@ -625,6 +740,7 @@ module Stripe
|
|
625
740
|
error_description: "This authorization code has already been used. All tokens issued with this code have been revoked."), status: 400)
|
626
741
|
|
627
742
|
client = StripeClient.new
|
743
|
+
|
628
744
|
opts = { api_base: Stripe.connect_base }
|
629
745
|
e = assert_raises Stripe::OAuth::InvalidGrantError do
|
630
746
|
client.execute_request(:post, "/oauth/token", opts)
|
@@ -641,6 +757,7 @@ module Stripe
|
|
641
757
|
error_description: "This application is not connected to stripe account acct_19tLK7DSlTMT26Mk, or that account does not exist."), status: 401)
|
642
758
|
|
643
759
|
client = StripeClient.new
|
760
|
+
|
644
761
|
opts = { api_base: Stripe.connect_base }
|
645
762
|
e = assert_raises Stripe::OAuth::InvalidClientError do
|
646
763
|
client.execute_request(:post, "/oauth/deauthorize", opts)
|
@@ -657,6 +774,7 @@ module Stripe
|
|
657
774
|
error_description: "Something."), status: 401)
|
658
775
|
|
659
776
|
client = StripeClient.new
|
777
|
+
|
660
778
|
opts = { api_base: Stripe.connect_base }
|
661
779
|
e = assert_raises Stripe::OAuth::OAuthError do
|
662
780
|
client.execute_request(:post, "/oauth/deauthorize", opts)
|
@@ -817,6 +935,22 @@ module Stripe
|
|
817
935
|
end
|
818
936
|
end
|
819
937
|
|
938
|
+
context "#connection_manager" do
|
939
|
+
should "warn that #connection_manager is deprecated" do
|
940
|
+
old_stderr = $stderr
|
941
|
+
$stderr = StringIO.new
|
942
|
+
begin
|
943
|
+
client = StripeClient.new
|
944
|
+
client.connection_manager
|
945
|
+
message = "NOTE: Stripe::StripeClient#connection_manager is " \
|
946
|
+
"deprecated"
|
947
|
+
assert_match Regexp.new(message), $stderr.string
|
948
|
+
ensure
|
949
|
+
$stderr = old_stderr
|
950
|
+
end
|
951
|
+
end
|
952
|
+
end
|
953
|
+
|
820
954
|
context "#request" do
|
821
955
|
should "return a result and response object" do
|
822
956
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stripe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stripe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Stripe is the easiest way to accept payments online. See https://stripe.com
|
14
14
|
for details.
|