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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -4
  3. data/.rubocop_todo.yml +10 -9
  4. data/.travis.yml +1 -5
  5. data/CHANGELOG.md +22 -0
  6. data/Gemfile +2 -12
  7. data/README.md +10 -10
  8. data/Rakefile +8 -7
  9. data/VERSION +1 -1
  10. data/lib/stripe.rb +56 -15
  11. data/lib/stripe/api_operations/list.rb +0 -6
  12. data/lib/stripe/connection_manager.rb +131 -0
  13. data/lib/stripe/error_object.rb +94 -0
  14. data/lib/stripe/errors.rb +15 -2
  15. data/lib/stripe/list_object.rb +2 -1
  16. data/lib/stripe/multipart_encoder.rb +131 -0
  17. data/lib/stripe/object_types.rb +0 -1
  18. data/lib/stripe/resources.rb +0 -1
  19. data/lib/stripe/resources/account.rb +1 -5
  20. data/lib/stripe/resources/account_link.rb +1 -1
  21. data/lib/stripe/resources/alipay_account.rb +1 -1
  22. data/lib/stripe/resources/apple_pay_domain.rb +1 -1
  23. data/lib/stripe/resources/application_fee.rb +1 -12
  24. data/lib/stripe/resources/application_fee_refund.rb +1 -1
  25. data/lib/stripe/resources/balance.rb +1 -1
  26. data/lib/stripe/resources/balance_transaction.rb +1 -1
  27. data/lib/stripe/resources/bank_account.rb +1 -1
  28. data/lib/stripe/resources/bitcoin_receiver.rb +1 -1
  29. data/lib/stripe/resources/bitcoin_transaction.rb +1 -1
  30. data/lib/stripe/resources/capability.rb +1 -1
  31. data/lib/stripe/resources/card.rb +1 -1
  32. data/lib/stripe/resources/charge.rb +7 -69
  33. data/lib/stripe/resources/checkout/session.rb +1 -1
  34. data/lib/stripe/resources/country_spec.rb +1 -1
  35. data/lib/stripe/resources/coupon.rb +1 -1
  36. data/lib/stripe/resources/credit_note.rb +1 -1
  37. data/lib/stripe/resources/customer.rb +3 -63
  38. data/lib/stripe/resources/customer_balance_transaction.rb +1 -1
  39. data/lib/stripe/resources/discount.rb +1 -1
  40. data/lib/stripe/resources/dispute.rb +1 -7
  41. data/lib/stripe/resources/ephemeral_key.rb +1 -1
  42. data/lib/stripe/resources/event.rb +1 -1
  43. data/lib/stripe/resources/exchange_rate.rb +1 -1
  44. data/lib/stripe/resources/file.rb +3 -13
  45. data/lib/stripe/resources/file_link.rb +1 -1
  46. data/lib/stripe/resources/invoice.rb +6 -1
  47. data/lib/stripe/resources/invoice_item.rb +1 -1
  48. data/lib/stripe/resources/invoice_line_item.rb +1 -1
  49. data/lib/stripe/resources/issuing/authorization.rb +1 -1
  50. data/lib/stripe/resources/issuing/card.rb +1 -1
  51. data/lib/stripe/resources/issuing/card_details.rb +1 -1
  52. data/lib/stripe/resources/issuing/cardholder.rb +1 -1
  53. data/lib/stripe/resources/issuing/dispute.rb +1 -1
  54. data/lib/stripe/resources/issuing/transaction.rb +1 -1
  55. data/lib/stripe/resources/login_link.rb +1 -1
  56. data/lib/stripe/resources/order.rb +1 -9
  57. data/lib/stripe/resources/order_return.rb +1 -1
  58. data/lib/stripe/resources/payment_intent.rb +1 -1
  59. data/lib/stripe/resources/payment_method.rb +1 -1
  60. data/lib/stripe/resources/payout.rb +1 -7
  61. data/lib/stripe/resources/person.rb +1 -1
  62. data/lib/stripe/resources/plan.rb +1 -1
  63. data/lib/stripe/resources/product.rb +1 -1
  64. data/lib/stripe/resources/radar/early_fraud_warning.rb +1 -1
  65. data/lib/stripe/resources/radar/value_list.rb +1 -1
  66. data/lib/stripe/resources/radar/value_list_item.rb +1 -1
  67. data/lib/stripe/resources/recipient.rb +1 -5
  68. data/lib/stripe/resources/recipient_transfer.rb +1 -1
  69. data/lib/stripe/resources/refund.rb +1 -1
  70. data/lib/stripe/resources/reporting/report_run.rb +1 -1
  71. data/lib/stripe/resources/reporting/report_type.rb +1 -1
  72. data/lib/stripe/resources/reversal.rb +1 -1
  73. data/lib/stripe/resources/review.rb +1 -1
  74. data/lib/stripe/resources/setup_intent.rb +1 -1
  75. data/lib/stripe/resources/sigma/scheduled_query_run.rb +1 -1
  76. data/lib/stripe/resources/sku.rb +1 -1
  77. data/lib/stripe/resources/source.rb +1 -7
  78. data/lib/stripe/resources/source_transaction.rb +1 -1
  79. data/lib/stripe/resources/subscription.rb +9 -9
  80. data/lib/stripe/resources/subscription_item.rb +1 -1
  81. data/lib/stripe/resources/subscription_schedule.rb +1 -1
  82. data/lib/stripe/resources/tax_id.rb +1 -1
  83. data/lib/stripe/resources/tax_rate.rb +1 -1
  84. data/lib/stripe/resources/terminal/connection_token.rb +1 -1
  85. data/lib/stripe/resources/terminal/location.rb +1 -1
  86. data/lib/stripe/resources/terminal/reader.rb +1 -1
  87. data/lib/stripe/resources/three_d_secure.rb +1 -1
  88. data/lib/stripe/resources/token.rb +1 -1
  89. data/lib/stripe/resources/topup.rb +1 -1
  90. data/lib/stripe/resources/transfer.rb +1 -6
  91. data/lib/stripe/resources/usage_record.rb +1 -17
  92. data/lib/stripe/resources/usage_record_summary.rb +1 -1
  93. data/lib/stripe/resources/webhook_endpoint.rb +1 -1
  94. data/lib/stripe/stripe_client.rb +281 -183
  95. data/lib/stripe/stripe_object.rb +4 -23
  96. data/lib/stripe/stripe_response.rb +53 -21
  97. data/lib/stripe/util.rb +10 -11
  98. data/lib/stripe/version.rb +1 -1
  99. data/lib/stripe/webhook.rb +1 -1
  100. data/stripe.gemspec +6 -9
  101. data/test/stripe/account_test.rb +0 -16
  102. data/test/stripe/api_operations_test.rb +2 -2
  103. data/test/stripe/api_resource_test.rb +2 -10
  104. data/test/stripe/charge_test.rb +0 -16
  105. data/test/stripe/connection_manager_test.rb +138 -0
  106. data/test/stripe/customer_test.rb +1 -44
  107. data/test/stripe/errors_test.rb +29 -8
  108. data/test/stripe/file_test.rb +0 -10
  109. data/test/stripe/invoice_test.rb +17 -1
  110. data/test/stripe/list_object_test.rb +0 -16
  111. data/test/stripe/login_link_test.rb +1 -1
  112. data/test/stripe/multipart_encoder_test.rb +130 -0
  113. data/test/stripe/payment_intent_test.rb +1 -1
  114. data/test/stripe/setup_intent_test.rb +1 -1
  115. data/test/stripe/source_test.rb +0 -18
  116. data/test/stripe/stripe_client_test.rb +214 -29
  117. data/test/stripe/stripe_object_test.rb +7 -35
  118. data/test/stripe/stripe_response_test.rb +70 -24
  119. data/test/stripe/subscription_test.rb +2 -2
  120. data/test/stripe/webhook_test.rb +2 -2
  121. data/test/stripe_mock.rb +4 -3
  122. data/test/stripe_test.rb +0 -13
  123. data/test/test_helper.rb +10 -5
  124. metadata +11 -39
  125. data/lib/stripe/resources/issuer_fraud_record.rb +0 -9
  126. data/test/stripe/file_upload_test.rb +0 -79
  127. data/test/stripe/issuer_fraud_record_test.rb +0 -20
  128. data/test/stripe/usage_record_test.rb +0 -28
