stripe 1.58.0 → 2.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +4 -0
  3. data/.travis.yml +1 -2
  4. data/Gemfile +11 -12
  5. data/History.txt +8 -0
  6. data/README.md +44 -31
  7. data/Rakefile +1 -1
  8. data/VERSION +1 -1
  9. data/lib/stripe.rb +4 -344
  10. data/lib/stripe/account.rb +4 -4
  11. data/lib/stripe/api_operations/create.rb +2 -2
  12. data/lib/stripe/api_operations/delete.rb +2 -2
  13. data/lib/stripe/api_operations/list.rb +2 -2
  14. data/lib/stripe/api_operations/request.rb +9 -3
  15. data/lib/stripe/api_operations/save.rb +4 -6
  16. data/lib/stripe/api_resource.rb +2 -2
  17. data/lib/stripe/bank_account.rb +2 -2
  18. data/lib/stripe/bitcoin_receiver.rb +1 -1
  19. data/lib/stripe/charge.rb +12 -12
  20. data/lib/stripe/customer.rb +6 -6
  21. data/lib/stripe/dispute.rb +2 -3
  22. data/lib/stripe/errors.rb +20 -10
  23. data/lib/stripe/invoice.rb +4 -4
  24. data/lib/stripe/list_object.rb +2 -2
  25. data/lib/stripe/order.rb +4 -4
  26. data/lib/stripe/reversal.rb +1 -1
  27. data/lib/stripe/source.rb +2 -2
  28. data/lib/stripe/stripe_client.rb +396 -0
  29. data/lib/stripe/stripe_response.rb +48 -0
  30. data/lib/stripe/transfer.rb +2 -2
  31. data/lib/stripe/util.rb +6 -6
  32. data/lib/stripe/version.rb +1 -1
  33. data/spec/fixtures.json +0 -0
  34. data/spec/fixtures.yaml +0 -0
  35. data/spec/spec.json +0 -0
  36. data/spec/spec.yaml +0 -0
  37. data/stripe.gemspec +1 -1
  38. data/test/api_fixtures.rb +29 -0
  39. data/test/api_stub_helpers.rb +125 -0
  40. data/test/stripe/account_test.rb +153 -247
  41. data/test/stripe/alipay_account_test.rb +10 -2
  42. data/test/stripe/api_operations_test.rb +3 -3
  43. data/test/stripe/api_resource_test.rb +139 -499
  44. data/test/stripe/apple_pay_domain_test.rb +22 -23
  45. data/test/stripe/application_fee_refund_test.rb +22 -31
  46. data/test/stripe/application_fee_test.rb +6 -17
  47. data/test/stripe/balance_test.rb +3 -3
  48. data/test/stripe/bank_account_test.rb +31 -11
  49. data/test/stripe/bitcoin_receiver_test.rb +51 -42
  50. data/test/stripe/bitcoin_transaction_test.rb +11 -19
  51. data/test/stripe/charge_test.rb +39 -126
  52. data/test/stripe/country_spec_test.rb +7 -30
  53. data/test/stripe/coupon_test.rb +33 -17
  54. data/test/stripe/customer_card_test.rb +25 -46
  55. data/test/stripe/customer_test.rb +86 -81
  56. data/test/stripe/dispute_test.rb +27 -38
  57. data/test/stripe/errors_test.rb +2 -2
  58. data/test/stripe/file_upload_test.rb +32 -24
  59. data/test/stripe/invoice_item_test.rb +46 -10
  60. data/test/stripe/invoice_test.rb +48 -48
  61. data/test/stripe/list_object_test.rb +22 -31
  62. data/test/stripe/order_return_test.rb +11 -15
  63. data/test/stripe/order_test.rb +38 -51
  64. data/test/stripe/plan_test.rb +39 -18
  65. data/test/stripe/product_test.rb +29 -33
  66. data/test/stripe/recipient_card_test.rb +23 -40
  67. data/test/stripe/recipient_test.rb +39 -10
  68. data/test/stripe/refund_test.rb +20 -45
  69. data/test/stripe/reversal_test.rb +27 -31
  70. data/test/stripe/sku_test.rb +36 -19
  71. data/test/stripe/source_test.rb +26 -66
  72. data/test/stripe/stripe_client_test.rb +428 -0
  73. data/test/stripe/stripe_object_test.rb +6 -2
  74. data/test/stripe/stripe_response_test.rb +46 -0
  75. data/test/stripe/subscription_item_test.rb +37 -59
  76. data/test/stripe/subscription_test.rb +40 -176
  77. data/test/stripe/three_d_secure_test.rb +13 -12
  78. data/test/stripe/transfer_test.rb +36 -19
  79. data/test/stripe_test.rb +3 -36
  80. data/test/test_data.rb +5 -931
  81. data/test/test_helper.rb +21 -25
  82. metadata +22 -17
  83. data/test/stripe/charge_refund_test.rb +0 -67
  84. data/test/stripe/metadata_test.rb +0 -129
