stripe 1.58.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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