@@ -4,6 +4,6 @@ module Stripe
4
4
  class OrderReturn < APIResource
5
5
  extend Stripe::APIOperations::List
6
6
 
7
- OBJECT_NAME = "order_return".freeze
7
+ OBJECT_NAME = "order_return"
8
8
  end
9
9
  end
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "payment_intent".freeze
9
+ OBJECT_NAME = "payment_intent"
10
10
 
11
11
  custom_method :cancel, http_verb: :post
12
12
  custom_method :capture, http_verb: :post
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "payment_method".freeze
9
+ OBJECT_NAME = "payment_method"
10
10
 
11
11
  custom_method :attach, http_verb: :post
12
12
  custom_method :detach, http_verb: :post
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "payout".freeze
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
@@ -5,7 +5,7 @@ module Stripe
5
5
  extend Stripe::APIOperations::List
6
6
  include Stripe::APIOperations::Save
7
7
 
8
- OBJECT_NAME = "person".freeze
8
+ OBJECT_NAME = "person"
9
9
 
10
10
  def resource_url
11
11
  if !respond_to?(:account) || account.nil?
@@ -7,6 +7,6 @@ module Stripe
7
7
  extend Stripe::APIOperations::List
8
8
  include Stripe::APIOperations::Save
9
9
 
10
- OBJECT_NAME = "plan".freeze
10
+ OBJECT_NAME = "plan"
11
11
  end