@@ -37,8 +37,8 @@ module Stripe
37
37
 
38
38
  def reject(params={}, opts={})
39
39
  opts = Util.normalize_opts(opts)
40
- response, opts = request(:post, resource_url + '/reject', params, opts)
41
- initialize_from(response, opts)
40
+ resp, opts = request(:post, resource_url + '/reject', params, opts)
41
+ initialize_from(resp.data, opts)
42
42
  end
43
43
 
44
44
  # Somewhat unfortunately, we attempt to do a special encoding trick when
@@ -93,9 +93,9 @@ module Stripe
93
93
 
94
94
  def deauthorize(client_id, opts={})
95
95
  opts = {:api_base => Stripe.connect_base}.merge(Util.normalize_opts(opts))
96
- response, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts)
96
+ resp, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts)
97
97
  opts.delete(:api_base) # the api_base here is a one-off, don't persist it
98
- Util.convert_to_stripe_object(response, opts)
98
+ Util.convert_to_stripe_object(resp.data, opts)
99
99
  end
100
100
 
101
101
  ARGUMENT_NOT_PROVIDED = Object.new
@@ -2,8 +2,8 @@ module Stripe
2
2
  module APIOperations
3
3
  module Create
4
4
  def create(params={}, opts={})
5
- response, opts = request(:post, resource_url, params, opts)
6
- Util.convert_to_stripe_object(response, opts)
5
+ resp, opts = request(:post, resource_url, params, opts)
6
+ Util.convert_to_stripe_object(resp.data, opts)
7
7
  end
8
8
  end
9
9
  end
@@ -3,8 +3,8 @@ module Stripe
3
3
  module Delete
4
4
  def delete(params={}, opts={})
5
5
  opts = Util.normalize_opts(opts)
6
- response, opts = request(:delete, resource_url, params, opts)
7
- initialize_from(response, opts)
6
+ resp, opts = request(:delete, resource_url, params, opts)
7
+ initialize_from(resp.data, opts)
8
8
  end
9
9
  end
10
10
  end
@@ -4,8 +4,8 @@ module Stripe
4
4
  def list(filters={}, opts={})
5
5
  opts = Util.normalize_opts(opts)
6
6
 
7
- response, opts = request(:get, resource_url, filters, opts)
8
- obj = ListObject.construct_from(response, opts)
7
+ resp, opts = request(:get, resource_url, filters, opts)
8
+ obj = ListObject.construct_from(resp.data, opts)
9
9
 
10
10
  # set filters so that we can fetch the same limit, expansions, and
11
11
  # predicates when accessing the next and previous pages
@@ -2,17 +2,23 @@ module Stripe
2
2
  module APIOperations
3
3
  module Request
4
4
  module ClassMethods
5
- OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :stripe_account, :stripe_version]
5
+ OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :client, :stripe_account, :stripe_version]
6
6
 
7
7
  def request(method, url, params={}, opts={})
8
8
  opts = Util.normalize_opts(opts)
9
+ opts[:client] ||= StripeClient.active_client
9
10
 
10
11
  headers = opts.clone
11
12
  api_key = headers.delete(:api_key)
12
13
  api_base = headers.delete(:api_base)
14
+ client = headers.delete(:client)
13
15
  # Assume all remaining opts must be headers
14
16
 
15
- response, opts[:api_key] = Stripe.request(method, url, api_key, params, headers, api_base)
17
+ resp, opts[:api_key] = client.execute_request(
18
+ method, url,
19
+ api_base: api_base, api_key: api_key,
20
+ headers: headers, params: params
21
+ )
16
22
 
17
23
  # Hash#select returns an array before 1.9
18
24
  opts_to_persist = {}
