stripe 4.20.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -4
- data/.rubocop_todo.yml +10 -9
- data/.travis.yml +2 -6
- data/CHANGELOG.md +52 -1
- data/Gemfile +2 -12
- data/README.md +10 -10
- data/Rakefile +8 -7
- data/VERSION +1 -1
- data/lib/stripe/api_operations/list.rb +0 -6
- data/lib/stripe/api_resource.rb +16 -0
- 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 +1 -4
- data/lib/stripe/resources/account.rb +7 -7
- 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 -5
- 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 +7 -3
- data/lib/stripe/resources/customer.rb +5 -66
- data/lib/stripe/resources/customer_balance_transaction.rb +1 -1
- data/lib/stripe/resources/discount.rb +1 -1
- data/lib/stripe/resources/dispute.rb +7 -9
- 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 +36 -11
- 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 +13 -5
- data/lib/stripe/resources/issuing/card.rb +7 -3
- 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 +13 -13
- data/lib/stripe/resources/order_return.rb +1 -1
- data/lib/stripe/resources/payment_intent.rb +19 -7
- data/lib/stripe/resources/payment_method.rb +13 -5
- data/lib/stripe/resources/payout.rb +7 -9
- 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 +7 -3
- data/lib/stripe/resources/setup_intent.rb +32 -0
- 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 +7 -9
- data/lib/stripe/resources/source_transaction.rb +1 -1
- data/lib/stripe/resources/subscription.rb +9 -9
- data/lib/stripe/resources/subscription_item.rb +4 -1
- data/lib/stripe/resources/subscription_schedule.rb +13 -13
- 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 +7 -3
- data/lib/stripe/resources/transfer.rb +7 -8
- 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/resources.rb +1 -2
- 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 +14 -11
- data/lib/stripe/version.rb +1 -1
- data/lib/stripe/webhook.rb +1 -1
- data/lib/stripe.rb +56 -15
- data/stripe.gemspec +10 -3
- data/test/stripe/account_test.rb +0 -16
- data/test/stripe/api_operations_test.rb +2 -2
- data/test/stripe/api_resource_test.rb +98 -8
- data/test/stripe/balance_transaction_test.rb +20 -0
- 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 +2 -2
- data/test/stripe/setup_intent_test.rb +84 -0
- 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_item_test.rb +12 -0
- data/test/stripe/subscription_schedule_test.rb +0 -34
- 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 +23 -43
- data/lib/stripe/resources/issuer_fraud_record.rb +0 -9
- data/lib/stripe/resources/subscription_schedule_revision.rb +0 -34
- data/test/stripe/file_upload_test.rb +0 -79
- data/test/stripe/issuer_fraud_record_test.rb +0 -20
- data/test/stripe/subscription_schedule_revision_test.rb +0 -37
- data/test/stripe/usage_record_test.rb +0 -28
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"]
|