12
12
  end
@@ -7,6 +7,6 @@ module Stripe
7
7
  extend Stripe::APIOperations::List
8
8
  include Stripe::APIOperations::Save
9
9
 
10
- OBJECT_NAME = "product".freeze
10
+ OBJECT_NAME = "product"
11
11
  end
12
12
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  class EarlyFraudWarning < APIResource
6
6
  extend Stripe::APIOperations::List
7
7
 
8
- OBJECT_NAME = "radar.early_fraud_warning".freeze
8
+ OBJECT_NAME = "radar.early_fraud_warning"
9
9
  end
10
10
  end
11
11
  end
@@ -8,7 +8,7 @@ module Stripe
8
8
  extend Stripe::APIOperations::List
9
9
  include Stripe::APIOperations::Save
10
10
 
11
- OBJECT_NAME = "radar.value_list".freeze
11
+ OBJECT_NAME = "radar.value_list"
12
12
  end
13
13
  end
14
14
  end
@@ -7,7 +7,7 @@ module Stripe
7
7
  include Stripe::APIOperations::Delete
8
8
  extend Stripe::APIOperations::List
9
9
 
10
- OBJECT_NAME = "radar.value_list_item".freeze
10
+ OBJECT_NAME = "radar.value_list_item"
11
11
  end
12
12
  end
13
13
  end
@@ -8,10 +8,6 @@ module Stripe
8
8
  extend Stripe::APIOperations::List
9
9
  include Stripe::APIOperations::Save
10
10
 
11
- OBJECT_NAME = "recipient".freeze
12
-
13
- def transfers
14
- Transfer.all({ recipient: id }, @api_key)
15
- end
11
+ OBJECT_NAME = "recipient"
16
12
  end
17
13
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Stripe
4
4
  class RecipientTransfer < StripeObject
5
- OBJECT_NAME = "recipient_transfer".freeze
5
+ OBJECT_NAME = "recipient_transfer"
6
6
  end
7
7
  end
@@ -6,6 +6,6 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "refund".freeze
9
+ OBJECT_NAME = "refund"
10
10
  end
11
11
  end
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::Create
7
7
  extend Stripe::APIOperations::List
8
8
 
9
- OBJECT_NAME = "reporting.report_run".freeze
9
+ OBJECT_NAME = "reporting.report_run"
10
10
  end
11
11
  end
12
12
  end
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::Create
7
7
  extend Stripe::APIOperations::List
8
8
 
9
- OBJECT_NAME = "reporting.report_type".freeze
9
+ OBJECT_NAME = "reporting.report_type"
10
10
  end
11
11
  end
12
12
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  extend Stripe::APIOperations::List
6
6
  include Stripe::APIOperations::Save
7
7
 
8
- OBJECT_NAME = "transfer_reversal".freeze
8
+ OBJECT_NAME = "transfer_reversal"
9
9
 
10
10
  def resource_url
11
11
  "#{Transfer.resource_url}/#{CGI.escape(transfer)}/reversals" \
@@ -4,7 +4,7 @@ module Stripe
4
4
  class Review < APIResource
5
5
  extend Stripe::APIOperations::List
6
6
 
7
- OBJECT_NAME = "review".freeze
7
+ OBJECT_NAME = "review"
8
8
 
9
9
  custom_method :approve, http_verb: :post
10
10
 
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "setup_intent".freeze
9
+ OBJECT_NAME = "setup_intent"
10
10
 
11
11
  custom_method :cancel, http_verb: :post
12
12
  custom_method :confirm, http_verb: :post
@@ -5,7 +5,7 @@ module Stripe
5
5
  class ScheduledQueryRun < APIResource