@@ -22,7 +28,7 @@ module Stripe
22
28
  end
23
29
  end
24
30
 
25
- [response, opts_to_persist]
31
+ [resp, opts_to_persist]
26
32
  end
27
33
  end
28
34
 
@@ -21,8 +21,8 @@ module Stripe
21
21
  end
22
22
  end
23
23
 
24
- response, opts = request(:post, "#{resource_url}/#{id}", params, opts)
25
- Util.convert_to_stripe_object(response, opts)
24
+ resp, opts = request(:post, "#{resource_url}/#{id}", params, opts)
25
+ Util.convert_to_stripe_object(resp.data, opts)
26
26
  end
27
27
  end
28
28
 
@@ -57,10 +57,8 @@ module Stripe
57
57
  # generated a uri for this object with an identifier baked in
58
58
  values.delete(:id)
59
59
 
60
- response, opts = request(:post, save_url, values, opts)
61
- initialize_from(response, opts)
62
-
63
- self
60
+ resp, opts = request(:post, save_url, values, opts)
61
+ initialize_from(resp.data, opts)
64
62
  end
65
63
 
66
64
  def self.included(base)
@@ -54,8 +54,8 @@ module Stripe
54
54
  end
55
55
 
56
56
  def refresh
57
- response, opts = request(:get, resource_url, @retrieve_params)
58
- initialize_from(response, opts)
57
+ resp, opts = request(:get, resource_url, @retrieve_params)
58
+ initialize_from(resp.data, opts)
59
59
  end
60
60
 
61
61
  def self.retrieve(id, opts={})
@@ -5,8 +5,8 @@ module Stripe
5
5
  extend Stripe::APIOperations::List
6
6
 
7
7
  def verify(params={}, opts={})
8
- response, opts = request(:post, resource_url + '/verify', params, opts)
9
- initialize_from(response, opts)
8
+ resp, opts = request(:post, resource_url + '/verify', params, opts)
9
+ initialize_from(resp.data, opts)
10
10
  end
11
11
 
12
12
  def resource_url
@@ -10,7 +10,7 @@ module Stripe
10
10
  end
11
11
 
12
12
  def resource_url
13
- if respond_to?(:customer) && !self.customer.nil?
13
+ if respond_to?(:customer) && !self.customer.nil? && self.customer != ""
14
14
  "#{Customer.resource_url}/#{CGI.escape(customer)}/sources/#{CGI.escape(id)}"
15
15
  else
16
16
  "#{self.class.resource_url}/#{CGI.escape(id)}"
@@ -20,41 +20,41 @@ module Stripe
20
20
  # from the server
21
21
  self.refresh
22
22
  else
23
- response, opts = request(:post, refund_url, params, opts)
24
- initialize_from(response, opts)
23
+ resp, opts = request(:post, refund_url, params, opts)
24
+ initialize_from(resp.data, opts)
25
25
  end
26
26
  end
27
27
 
28
28
  def capture(params={}, opts={})
29
- response, opts = request(:post, capture_url, params, opts)
30
- initialize_from(response, opts)
29
+ resp, opts = request(:post, capture_url, params, opts)
30
+ initialize_from(resp.data, opts)
31
31
  end
32
32
 
33
33
  def update_dispute(params={}, opts={})
34
- response, opts = request(:post, dispute_url, params, opts)
35
- initialize_from({ :dispute => response }, opts, true)
34
+ resp, opts = request(:post, dispute_url, params, opts)
35
+ initialize_from({ :dispute => resp.data }, opts, true)
36
36
  dispute
37
37
  end
38
38
 
39
39
  def close_dispute(params={}, opts={})
40
- response, opts = request(:post, close_dispute_url, params, opts)
41
- initialize_from(response, opts)
40
+ resp, opts = request(:post, close_dispute_url, params, opts)
41
+ initialize_from(resp.data, opts)
42
42
  end
43
43
 
44
44
  def mark_as_fraudulent
45
45
  params = {
46
46
  :fraud_details => { :user_report => 'fraudulent' }
47
47
  }
48
- response, opts = request(:post, resource_url, params)
49
- initialize_from(response, opts)
48
+ resp, opts = request(:post, resource_url, params)
49
+ initialize_from(resp.data, opts)
50
50
  end
51
51
 
52
52
  def mark_as_safe
53
53
  params = {
54
54
  :fraud_details => { :user_report => 'safe' }
55
55
  }
