stripe 4.24.0 → 5.0.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 +17 -4
- data/.rubocop_todo.yml +10 -9
- data/.travis.yml +1 -5
- data/CHANGELOG.md +22 -0
- data/Gemfile +2 -12
- data/README.md +10 -10
- data/Rakefile +8 -7
- data/VERSION +1 -1
- data/lib/stripe.rb +56 -15
- data/lib/stripe/api_operations/list.rb +0 -6
- data/lib/stripe/connection_manager.rb +131 -0
- data/lib/stripe/error_object.rb +94 -0
- data/lib/stripe/errors.rb +15 -2
- data/lib/stripe/list_object.rb +2 -1
- data/lib/stripe/multipart_encoder.rb +131 -0
- data/lib/stripe/object_types.rb +0 -1
- data/lib/stripe/resources.rb +0 -1
- data/lib/stripe/resources/account.rb +1 -5
- data/lib/stripe/resources/account_link.rb +1 -1
- data/lib/stripe/resources/alipay_account.rb +1 -1
- data/lib/stripe/resources/apple_pay_domain.rb +1 -1
- data/lib/stripe/resources/application_fee.rb +1 -12
- data/lib/stripe/resources/application_fee_refund.rb +1 -1
- data/lib/stripe/resources/balance.rb +1 -1
- data/lib/stripe/resources/balance_transaction.rb +1 -1
- data/lib/stripe/resources/bank_account.rb +1 -1
- data/lib/stripe/resources/bitcoin_receiver.rb +1 -1
- data/lib/stripe/resources/bitcoin_transaction.rb +1 -1
- data/lib/stripe/resources/capability.rb +1 -1
- data/lib/stripe/resources/card.rb +1 -1
- data/lib/stripe/resources/charge.rb +7 -69
- data/lib/stripe/resources/checkout/session.rb +1 -1
- data/lib/stripe/resources/country_spec.rb +1 -1
- data/lib/stripe/resources/coupon.rb +1 -1
- data/lib/stripe/resources/credit_note.rb +1 -1
- data/lib/stripe/resources/customer.rb +3 -63
- data/lib/stripe/resources/customer_balance_transaction.rb +1 -1
- data/lib/stripe/resources/discount.rb +1 -1
- data/lib/stripe/resources/dispute.rb +1 -7
- data/lib/stripe/resources/ephemeral_key.rb +1 -1
- data/lib/stripe/resources/event.rb +1 -1
- data/lib/stripe/resources/exchange_rate.rb +1 -1
- data/lib/stripe/resources/file.rb +3 -13
- data/lib/stripe/resources/file_link.rb +1 -1
- data/lib/stripe/resources/invoice.rb +6 -1
- data/lib/stripe/resources/invoice_item.rb +1 -1
- data/lib/stripe/resources/invoice_line_item.rb +1 -1
- data/lib/stripe/resources/issuing/authorization.rb +1 -1
- data/lib/stripe/resources/issuing/card.rb +1 -1
- data/lib/stripe/resources/issuing/card_details.rb +1 -1
- data/lib/stripe/resources/issuing/cardholder.rb +1 -1
- data/lib/stripe/resources/issuing/dispute.rb +1 -1
- data/lib/stripe/resources/issuing/transaction.rb +1 -1
- data/lib/stripe/resources/login_link.rb +1 -1
- data/lib/stripe/resources/order.rb +1 -9
- data/lib/stripe/resources/order_return.rb +1 -1
- data/lib/stripe/resources/payment_intent.rb +1 -1
- data/lib/stripe/resources/payment_method.rb +1 -1
- data/lib/stripe/resources/payout.rb +1 -7
- data/lib/stripe/resources/person.rb +1 -1
- data/lib/stripe/resources/plan.rb +1 -1
- data/lib/stripe/resources/product.rb +1 -1
- data/lib/stripe/resources/radar/early_fraud_warning.rb +1 -1
- data/lib/stripe/resources/radar/value_list.rb +1 -1
- data/lib/stripe/resources/radar/value_list_item.rb +1 -1
- data/lib/stripe/resources/recipient.rb +1 -5
- data/lib/stripe/resources/recipient_transfer.rb +1 -1
- data/lib/stripe/resources/refund.rb +1 -1
- data/lib/stripe/resources/reporting/report_run.rb +1 -1
- data/lib/stripe/resources/reporting/report_type.rb +1 -1
- data/lib/stripe/resources/reversal.rb +1 -1
- data/lib/stripe/resources/review.rb +1 -1
- data/lib/stripe/resources/setup_intent.rb +1 -1
- data/lib/stripe/resources/sigma/scheduled_query_run.rb +1 -1
- data/lib/stripe/resources/sku.rb +1 -1
- data/lib/stripe/resources/source.rb +1 -7
- data/lib/stripe/resources/source_transaction.rb +1 -1
- data/lib/stripe/resources/subscription.rb +9 -9
- data/lib/stripe/resources/subscription_item.rb +1 -1
- data/lib/stripe/resources/subscription_schedule.rb +1 -1
- data/lib/stripe/resources/tax_id.rb +1 -1
- data/lib/stripe/resources/tax_rate.rb +1 -1
- data/lib/stripe/resources/terminal/connection_token.rb +1 -1
- data/lib/stripe/resources/terminal/location.rb +1 -1
- data/lib/stripe/resources/terminal/reader.rb +1 -1
- data/lib/stripe/resources/three_d_secure.rb +1 -1
- data/lib/stripe/resources/token.rb +1 -1
- data/lib/stripe/resources/topup.rb +1 -1
- data/lib/stripe/resources/transfer.rb +1 -6
- data/lib/stripe/resources/usage_record.rb +1 -17
- data/lib/stripe/resources/usage_record_summary.rb +1 -1
- data/lib/stripe/resources/webhook_endpoint.rb +1 -1
- data/lib/stripe/stripe_client.rb +281 -183
- data/lib/stripe/stripe_object.rb +4 -23
- data/lib/stripe/stripe_response.rb +53 -21
- data/lib/stripe/util.rb +10 -11
- data/lib/stripe/version.rb +1 -1
- data/lib/stripe/webhook.rb +1 -1
- data/stripe.gemspec +6 -9
- data/test/stripe/account_test.rb +0 -16
- data/test/stripe/api_operations_test.rb +2 -2
- data/test/stripe/api_resource_test.rb +2 -10
- data/test/stripe/charge_test.rb +0 -16
- data/test/stripe/connection_manager_test.rb +138 -0
- data/test/stripe/customer_test.rb +1 -44
- data/test/stripe/errors_test.rb +29 -8
- data/test/stripe/file_test.rb +0 -10
- data/test/stripe/invoice_test.rb +17 -1
- data/test/stripe/list_object_test.rb +0 -16
- data/test/stripe/login_link_test.rb +1 -1
- data/test/stripe/multipart_encoder_test.rb +130 -0
- data/test/stripe/payment_intent_test.rb +1 -1
- data/test/stripe/setup_intent_test.rb +1 -1
- data/test/stripe/source_test.rb +0 -18
- data/test/stripe/stripe_client_test.rb +214 -29
- data/test/stripe/stripe_object_test.rb +7 -35
- data/test/stripe/stripe_response_test.rb +70 -24
- data/test/stripe/subscription_test.rb +2 -2
- data/test/stripe/webhook_test.rb +2 -2
- data/test/stripe_mock.rb +4 -3
- data/test/stripe_test.rb +0 -13
- data/test/test_helper.rb +10 -5
- metadata +11 -39
- data/lib/stripe/resources/issuer_fraud_record.rb +0 -9
- data/test/stripe/file_upload_test.rb +0 -79
- data/test/stripe/issuer_fraud_record_test.rb +0 -20
- data/test/stripe/usage_record_test.rb +0 -28
@@ -6,7 +6,7 @@ module Stripe
|
|
6
6
|
extend Stripe::APIOperations::List
|
7
7
|
include Stripe::APIOperations::Save
|
8
8
|
|
9
|
-
OBJECT_NAME = "payout"
|
9
|
+
OBJECT_NAME = "payout"
|
10
10
|
|
11
11
|
custom_method :cancel, http_verb: :post
|
12
12
|
|
@@ -18,11 +18,5 @@ module Stripe
|
|
18
18
|
opts: opts
|
19
19
|
)
|
20
20
|
end
|
21
|
-
|
22
|
-
def cancel_url
|
23
|
-
resource_url + "/cancel"
|
24
|
-
end
|
25
|
-
extend Gem::Deprecate
|
26
|
-
deprecate :cancel_url, :none, 2019, 11
|
27
21
|
end
|
28
22
|
end
|
data/lib/stripe/resources/sku.rb
CHANGED
@@ -5,7 +5,7 @@ module Stripe
|
|
5
5
|
extend Stripe::APIOperations::Create
|
6
6
|
include Stripe::APIOperations::Save
|
7
7
|
|
8
|
-
OBJECT_NAME = "source"
|
8
|
+
OBJECT_NAME = "source"
|
9
9
|
|
10
10
|
custom_method :verify, http_verb: :post
|
11
11
|
|
@@ -31,12 +31,6 @@ module Stripe
|
|
31
31
|
initialize_from(resp.data, opts)
|
32
32
|
end
|
33
33
|
|
34
|
-
def delete(params = {}, opts = {})
|
35
|
-
detach(params, opts)
|
36
|
-
end
|
37
|
-
extend Gem::Deprecate
|
38
|
-
deprecate :delete, "#detach", 2017, 10
|
39
|
-
|
40
34
|
def source_transactions(params = {}, opts = {})
|
41
35
|
resp, opts = request(:get, resource_url + "/source_transactions", params,
|
42
36
|
opts)
|
@@ -7,19 +7,19 @@ module Stripe
|
|
7
7
|
extend Stripe::APIOperations::List
|
8
8
|
include Stripe::APIOperations::Save
|
9
9
|
|
10
|
-
OBJECT_NAME = "subscription"
|
10
|
+
OBJECT_NAME = "subscription"
|
11
11
|
|
12
12
|
custom_method :delete_discount, http_verb: :delete, http_path: "discount"
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
def delete_discount(params = {}, opts = {})
|
15
|
+
request_stripe_object(
|
16
|
+
method: :delete,
|
17
|
+
path: resource_url + "/discount",
|
18
|
+
params: params,
|
19
|
+
opts: opts
|
20
|
+
)
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
resource_url + "/discount"
|
23
|
-
end
|
23
|
+
save_nested_resource :source
|
24
24
|
end
|
25
25
|
end
|
@@ -6,7 +6,7 @@ module Stripe
|
|
6
6
|
extend Stripe::APIOperations::List
|
7
7
|
include Stripe::APIOperations::Save
|
8
8
|
|
9
|
-
OBJECT_NAME = "subscription_schedule"
|
9
|
+
OBJECT_NAME = "subscription_schedule"
|
10
10
|
|
11
11
|
custom_method :cancel, http_verb: :post
|
12
12
|
custom_method :release, http_verb: :post
|
@@ -7,7 +7,7 @@ module Stripe
|
|
7
7
|
include Stripe::APIOperations::Save
|
8
8
|
extend Stripe::APIOperations::NestedResource
|
9
9
|
|
10
|
-
OBJECT_NAME = "transfer"
|
10
|
+
OBJECT_NAME = "transfer"
|
11
11
|
|
12
12
|
custom_method :cancel, http_verb: :post
|
13
13
|
|
@@ -22,10 +22,5 @@ module Stripe
|
|
22
22
|
opts: opts
|
23
23
|
)
|
24
24
|
end
|
25
|
-
|
26
|
-
def cancel_url
|
27
|
-
resource_url + "/cancel"
|
28
|
-
end
|
29
|
-
deprecate :cancel_url, :none, 2019, 11
|
30
25
|
end
|
31
26
|
end
|
@@ -2,22 +2,6 @@
|
|
2
2
|
|
3
3
|
module Stripe
|
4
4
|
class UsageRecord < APIResource
|
5
|
-
OBJECT_NAME = "usage_record"
|
6
|
-
|
7
|
-
def self.create(params = {}, opts = {})
|
8
|
-
unless params.key?(:subscription_item)
|
9
|
-
raise ArgumentError, "Params must have a subscription_item key"
|
10
|
-
end
|
11
|
-
req_params = params.clone.delete_if do |key, _value|
|
12
|
-
key == :subscription_item
|
13
|
-
end
|
14
|
-
resp, opts = request(
|
15
|
-
:post,
|
16
|
-
"/v1/subscription_items/#{params[:subscription_item]}/usage_records",
|
17
|
-
req_params,
|
18
|
-
opts
|
19
|
-
)
|
20
|
-
Util.convert_to_stripe_object(resp.data, opts)
|
21
|
-
end
|
5
|
+
OBJECT_NAME = "usage_record"
|
22
6
|
end
|
23
7
|
end
|
data/lib/stripe/stripe_client.rb
CHANGED
@@ -5,85 +5,98 @@ 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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
# A set of all known connection managers across all threads and a mutex to
|
9
|
+
# synchronize global access to them.
|
10
|
+
@all_connection_managers = []
|
11
|
+
@all_connection_managers_mutex = Mutex.new
|
12
|
+
|
13
|
+
attr_accessor :connection_manager
|
14
|
+
|
15
|
+
# Initializes a new `StripeClient`. Expects a `ConnectionManager` object,
|
16
|
+
# and uses a default connection manager unless one is passed.
|
17
|
+
def initialize(connection_manager = nil)
|
18
|
+
self.connection_manager = connection_manager ||
|
19
|
+
self.class.default_connection_manager
|
14
20
|
@system_profiler = SystemProfiler.new
|
15
21
|
@last_request_metrics = nil
|
16
22
|
end
|
17
23
|
|
24
|
+
# Gets a currently active `StripeClient`. Set for the current thread when
|
25
|
+
# `StripeClient#request` is being run so that API operations being executed
|
26
|
+
# inside of that block can find the currently active client. It's reset to
|
27
|
+
# the original value (hopefully `nil`) after the block ends.
|
28
|
+
#
|
29
|
+
# For internal use only. Does not provide a stable API and may be broken
|
30
|
+
# with future non-major changes.
|
18
31
|
def self.active_client
|
19
|
-
|
32
|
+
current_thread_context.active_client || default_client
|
20
33
|
end
|
21
34
|
|
22
|
-
|
23
|
-
|
24
|
-
|
35
|
+
# Finishes any active connections by closing their TCP connection and
|
36
|
+
# clears them from internal tracking in all connection managers across all
|
37
|
+
# threads.
|
38
|
+
#
|
39
|
+
# For internal use only. Does not provide a stable API and may be broken
|
40
|
+
# with future non-major changes.
|
41
|
+
def self.clear_all_connection_managers
|
42
|
+
# Just a quick path for when configuration is being set for the first
|
43
|
+
# time before any connections have been opened. There is technically some
|
44
|
+
# potential for thread raciness here, but not in a practical sense.
|
45
|
+
return if @all_connection_managers.empty?
|
46
|
+
|
47
|
+
@all_connection_managers_mutex.synchronize do
|
48
|
+
@all_connection_managers.each(&:clear)
|
49
|
+
end
|
25
50
|
end
|
26
51
|
|
27
|
-
# A default
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
# of connection re-use, so make sure that we have a separate connection
|
33
|
-
# object per thread.
|
34
|
-
Thread.current[:stripe_client_default_conn] ||= begin
|
35
|
-
conn = Faraday.new do |builder|
|
36
|
-
builder.use Faraday::Request::Multipart
|
37
|
-
builder.use Faraday::Request::UrlEncoded
|
38
|
-
builder.use Faraday::Response::RaiseError
|
39
|
-
|
40
|
-
# Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
|
41
|
-
# so fall back to default there.
|
42
|
-
if Gem.win_platform? || RUBY_PLATFORM == "java"
|
43
|
-
builder.adapter :net_http
|
44
|
-
else
|
45
|
-
builder.adapter :net_http_persistent
|
46
|
-
end
|
47
|
-
end
|
52
|
+
# A default client for the current thread.
|
53
|
+
def self.default_client
|
54
|
+
current_thread_context.default_client ||=
|
55
|
+
StripeClient.new(default_connection_manager)
|
56
|
+
end
|
48
57
|
|
49
|
-
|
58
|
+
# A default connection manager for the current thread.
|
59
|
+
def self.default_connection_manager
|
60
|
+
current_thread_context.default_connection_manager ||= begin
|
61
|
+
connection_manager = ConnectionManager.new
|
50
62
|
|
51
|
-
|
52
|
-
|
53
|
-
conn.ssl.cert_store = Stripe.ca_store
|
54
|
-
else
|
55
|
-
conn.ssl.verify = false
|
56
|
-
|
57
|
-
unless @verify_ssl_warned
|
58
|
-
@verify_ssl_warned = true
|
59
|
-
warn("WARNING: Running without SSL cert verification. " \
|
60
|
-
"You should never do this in production. " \
|
61
|
-
"Execute `Stripe.verify_ssl_certs = true` to enable " \
|
62
|
-
"verification.")
|
63
|
-
end
|
63
|
+
@all_connection_managers_mutex.synchronize do
|
64
|
+
@all_connection_managers << connection_manager
|
64
65
|
end
|
65
66
|
|
66
|
-
|
67
|
+
connection_manager
|
67
68
|
end
|
68
69
|
end
|
69
70
|
|
70
71
|
# Checks if an error is a problem that we should retry on. This includes
|
71
72
|
# both socket errors that may represent an intermittent problem and some
|
72
73
|
# special HTTP statuses.
|
73
|
-
def self.should_retry?(error, num_retries)
|
74
|
+
def self.should_retry?(error, method:, num_retries:)
|
74
75
|
return false if num_retries >= Stripe.max_network_retries
|
75
76
|
|
76
77
|
# Retry on timeout-related problems (either on open or read).
|
77
|
-
return true if error.is_a?(
|
78
|
+
return true if error.is_a?(Net::OpenTimeout)
|
79
|
+
return true if error.is_a?(Net::ReadTimeout)
|
78
80
|
|
79
81
|
# Destination refused the connection, the connection was reset, or a
|
80
82
|
# variety of other connection failures. This could occur from a single
|
81
83
|
# saturated server, so retry in case it's intermittent.
|
82
|
-
return true if error.is_a?(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
+
# 409 Conflict
|
89
|
+
return true if error.http_status == 409
|
90
|
+
|
91
|
+
# 500 Internal Server Error
|
92
|
+
#
|
93
|
+
# We only bother retrying these for non-POST requests. POSTs end up
|
94
|
+
# being cached by the idempotency layer so there's no purpose in
|
95
|
+
# retrying them.
|
96
|
+
return true if error.http_status == 500 && method != :post
|
97
|
+
|
98
|
+
# 503 Service Unavailable
|
99
|
+
return true if error.http_status == 503
|
87
100
|
end
|
88
101
|
|
89
102
|
false
|
@@ -114,127 +127,187 @@ module Stripe
|
|
114
127
|
# charge, resp = client.request { Charge.create }
|
115
128
|
#
|
116
129
|
def request
|
117
|
-
|
118
|
-
|
119
|
-
|
130
|
+
old_stripe_client = self.class.current_thread_context.active_client
|
131
|
+
self.class.current_thread_context.active_client = self
|
132
|
+
|
133
|
+
if self.class.current_thread_context.last_responses&.key?(object_id)
|
134
|
+
raise "calls to StripeClient#request cannot be nested within a thread"
|
135
|
+
end
|
136
|
+
|
137
|
+
self.class.current_thread_context.last_responses ||= {}
|
138
|
+
self.class.current_thread_context.last_responses[object_id] = nil
|
120
139
|
|
121
140
|
begin
|
122
141
|
res = yield
|
123
|
-
[res,
|
142
|
+
[res, self.class.current_thread_context.last_responses[object_id]]
|
124
143
|
ensure
|
125
|
-
|
144
|
+
self.class.current_thread_context.active_client = old_stripe_client
|
145
|
+
self.class.current_thread_context.last_responses.delete(object_id)
|
126
146
|
end
|
127
147
|
end
|
128
148
|
|
129
149
|
def execute_request(method, path,
|
130
150
|
api_base: nil, api_key: nil, headers: {}, params: {})
|
151
|
+
raise ArgumentError, "method should be a symbol" \
|
152
|
+
unless method.is_a?(Symbol)
|
153
|
+
raise ArgumentError, "path should be a string" \
|
154
|
+
unless path.is_a?(String)
|
155
|
+
|
131
156
|
api_base ||= Stripe.api_base
|
132
157
|
api_key ||= Stripe.api_key
|
133
158
|
params = Util.objects_to_ids(params)
|
134
159
|
|
135
160
|
check_api_key!(api_key)
|
136
161
|
|
137
|
-
|
162
|
+
body_params = nil
|
138
163
|
query_params = nil
|
139
|
-
case method
|
164
|
+
case method
|
140
165
|
when :get, :head, :delete
|
141
166
|
query_params = params
|
142
167
|
else
|
143
|
-
|
168
|
+
body_params = params
|
144
169
|
end
|
145
170
|
|
146
|
-
|
147
|
-
# parameters in `query_params` and query parameters that are appended
|
148
|
-
# onto the end of the given path. In this case, Faraday will silently
|
149
|
-
# discard the URL's parameters which may break a request.
|
150
|
-
#
|
151
|
-
# Here we decode any parameters that were added onto the end of a path
|
152
|
-
# and add them to `query_params` so that all parameters end up in one
|
153
|
-
# place and all of them are correctly included in the final request.
|
154
|
-
u = URI.parse(path)
|
155
|
-
unless u.query.nil?
|
156
|
-
query_params ||= {}
|
157
|
-
query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
|
158
|
-
|
159
|
-
# Reset the path minus any query parameters that were specified.
|
160
|
-
path = u.path
|
161
|
-
end
|
171
|
+
query_params, path = merge_query_params(query_params, path)
|
162
172
|
|
163
173
|
headers = request_headers(api_key, method)
|
164
174
|
.update(Util.normalize_headers(headers))
|
165
|
-
params_encoder = FaradayStripeEncoder.new
|
166
175
|
url = api_url(path, api_base)
|
167
176
|
|
177
|
+
# Merge given query parameters with any already encoded in the path.
|
178
|
+
query = query_params ? Util.encode_parameters(query_params) : nil
|
179
|
+
|
180
|
+
# Encoding body parameters is a little more complex because we may have
|
181
|
+
# to send a multipart-encoded body. `body_log` is produced separately as
|
182
|
+
# a log-friendly variant of the encoded form. File objects are displayed
|
183
|
+
# as such instead of as their file contents.
|
184
|
+
body, body_log =
|
185
|
+
body_params ? encode_body(body_params, headers) : [nil, nil]
|
186
|
+
|
168
187
|
# stores information on the request we're about to make so that we don't
|
169
188
|
# have to pass as many parameters around for logging.
|
170
189
|
context = RequestLogContext.new
|
171
190
|
context.account = headers["Stripe-Account"]
|
172
191
|
context.api_key = api_key
|
173
192
|
context.api_version = headers["Stripe-Version"]
|
174
|
-
context.body =
|
193
|
+
context.body = body_log
|
175
194
|
context.idempotency_key = headers["Idempotency-Key"]
|
176
195
|
context.method = method
|
177
196
|
context.path = path
|
178
|
-
context.
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
conn.run_request(method, url, body, headers) do |req|
|
186
|
-
req.options.open_timeout = Stripe.open_timeout
|
187
|
-
req.options.params_encoder = params_encoder
|
188
|
-
req.options.timeout = Stripe.read_timeout
|
189
|
-
req.params = query_params unless query_params.nil?
|
190
|
-
end
|
197
|
+
context.query = query
|
198
|
+
|
199
|
+
http_resp = execute_request_with_rescues(method, api_base, context) do
|
200
|
+
connection_manager.execute_request(method, url,
|
201
|
+
body: body,
|
202
|
+
headers: headers,
|
203
|
+
query: query)
|
191
204
|
end
|
192
205
|
|
193
206
|
begin
|
194
|
-
resp = StripeResponse.
|
207
|
+
resp = StripeResponse.from_net_http(http_resp)
|
195
208
|
rescue JSON::ParserError
|
196
|
-
raise general_api_error(http_resp.
|
209
|
+
raise general_api_error(http_resp.code.to_i, http_resp.body)
|
210
|
+
end
|
211
|
+
|
212
|
+
# If being called from `StripeClient#request`, put the last response in
|
213
|
+
# thread-local memory so that it can be returned to the user. Don't store
|
214
|
+
# anything otherwise so that we don't leak memory.
|
215
|
+
if self.class.current_thread_context.last_responses&.key?(object_id)
|
216
|
+
self.class.current_thread_context.last_responses[object_id] = resp
|
197
217
|
end
|
198
218
|
|
199
|
-
# Allows StripeClient#request to return a response object to a caller.
|
200
|
-
@last_response = resp
|
201
219
|
[resp, api_key]
|
202
220
|
end
|
203
221
|
|
204
|
-
# Used to workaround buggy behavior in Faraday: the library will try to
|
205
|
-
# reshape anything that we pass to `req.params` with one of its default
|
206
|
-
# encoders. I don't think this process is supposed to be lossy, but it is
|
207
|
-
# -- in particular when we send our integer-indexed maps (i.e. arrays),
|
208
|
-
# Faraday ends up stripping out the integer indexes.
|
209
222
|
#
|
210
|
-
#
|
211
|
-
# telling Faraday to use that.
|
223
|
+
# private
|
212
224
|
#
|
213
|
-
# The class also performs simple caching so that we don't have to encode
|
214
|
-
# parameters twice for every request (once to build the request and once
|
215
|
-
# for logging).
|
216
|
-
#
|
217
|
-
# When initialized with `multipart: true`, the encoder just inspects the
|
218
|
-
# hash instead to get a decent representation for logging. In the case of a
|
219
|
-
# multipart request, Faraday won't use the result of this encoder.
|
220
|
-
class FaradayStripeEncoder
|
221
|
-
def initialize
|
222
|
-
@cache = {}
|
223
|
-
end
|
224
225
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
226
|
+
ERROR_MESSAGE_CONNECTION =
|
227
|
+
"Unexpected error communicating when trying to connect to " \
|
228
|
+
"Stripe (%s). You may be seeing this message because your DNS is not " \
|
229
|
+
"working or you don't have an internet connection. To check, try " \
|
230
|
+
"running `host stripe.com` from the command line."
|
231
|
+
ERROR_MESSAGE_SSL =
|
232
|
+
"Could not establish a secure connection to Stripe (%s), you " \
|
233
|
+
"may need to upgrade your OpenSSL version. To check, try running " \
|
234
|
+
"`openssl s_client -connect api.stripe.com:443` from the command " \
|
235
|
+
"line."
|
236
|
+
|
237
|
+
# Common error suffix sared by both connect and read timeout messages.
|
238
|
+
ERROR_MESSAGE_TIMEOUT_SUFFIX =
|
239
|
+
"Please check your internet connection and try again. " \
|
240
|
+
"If this problem persists, you should check Stripe's service " \
|
241
|
+
"status at https://status.stripe.com, or let us know at " \
|
242
|
+
"support@stripe.com."
|
243
|
+
|
244
|
+
ERROR_MESSAGE_TIMEOUT_CONNECT = (
|
245
|
+
"Timed out connecting to Stripe (%s). " +
|
246
|
+
ERROR_MESSAGE_TIMEOUT_SUFFIX
|
247
|
+
).freeze
|
248
|
+
|
249
|
+
ERROR_MESSAGE_TIMEOUT_READ = (
|
250
|
+
"Timed out communicating with Stripe (%s). " +
|
251
|
+
ERROR_MESSAGE_TIMEOUT_SUFFIX
|
252
|
+
).freeze
|
253
|
+
|
254
|
+
# Maps types of exceptions that we're likely to see during a network
|
255
|
+
# request to more user-friendly messages that we put in front of people.
|
256
|
+
# The original error message is also appended onto the final exception for
|
257
|
+
# full transparency.
|
258
|
+
NETWORK_ERROR_MESSAGES_MAP = {
|
259
|
+
Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
|
260
|
+
SocketError => ERROR_MESSAGE_CONNECTION,
|
261
|
+
|
262
|
+
Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
|
263
|
+
Net::ReadTimeout => ERROR_MESSAGE_TIMEOUT_READ,
|
264
|
+
|
265
|
+
OpenSSL::SSL::SSLError => ERROR_MESSAGE_SSL,
|
266
|
+
}.freeze
|
267
|
+
private_constant :NETWORK_ERROR_MESSAGES_MAP
|
268
|
+
|
269
|
+
# A record representing any data that `StripeClient` puts into
|
270
|
+
# `Thread.current`. Making it a class likes this gives us a little extra
|
271
|
+
# type safety and lets us document what each field does.
|
272
|
+
#
|
273
|
+
# For internal use only. Does not provide a stable API and may be broken
|
274
|
+
# with future non-major changes.
|
275
|
+
class ThreadContext
|
276
|
+
# A `StripeClient` that's been flagged as currently active within a
|
277
|
+
# thread by `StripeClient#request`. A client stays active until the
|
278
|
+
# completion of the request block.
|
279
|
+
attr_accessor :active_client
|
280
|
+
|
281
|
+
# A default `StripeClient` object for the thread. Used in all cases where
|
282
|
+
# the user hasn't specified their own.
|
283
|
+
attr_accessor :default_client
|
284
|
+
|
285
|
+
# A default `ConnectionManager` for the thread. Normally shared between
|
286
|
+
# all `StripeClient` objects on a particular thread, and created so as to
|
287
|
+
# minimize the number of open connections that an application needs.
|
288
|
+
attr_accessor :default_connection_manager
|
289
|
+
|
290
|
+
# A temporary map of object IDs to responses from last executed API
|
291
|
+
# calls. Used to return a responses from calls to `StripeClient#request`.
|
292
|
+
#
|
293
|
+
# Stored in the thread data to make the use of a single `StripeClient`
|
294
|
+
# object safe across multiple threads. Stored as a map so that multiple
|
295
|
+
# `StripeClient` objects can run concurrently on the same thread.
|
296
|
+
#
|
297
|
+
# Responses are only left in as long as they're needed, which means
|
298
|
+
# they're removed as soon as a call leaves `StripeClient#request`, and
|
299
|
+
# because that's wrapped in an `ensure` block, they should never leave
|
300
|
+
# garbage in `Thread.current`.
|
301
|
+
attr_accessor :last_responses
|
302
|
+
end
|
232
303
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
304
|
+
# Access data stored for `StripeClient` within the thread's current
|
305
|
+
# context. Returns `ThreadContext`.
|
306
|
+
#
|
307
|
+
# For internal use only. Does not provide a stable API and may be broken
|
308
|
+
# with future non-major changes.
|
309
|
+
def self.current_thread_context
|
310
|
+
Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
|
238
311
|
end
|
239
312
|
|
240
313
|
private def api_url(url = "", api_base = nil)
|
@@ -258,14 +331,48 @@ module Stripe
|
|
258
331
|
"email support@stripe.com if you have any questions.)"
|
259
332
|
end
|
260
333
|
|
261
|
-
|
334
|
+
# Encodes a set of body parameters using multipart if `Content-Type` is set
|
335
|
+
# for that, or standard form-encoding otherwise. Returns the encoded body
|
336
|
+
# and a version of the encoded body that's safe to be logged.
|
337
|
+
private def encode_body(body_params, headers)
|
338
|
+
body = nil
|
339
|
+
flattened_params = Util.flatten_params(body_params)
|
340
|
+
|
341
|
+
if headers["Content-Type"] == MultipartEncoder::MULTIPART_FORM_DATA
|
342
|
+
body, content_type = MultipartEncoder.encode(flattened_params)
|
343
|
+
|
344
|
+
# Set a new content type that also includes the multipart boundary.
|
345
|
+
# See `MultipartEncoder` for details.
|
346
|
+
headers["Content-Type"] = content_type
|
347
|
+
|
348
|
+
# `#to_s` any complex objects like files and the like to build output
|
349
|
+
# that's more condusive to logging.
|
350
|
+
flattened_params =
|
351
|
+
flattened_params.map { |k, v| [k, v.is_a?(String) ? v : v.to_s] }.to_h
|
352
|
+
else
|
353
|
+
body = Util.encode_parameters(body_params)
|
354
|
+
end
|
355
|
+
|
356
|
+
# We don't use `Util.encode_parameters` partly as an optimization (to not
|
357
|
+
# redo work we've already done), and partly because the encoded forms of
|
358
|
+
# certain characters introduce a lot of visual noise and it's nice to
|
359
|
+
# have a clearer format for logs.
|
360
|
+
body_log = flattened_params.map { |k, v| "#{k}=#{v}" }.join("&")
|
361
|
+
|
362
|
+
[body, body_log]
|
363
|
+
end
|
364
|
+
|
365
|
+
private def execute_request_with_rescues(method, api_base, context)
|
262
366
|
num_retries = 0
|
263
367
|
begin
|
264
368
|
request_start = Time.now
|
265
369
|
log_request(context, num_retries)
|
266
370
|
resp = yield
|
267
|
-
context = context.
|
268
|
-
|
371
|
+
context = context.dup_from_response_headers(resp)
|
372
|
+
|
373
|
+
handle_error_response(resp, context) if resp.code.to_i >= 400
|
374
|
+
|
375
|
+
log_response(context, request_start, resp.code.to_i, resp.body)
|
269
376
|
|
270
377
|
if Stripe.enable_telemetry? && context.request_id
|
271
378
|
request_duration_ms = ((Time.now - request_start) * 1000).to_int
|
@@ -281,27 +388,25 @@ module Stripe
|
|
281
388
|
# taint the original on a retry.
|
282
389
|
error_context = context
|
283
390
|
|
284
|
-
if e.
|
285
|
-
error_context = context.
|
391
|
+
if e.is_a?(Stripe::StripeError)
|
392
|
+
error_context = context.dup_from_response_headers(e.http_headers)
|
286
393
|
log_response(error_context, request_start,
|
287
|
-
e.
|
394
|
+
e.http_status, e.http_body)
|
288
395
|
else
|
289
396
|
log_response_error(error_context, request_start, e)
|
290
397
|
end
|
291
398
|
|
292
|
-
if self.class.should_retry?(e, num_retries)
|
399
|
+
if self.class.should_retry?(e, method: method, num_retries: num_retries)
|
293
400
|
num_retries += 1
|
294
401
|
sleep self.class.sleep_time(num_retries)
|
295
402
|
retry
|
296
403
|
end
|
297
404
|
|
298
405
|
case e
|
299
|
-
when
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
handle_network_error(e, error_context, num_retries, api_base)
|
304
|
-
end
|
406
|
+
when Stripe::StripeError
|
407
|
+
raise
|
408
|
+
when *NETWORK_ERROR_MESSAGES_MAP.keys
|
409
|
+
handle_network_error(e, error_context, num_retries, api_base)
|
305
410
|
|
306
411
|
# Only handle errors when we know we can do so, and re-raise otherwise.
|
307
412
|
# This should be pretty infrequent.
|
@@ -332,12 +437,12 @@ module Stripe
|
|
332
437
|
|
333
438
|
private def handle_error_response(http_resp, context)
|
334
439
|
begin
|
335
|
-
resp = StripeResponse.
|
440
|
+
resp = StripeResponse.from_net_http(http_resp)
|
336
441
|
error_data = resp.data[:error]
|
337
442
|
|
338
443
|
raise StripeError, "Indeterminate error" unless error_data
|
339
444
|
rescue JSON::ParserError, StripeError
|
340
|
-
raise general_api_error(http_resp
|
445
|
+
raise general_api_error(http_resp.code.to_i, http_resp.body)
|
341
446
|
end
|
342
447
|
|
343
448
|
error = if error_data.is_a?(String)
|
@@ -350,6 +455,28 @@ module Stripe
|
|
350
455
|
raise(error)
|
351
456
|
end
|
352
457
|
|
458
|
+
# Works around an edge case where we end up with both query parameters from
|
459
|
+
# parameteers and query parameters that were appended onto the end of the
|
460
|
+
# given path.
|
461
|
+
#
|
462
|
+
# Decode any parameters that were added onto the end of a path and add them
|
463
|
+
# to a unified query parameter hash so that all parameters end up in one
|
464
|
+
# place and all of them are correctly included in the final request.
|
465
|
+
private def merge_query_params(query_params, path)
|
466
|
+
u = URI.parse(path)
|
467
|
+
|
468
|
+
# Return original results if there was nothing to be found.
|
469
|
+
return query_params, path if u.query.nil?
|
470
|
+
|
471
|
+
query_params ||= {}
|
472
|
+
query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
|
473
|
+
|
474
|
+
# Reset the path minus any query parameters that were specified.
|
475
|
+
path = u.path
|
476
|
+
|
477
|
+
[query_params, path]
|
478
|
+
end
|
479
|
+
|
353
480
|
private def specific_api_error(resp, error_data, context)
|
354
481
|
Util.log_error("Stripe API error",
|
355
482
|
status: resp.http_status,
|
@@ -384,11 +511,8 @@ module Stripe
|
|
384
511
|
when 401
|
385
512
|
AuthenticationError.new(error_data[:message], opts)
|
386
513
|
when 402
|
387
|
-
# TODO: modify CardError constructor to make code a keyword argument
|
388
|
-
# so we don't have to delete it from opts
|
389
|
-
opts.delete(:code)
|
390
514
|
CardError.new(
|
391
|
-
error_data[:message], error_data[:param],
|
515
|
+
error_data[:message], error_data[:param],
|
392
516
|
opts
|
393
517
|
)
|
394
518
|
when 403
|
@@ -444,33 +568,18 @@ module Stripe
|
|
444
568
|
idempotency_key: context.idempotency_key,
|
445
569
|
request_id: context.request_id)
|
446
570
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
"Stripe. You may be seeing this message because your DNS is not " \
|
451
|
-
"working. To check, try running `host stripe.com` from the " \
|
452
|
-
"command line."
|
453
|
-
|
454
|
-
when Faraday::SSLError
|
455
|
-
message = "Could not establish a secure connection to Stripe, you " \
|
456
|
-
"may need to upgrade your OpenSSL version. To check, try running " \
|
457
|
-
"`openssl s_client -connect api.stripe.com:443` from the command " \
|
458
|
-
"line."
|
459
|
-
|
460
|
-
when Faraday::TimeoutError
|
461
|
-
api_base ||= Stripe.api_base
|
462
|
-
message = "Could not connect to Stripe (#{api_base}). " \
|
463
|
-
"Please check your internet connection and try again. " \
|
464
|
-
"If this problem persists, you should check Stripe's service " \
|
465
|
-
"status at https://status.stripe.com, or let us know at " \
|
466
|
-
"support@stripe.com."
|
467
|
-
|
468
|
-
else
|
469
|
-
message = "Unexpected error communicating with Stripe. " \
|
470
|
-
"If this problem persists, let us know at support@stripe.com."
|
571
|
+
errors, message = NETWORK_ERROR_MESSAGES_MAP.detect do |(e, _)|
|
572
|
+
error.is_a?(e)
|
573
|
+
end
|
471
574
|
|
575
|
+
if errors.nil?
|
576
|
+
message = "Unexpected error #{error.class.name} communicating " \
|
577
|
+
"with Stripe. Please let us know at support@stripe.com."
|
472
578
|
end
|
473
579
|
|
580
|
+
api_base ||= Stripe.api_base
|
581
|
+
message = message % api_base
|
582
|
+
|
474
583
|
message += " Request was retried #{num_retries} times." if num_retries > 0
|
475
584
|
|
476
585
|
raise APIConnectionError,
|
@@ -530,7 +639,7 @@ module Stripe
|
|
530
639
|
Util.log_debug("Request details",
|
531
640
|
body: context.body,
|
532
641
|
idempotency_key: context.idempotency_key,
|
533
|
-
|
642
|
+
query: context.query)
|
534
643
|
end
|
535
644
|
|
536
645
|
private def log_response(context, request_start, status, body)
|
@@ -577,7 +686,7 @@ module Stripe
|
|
577
686
|
attr_accessor :idempotency_key
|
578
687
|
attr_accessor :method
|
579
688
|
attr_accessor :path
|
580
|
-
attr_accessor :
|
689
|
+
attr_accessor :query
|
581
690
|
attr_accessor :request_id
|
582
691
|
|
583
692
|
# The idea with this method is that we might want to update some of
|
@@ -586,18 +695,7 @@ module Stripe
|
|
586
695
|
# with for a request. For example, we should trust whatever came back in
|
587
696
|
# a `Stripe-Version` header beyond what configuration information that we
|
588
697
|
# might have had available.
|
589
|
-
def
|
590
|
-
return self if resp.nil?
|
591
|
-
|
592
|
-
# Faraday's API is a little unusual. Normally it'll produce a response
|
593
|
-
# object with a `headers` method, but on error what it puts into
|
594
|
-
# `e.response` is an untyped `Hash`.
|
595
|
-
headers = if resp.is_a?(Faraday::Response)
|
596
|
-
resp.headers
|
597
|
-
else
|
598
|
-
resp[:headers]
|
599
|
-
end
|
600
|
-
|
698
|
+
def dup_from_response_headers(headers)
|
601
699
|
context = dup
|
602
700
|
context.account = headers["Stripe-Account"]
|
603
701
|
context.api_version = headers["Stripe-Version"]
|