6
6
  extend Stripe::APIOperations::List
7
7
 
8
- OBJECT_NAME = "scheduled_query_run".freeze
8
+ OBJECT_NAME = "scheduled_query_run"
9
9
 
10
10
  def self.resource_url
11
11
  "/v1/sigma/scheduled_query_runs"
@@ -7,6 +7,6 @@ module Stripe
7
7
  extend Stripe::APIOperations::List
8
8
  include Stripe::APIOperations::Save
9
9
 
10
- OBJECT_NAME = "sku".freeze
10
+ OBJECT_NAME = "sku"
11
11
  end
12
12
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  extend Stripe::APIOperations::Create
6
6
  include Stripe::APIOperations::Save
7
7
 
8
- OBJECT_NAME = "source".freeze
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Stripe
4
4
  class SourceTransaction < StripeObject
5
- OBJECT_NAME = "source_transaction".freeze
5
+ OBJECT_NAME = "source_transaction"
6
6
  end
7
7
  end
@@ -7,19 +7,19 @@ module Stripe
7
7
  extend Stripe::APIOperations::List
8
8
  include Stripe::APIOperations::Save
9
9
 
10
- OBJECT_NAME = "subscription".freeze
10
+ OBJECT_NAME = "subscription"
11
11
 
12
12
  custom_method :delete_discount, http_verb: :delete, http_path: "discount"
13
13
 
14
- save_nested_resource :source
15
-
16
- def delete_discount
17
- _, opts = request(:delete, discount_url)
18
- initialize_from({ discount: nil }, opts, true)
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
- private def discount_url
22
- resource_url + "/discount"
23
- end
23
+ save_nested_resource :source
24
24
  end
25
25
  end
@@ -8,7 +8,7 @@ module Stripe
8
8
  include Stripe::APIOperations::Save
9
9
  extend Stripe::APIOperations::NestedResource
10
10
 
11
- OBJECT_NAME = "subscription_item".freeze
11
+ OBJECT_NAME = "subscription_item"
12
12
 
13
13
  nested_resource_class_methods :usage_record, operations: %i[create]
14
14
 
@@ -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".freeze
9
+ OBJECT_NAME = "subscription_schedule"
10
10
 
11
11
  custom_method :cancel, http_verb: :post
12
12
  custom_method :release, http_verb: :post
@@ -5,7 +5,7 @@ module Stripe
5
5
  include Stripe::APIOperations::Delete
6
6
  extend Stripe::APIOperations::List
7
7
 
8
- OBJECT_NAME = "tax_id".freeze
8
+ OBJECT_NAME = "tax_id"
9
9
 
10
10
  def resource_url
11
11
  if !respond_to?(:customer) || customer.nil?
@@ -6,6 +6,6 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "tax_rate".freeze
9
+ OBJECT_NAME = "tax_rate"
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  class ConnectionToken < APIResource
6
6
  extend Stripe::APIOperations::Create
7
7
 
8
- OBJECT_NAME = "terminal.connection_token".freeze
8
+ OBJECT_NAME = "terminal.connection_token"
9
9
  end
10
10
  end
11
11
  end
@@ -8,7 +8,7 @@ module Stripe
8
8
  extend Stripe::APIOperations::List
9
9
  include Stripe::APIOperations::Save
10
10
 
11
- OBJECT_NAME = "terminal.location".freeze
11
+ OBJECT_NAME = "terminal.location"
12
12
  end
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Stripe
8
8
  extend Stripe::APIOperations::List
9
9
  include Stripe::APIOperations::Save
10
10
 
11
- OBJECT_NAME = "terminal.reader".freeze
11
+ OBJECT_NAME = "terminal.reader"
12
12
  end
13
13
  end
14
14
  end
@@ -4,7 +4,7 @@ module Stripe
4
4
  class ThreeDSecure < APIResource
5
5
  extend Stripe::APIOperations::Create
6
6
 
7
- OBJECT_NAME = "three_d_secure".freeze
7
+ OBJECT_NAME = "three_d_secure"
8
8
 
9
9
  def self.resource_url
10
10
  "/v1/3d_secure"
@@ -4,6 +4,6 @@ module Stripe
4
4
  class Token < APIResource
5
5
  extend Stripe::APIOperations::Create
6
6
 
7
- OBJECT_NAME = "token".freeze
7
+ OBJECT_NAME = "token"
8
8
  end
9
9
  end
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "topup".freeze
9
+ OBJECT_NAME = "topup"
10
10
 