56
- response, opts = request(:post, resource_url, params)
57
- initialize_from(response, opts)
56
+ resp, opts = request(:post, resource_url, params)
57
+ initialize_from(resp.data, opts)
58
58
  end
59
59
 
60
60
  private
@@ -38,20 +38,20 @@ module Stripe
38
38
  end
39
39
 
40
40
  def cancel_subscription(params={}, opts={})
41
- response, opts = request(:delete, subscription_url, params, opts)
42
- initialize_from({ :subscription => response }, opts, true)
41
+ resp, opts = request(:delete, subscription_url, params, opts)
42
+ initialize_from({ :subscription => resp.data }, opts, true)
43
43
  subscription
44
44
  end
45
45
 
46
46
  def update_subscription(params={}, opts={})
47
- response, opts = request(:post, subscription_url, params, opts)
48
- initialize_from({ :subscription => response }, opts, true)
47
+ resp, opts = request(:post, subscription_url, params, opts)
48
+ initialize_from({ :subscription => resp.data }, opts, true)
49
49
  subscription
50
50
  end
51
51
 
52
52
  def create_subscription(params={}, opts={})
53
- response, opts = request(:post, subscriptions_url, params, opts)
54
- initialize_from({ :subscription => response }, opts, true)
53
+ resp, opts = request(:post, subscriptions_url, params, opts)
54
+ initialize_from({ :subscription => resp.data }, opts, true)
55
55
  subscription
56
56
  end
57
57
 
@@ -1,12 +1,11 @@
1
1
  module Stripe
2
2
  class Dispute < APIResource
3
3
  extend Stripe::APIOperations::List
4
- extend Stripe::APIOperations::Create
5
4
  include Stripe::APIOperations::Save
6
5
 
7
6
  def close(params={}, opts={})
8
- response, opts = request(:post, close_url, params, opts)
9
- initialize_from(response, opts)
7
+ resp, opts = request(:post, close_url, params, opts)
8
+ initialize_from(resp.data, opts)
10
9
  end
11
10
 
12
11
  def close_url
@@ -3,14 +3,22 @@ module Stripe
3
3
  # errors derive.
4
4
  class StripeError < StandardError
5
5
  attr_reader :message
6
- attr_reader :http_status
6
+
7
+ # Response contains a StripeResponse object that has some basic information
8
+ # about the response that conveyed the error.
9
+ attr_accessor :response
10
+
11
+ # These fields are now available as part of #response and that usage should
12
+ # be preferred.
7
13
  attr_reader :http_body
8
14
  attr_reader :http_headers
15
+ attr_reader :http_status
16
+ attr_reader :json_body # equivalent to #data
9
17
  attr_reader :request_id
10
- attr_reader :json_body
11
18
 
12
- def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil,
13
- http_headers=nil)
19
+ # Initializes a StripeError.
20
+ def initialize(message=nil, http_status: nil, http_body: nil, json_body: nil,
21
+ http_headers: nil)
14
22
  @message = message
15
23
  @http_status = http_status
16
24
  @http_body = http_body
@@ -49,9 +57,10 @@ module Stripe
49
57
  class CardError < StripeError
50
58
  attr_reader :param, :code
51
59
 
52
- def initialize(message, param, code, http_status=nil, http_body=nil, json_body=nil,
53
- http_headers=nil)
54
- super(message, http_status, http_body, json_body, http_headers)
60
+ def initialize(message, param, code, http_status: nil, http_body: nil, json_body: nil,
61
+ http_headers: nil)
62
+ super(message, http_status: http_status, http_body: http_body,
63
+ json_body: json_body, http_headers: http_headers)
55
64
  @param = param
56
65
  @code = code
57
66
  end
@@ -62,9 +71,10 @@ module Stripe
62
71
  class InvalidRequestError < StripeError
63
72
  attr_accessor :param
64
73
 
65
- def initialize(message, param, http_status=nil, http_body=nil, json_body=nil,
66
- http_headers=nil)
67
- super(message, http_status, http_body, json_body, http_headers)
74
+ def initialize(message, param, http_status: nil, http_body: nil, json_body: nil,
75
+ http_headers: nil)
76
+ super(message, http_status: http_status, http_body: http_body,
77
+ json_body: json_body, http_headers: http_headers)
68
78
  @param = param
69
79
  end
70
80
  end
@@ -5,13 +5,13 @@ module Stripe
5
5
  extend Stripe::APIOperations::Create
6
6
 
7
7
  def self.upcoming(params, opts={})
8
- response, opts = request(:get, upcoming_url, params, opts)
9
- Util.convert_to_stripe_object(response, opts)
8
+ resp, opts = request(:get, upcoming_url, params, opts)
9
+ Util.convert_to_stripe_object(resp.data, opts)
10
10
  end
11
11
 
12
12
  def pay(opts={})
13
- response, opts = request(:post, pay_url, {}, opts)
14
- initialize_from(response, opts)
13
+ resp, opts = request(:post, pay_url, {}, opts)
14
+ initialize_from(resp.data, opts)
15
15
  end
16
16
 
17
17
  private
@@ -64,8 +64,8 @@ module Stripe
64
64
 
65
65
  def retrieve(id, opts={})
66
66
  id, retrieve_params = Util.normalize_id(id)
67
- response, opts = request(:get,"#{resource_url}/#{CGI.escape(id)}", retrieve_params, opts)
68
- Util.convert_to_stripe_object(response, opts)
67
+ resp, opts = request(:get,"#{resource_url}/#{CGI.escape(id)}", retrieve_params, opts)
68
+ Util.convert_to_stripe_object(resp.data, opts)
69
69
  end
70
70
 
71
71
  # Fetches the next page in the resource list (if there is one).
@@ -5,13 +5,13 @@ module Stripe
5
5
  include Stripe::APIOperations::Save
6
6
 
7
7
  def pay(params, opts={})
8
- response, opts = request(:post, pay_url, params, opts)
9
- initialize_from(response, opts)
8
+ resp, opts = request(:post, pay_url, params, opts)
9
+ initialize_from(resp.data, opts)
10
10
  end
11
11
 
12
12
  def return_order(params, opts={})
13
- response, opts = request(:post, returns_url, params, opts)
14
- Util.convert_to_stripe_object(response, opts)
13
+ resp, opts = request(:post, returns_url, params, opts)
14
+ Util.convert_to_stripe_object(resp.data, opts)
15
15
  end
16
16
 
17
17
  private
@@ -1,7 +1,7 @@
1
1
  module Stripe
2
2
  class Reversal < APIResource
3
- include Stripe::APIOperations::Save
4
3
  extend Stripe::APIOperations::List
4
+ include Stripe::APIOperations::Save
5
5
 
6
6
  def resource_url
7
7
  "#{Transfer.resource_url}/#{CGI.escape(transfer)}/reversals/#{CGI.escape(id)}"
@@ -4,8 +4,8 @@ module Stripe
4
4
  include Stripe::APIOperations::Save
5
5
 
6
6
  def verify(params={}, opts={})