11
11
  custom_method :cancel, http_verb: :post
12
12
 
@@ -7,7 +7,7 @@ module Stripe
7
7
  include Stripe::APIOperations::Save
8
8
  extend Stripe::APIOperations::NestedResource
9
9
 
10
- OBJECT_NAME = "transfer".freeze
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".freeze
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Stripe
4
4
  class UsageRecordSummary < StripeObject
5
- OBJECT_NAME = "usage_record_summary".freeze
5
+ OBJECT_NAME = "usage_record_summary"
6
6
  end
7
7
  end
@@ -7,6 +7,6 @@ module Stripe
7
7
  extend Stripe::APIOperations::List
8
8
  include Stripe::APIOperations::Save
9
9
 
10
- OBJECT_NAME = "webhook_endpoint".freeze
10
+ OBJECT_NAME = "webhook_endpoint"
11
11
  end
12
12
  end
@@ -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
- attr_accessor :conn
9
-
10
- # Initializes a new StripeClient. Expects a Faraday connection object, and
11
- # uses a default connection unless one is passed.
12
- def initialize(conn = nil)
13
- self.conn = conn || self.class.default_conn
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
- Thread.current[:stripe_client] || default_client
32
+ current_thread_context.active_client || default_client
20
33
  end
21
34
 
22
- def self.default_client
23
- Thread.current[:stripe_client_default_client] ||=
24
- StripeClient.new(default_conn)
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 Faraday connection to be used when one isn't configured. This
28
- # object should never be mutated, and instead instantiating your own
29
- # connection and wrapping it in a StripeClient object should be preferred.
30
- def self.default_conn
31
- # We're going to keep connections around so that we can take advantage
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
- conn.proxy = Stripe.proxy if Stripe.proxy
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
- if Stripe.verify_ssl_certs
52
- conn.ssl.verify = true
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
- conn
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?(Faraday::TimeoutError)
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?(Faraday::ConnectionFailed)
83
-
84
- if error.is_a?(Faraday::ClientError) && error.response
85
- # 409 conflict
86
- return true if error.response[:status] == 409
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
- @last_response = nil
118
- old_stripe_client = Thread.current[:stripe_client]
119
- Thread.current[:stripe_client] = self
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, @last_response]
142
+ [res, self.class.current_thread_context.last_responses[object_id]]
124
143
  ensure
125
- Thread.current[:stripe_client] = old_stripe_client
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
- body = nil
162
+ body_params = nil
138
163
  query_params = nil
139
- case method.to_s.downcase.to_sym
164
+ case method
140
165
  when :get, :head, :delete
141
166
  query_params = params
142
167
  else
143
- body = params
168
+ body_params = params
144
169
  end
145
170
 
146
- # This works around an edge case where we end up with both query
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 = body ? params_encoder.encode(body) : nil
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.query_params = if query_params
179
- params_encoder.encode(query_params)
180
- end
181
-
182
- # note that both request body and query params will be passed through
183
- # `FaradayStripeEncoder`
184
- http_resp = execute_request_with_rescues(api_base, context) do
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.from_faraday_response(http_resp)
207
+ resp = StripeResponse.from_net_http(http_resp)
195
208
  rescue JSON::ParserError
196
- raise general_api_error(http_resp.status, http_resp.body)
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
- # We work around the problem by implementing our own simplified encoder and
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
- # This is quite subtle, but for a `multipart/form-data` request Faraday
226
- # will throw away the result of this encoder and build its body.
227
- def encode(hash)
228
- @cache.fetch(hash) do |k|
229
- @cache[k] = Util.encode_parameters(hash)
230
- end
231
- end
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
- # We should never need to do this so it's not implemented.
234
- def decode(_str)
235
- raise NotImplementedError,
236
- "#{self.class.name} does not implement #decode"
237
- end
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
- private def execute_request_with_rescues(api_base, context)
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.dup_from_response(resp)
268
- log_response(context, request_start, resp.status, resp.body)
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.respond_to?(:response) && e.response
285
- error_context = context.dup_from_response(e.response)
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.response[:status], e.response[:body])
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 Faraday::ClientError
300
- if e.response
301
- handle_error_response(e.response, error_context)
302
- else
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.from_faraday_hash(http_resp)
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[:status], http_resp[:body])
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], error_data[:code],
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
- case error
448
- when Faraday::ConnectionFailed
449
- message = "Unexpected error communicating when trying to connect to " \
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
- query_params: context.query_params)
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 :query_params
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 dup_from_response(resp)
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"]