7
- response, opts = request(:post, resource_url + '/verify', params, opts)
8
- initialize_from(response, opts)
7
+ resp, opts = request(:post, resource_url + '/verify', params, opts)
8
+ initialize_from(resp.data, opts)
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,396 @@
1
+ module Stripe
2
+ # StripeClient executes requests against the Stripe API and allows a user to
3
+ # recover both a resource a call returns as well as a response object that
4
+ # contains information on the HTTP call.
5
+ class StripeClient
6
+ attr_accessor :conn
7
+
8
+ # Initializes a new StripeClient. Expects a Faraday connection object, and
9
+ # uses a default connection unless one is passed.
10
+ def initialize(conn = nil)
11
+ self.conn = conn || self.class.default_conn
12
+ @system_profiler = SystemProfiler.new
13
+ end
14
+
15
+ def self.active_client
16
+ Thread.current[:stripe_client] || default_client
17
+ end
18
+
19
+ def self.default_client
20
+ @default_client ||= StripeClient.new(default_conn)
21
+ end
22
+
23
+ # A default Faraday connection to be used when one isn't configured. This
24
+ # object should never be mutated, and instead instantiating your own
25
+ # connection and wrapping it in a StripeClient object should be preferred.
26
+ def self.default_conn
27
+ # We're going to keep connections around so that we can take advantage
28
+ # of connection re-use, so make sure that we have a separate connection
29
+ # object per thread.
30
+ Thread.current[:stripe_client_default_conn] ||= begin
31
+ conn = Faraday.new do |c|
32
+ c.use Faraday::Request::UrlEncoded
33
+ c.use Faraday::Response::RaiseError
34
+ c.adapter Faraday.default_adapter
35
+ end
36
+
37
+ if Stripe.verify_ssl_certs
38
+ conn.ssl.verify = true
39
+ conn.ssl.cert_store = Stripe.ca_store
40
+ else
41
+ conn.ssl.verify = false
42
+
43
+ unless @verify_ssl_warned
44
+ @verify_ssl_warned = true
45
+ $stderr.puts("WARNING: Running without SSL cert verification. " \
46
+ "You should never do this in production. " \
47
+ "Execute 'Stripe.verify_ssl_certs = true' to enable verification.")
48
+ end
49
+ end
50
+
51
+ conn
52
+ end
53
+ end
54
+
55
+ # Checks if an error is a problem that we should retry on. This includes both
56
+ # socket errors that may represent an intermittent problem and some special
57
+ # HTTP statuses.
58
+ def self.should_retry?(e, retry_count)
59
+ return false if retry_count >= Stripe.max_network_retries
60
+
61
+ # Retry on timeout-related problems (either on open or read).
62
+ return true if e.is_a?(Faraday::TimeoutError)
63
+
64
+ # Destination refused the connection, the connection was reset, or a
65
+ # variety of other connection failures. This could occur from a single
66
+ # saturated server, so retry in case it's intermittent.
67
+ return true if e.is_a?(Faraday::ConnectionFailed)
68
+
69
+ if e.is_a?(Faraday::ClientError) && e.response
70
+ # 409 conflict
71
+ return true if e.response[:status] == 409
72
+ end
73
+
74
+ false
75
+ end
76
+
77
+ def self.sleep_time(retry_count)
78
+ # Apply exponential backoff with initial_network_retry_delay on the
79
+ # number of attempts so far as inputs. Do not allow the number to exceed
80
+ # max_network_retry_delay.
81
+ sleep_seconds = [Stripe.initial_network_retry_delay * (2 ** (retry_count - 1)), Stripe.max_network_retry_delay].min
82
+
83
+ # Apply some jitter by randomizing the value in the range of (sleep_seconds
84
+ # / 2) to (sleep_seconds).
85
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
86
+
87
+ # But never sleep less than the base sleep seconds.
88
+ sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max
89
+
90
+ sleep_seconds
91
+ end
92
+
93
+ # Executes the API call within the given block. Usage looks like:
94
+ #
95
+ # client = StripeClient.new
96
+ # charge, resp = client.request { Charge.create }
97
+ #
98
+ def request(&block)
99
+ @last_response = nil
100
+ old_stripe_client = Thread.current[:stripe_client]
101
+ Thread.current[:stripe_client] = self
102
+
103
+ begin
104
+ res = block.call
105
+ [res, @last_response]
106
+ ensure
107
+ Thread.current[:stripe_client] = old_stripe_client
108
+ end
109
+ end
110
+
111
+ def execute_request(method, url,
112
+ api_base: nil, api_key: nil, headers: {}, params: {})
113
+
114
+ api_base ||= Stripe.api_base
115
+ api_key ||= Stripe.api_key
116
+
117
+ check_api_key!(api_key)
118
+
119
+ params = Util.objects_to_ids(params)
120
+ url = api_url(url, api_base)
121
+
122
+ case method.to_s.downcase.to_sym
123
+ when :get, :head, :delete
124
+ # Make params into GET parameters
125
+ url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
126
+ payload = nil
127
+ else
128
+ if headers[:content_type] && headers[:content_type] == "multipart/form-data"
129
+ payload = params
130
+ else
131
+ payload = Util.encode_parameters(params)
132
+ end
133
+ end
134
+
135
+ http_resp = execute_request_with_rescues(api_base, 0) do
136
+ conn.run_request(
137
+ method,
138
+ url,
139
+ payload,
140
+ # TODO: Convert RestClient-style parameters.
141
+ request_headers(api_key, method).update(headers)
142
+ ) do |req|
143
+ req.options.open_timeout = Stripe.open_timeout
144
+ req.options.timeout = Stripe.read_timeout
145
+ end
146
+ end
147
+
148
+ begin
149
+ resp = StripeResponse.from_faraday_response(http_resp)
150
+ rescue JSON::ParserError
151
+ raise general_api_error(http_resp.code, http_resp.body)
152
+ end
153
+
154
+ # Allows StripeClient#request to return a response object to a caller.
155
+ @last_response = resp
156
+ [resp, api_key]
157
+ end
158
+
159
+ private
160
+
161
+ def api_url(url='', api_base=nil)
162
+ (api_base || Stripe.api_base) + url
163
+ end
164
+
165
+ def check_api_key!(api_key)
166
+ unless api_key
167
+ raise AuthenticationError.new('No API key provided. ' \
168
+ 'Set your API key using "Stripe.api_key = <API-KEY>". ' \
169
+ 'You can generate API keys from the Stripe web interface. ' \
170
+ 'See https://stripe.com/api for details, or email support@stripe.com ' \
171
+ 'if you have any questions.')
172
+ end
173
+
174
+ if api_key =~ /\s/
175
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' \
176
+ 'whitespace. (HINT: You can double-check your API key from the ' \
177
+ 'Stripe web interface. See https://stripe.com/api for details, or ' \
178
+ 'email support@stripe.com if you have any questions.)')
179
+ end
180
+ end
181
+
182
+ def execute_request_with_rescues(api_base, retry_count, &block)
183
+ begin
184
+ resp = block.call
185
+
186
+ # We rescue all exceptions from a request so that we have an easy spot to
187
+ # implement our retry logic across the board. We'll re-raise if it's a type
188
+ # of exception that we didn't expect to handle.
189
+ rescue => e
190
+ if self.class.should_retry?(e, retry_count)
191
+ retry_count = retry_count + 1
192
+ sleep self.class.sleep_time(retry_count)
193
+ retry
194
+ end
195
+
196
+ case e
197
+ when Faraday::ClientError
198
+ if e.response
199
+ handle_api_error(e.response)
200
+ else
201
+ handle_network_error(e, retry_count, api_base)
202
+ end
203
+
204
+ # Only handle errors when we know we can do so, and re-raise otherwise.
205
+ # This should be pretty infrequent.
206
+ else
207
+ raise
208
+ end
209
+ end
210
+
211
+ resp
212
+ end
213
+
214
+ def general_api_error(status, body)
215
+ APIError.new("Invalid response object from API: #{body.inspect} " +
216
+ "(HTTP response code was #{status})",
217
+ http_status: status, http_body: body)
218
+ end
219
+
220
+
221
+ def handle_api_error(http_resp)
222
+ begin
223
+ resp = StripeResponse.from_faraday_hash(http_resp)
224
+ error = resp.data[:error]
225
+
226
+ unless error && error.is_a?(Hash)
227
+ raise StripeError.new("Indeterminate error")
228
+ end
229
+
230
+ rescue JSON::ParserError, StripeError
231
+ raise general_api_error(http_resp[:status], http_resp[:body])
232
+ end
233
+
234
+ case resp.http_status
235
+ when 400, 404
236
+ error = InvalidRequestError.new(
237
+ error[:message], error[:param],
238
+ http_status: resp.http_status, http_body: resp.http_body,
239
+ json_body: resp.data, http_headers: resp.http_headers
240
+ )
241
+ when 401
242
+ error = AuthenticationError.new(
243
+ error[:message],
244
+ http_status: resp.http_status, http_body: resp.http_body,
245
+ json_body: resp.data, http_headers: resp.http_headers
246
+ )
247
+ when 402
248
+ error = CardError.new(
249
+ error[:message], error[:param], error[:code],
250
+ http_status: resp.http_status, http_body: resp.http_body,
251
+ json_body: resp.data, http_headers: resp.http_headers
252
+ )
253
+ when 403
254
+ error = PermissionError.new(
255
+ error[:message],
256
+ http_status: resp.http_status, http_body: resp.http_body,
257
+ json_body: resp.data, http_headers: resp.http_headers
258
+ )
259
+ when 429
260
+ error = RateLimitError.new(
261
+ error[:message],
262
+ http_status: resp.http_status, http_body: resp.http_body,
263
+ json_body: resp.data, http_headers: resp.http_headers
264
+ )
265
+ else
266
+ error = APIError.new(
267
+ error[:message],
268
+ http_status: resp.http_status, http_body: resp.http_body,
269
+ json_body: resp.data, http_headers: resp.http_headers
270
+ )
271
+ end
272
+
273
+ error.response = resp
274
+ raise(error)
275
+ end
276
+
277
+ def handle_network_error(e, retry_count, api_base=nil)
278
+ case e
279
+ when Faraday::ConnectionFailed
280
+ message = "Unexpected error communicating when trying to connect to Stripe. " \
281
+ "You may be seeing this message because your DNS is not working. " \
282
+ "To check, try running 'host stripe.com' from the command line."
283
+
284
+ when Faraday::SSLError
285
+ message = "Could not establish a secure connection to Stripe, you may " \
286
+ "need to upgrade your OpenSSL version. To check, try running " \
287
+ "'openssl s_client -connect api.stripe.com:443' from the " \
288
+ "command line."
289
+
290
+ when Faraday::TimeoutError
291
+ api_base = Stripe.api_base unless api_base
292
+ message = "Could not connect to Stripe (#{api_base}). " \
293
+ "Please check your internet connection and try again. " \
294
+ "If this problem persists, you should check Stripe's service status at " \
295
+ "https://twitter.com/stripestatus, or let us know at support@stripe.com."
296
+
297
+ else
298
+ message = "Unexpected error communicating with Stripe. " \
299
+ "If this problem persists, let us know at support@stripe.com."
300
+
301
+ end
302
+
303
+ if retry_count > 0
304
+ message += " Request was retried #{retry_count} times."
305
+ end
306
+
307
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
308
+ end
309
+
310
+ def request_headers(api_key, method)
311
+ headers = {
312
+ 'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}",
313
+ 'Authorization' => "Bearer #{api_key}",
314
+ 'Content-Type' => 'application/x-www-form-urlencoded'
315
+ }
316
+
317
+ # It is only safe to retry network failures on post and delete
318
+ # requests if we add an Idempotency-Key header
319
+ if [:post, :delete].include?(method) && Stripe.max_network_retries > 0
320
+ headers['Idempotency-Key'] ||= SecureRandom.uuid
321
+ end
322
+
323
+ headers['Stripe-Version'] = Stripe.api_version if Stripe.api_version
324
+ headers['Stripe-Account'] = Stripe.stripe_account if Stripe.stripe_account
325
+
326
+ user_agent = @system_profiler.user_agent
327
+ begin
328
+ headers.update(
329
+ 'X-Stripe-Client-User-Agent' => JSON.generate(user_agent)
330
+ )
331
+ rescue => e
332
+ headers.update(
333
+ 'X-Stripe-Client-Raw-User-Agent' => user_agent.inspect,
334
+ :error => "#{e} (#{e.class})"
335
+ )
336
+ end
337
+
338
+ headers
339
+ end
340
+
341
+ # SystemProfiler extracts information about the system that we're running
342
+ # in so that we can generate a rich user agent header to help debug
343
+ # integrations.
344
+ class SystemProfiler
345
+ def self.get_uname
346
+ if File.exist?('/proc/version')
347
+ File.read('/proc/version').strip
348
+ else
349
+ case RbConfig::CONFIG['host_os']
350
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
351
+ get_uname_from_system
352
+ when /mswin|mingw/i
353
+ get_uname_from_system_ver
354
+ else
355
+ "unknown platform"
356
+ end
357
+ end
358
+ end
359
+
360
+ def self.get_uname_from_system
361
+ (`uname -a 2>/dev/null` || '').strip
362
+ rescue Errno::ENOENT
363
+ "uname executable not found"
364
+ rescue Errno::ENOMEM # couldn't create subprocess
365
+ "uname lookup failed"
366
+ end
367
+
368
+ def self.get_uname_from_system_ver
369
+ (`ver` || '').strip
370
+ rescue Errno::ENOENT
371
+ "ver executable not found"
372
+ rescue Errno::ENOMEM # couldn't create subprocess
373
+ "uname lookup failed"
374
+ end
375
+
376
+ def initialize
377
+ @uname = self.class.get_uname
378
+ end
379
+
380
+ def user_agent
381
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
382
+
383
+ {
384
+ :bindings_version => Stripe::VERSION,
385
+ :lang => 'ruby',
386
+ :lang_version => lang_version,
387
+ :platform => RUBY_PLATFORM,
388
+ :engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
389
+ :publisher => 'stripe',
390
+ :uname => @uname,
391
+ :hostname => Socket.gethostname,
392
+ }
393
+ end
394
+ end
395
+ end
396
+ end