Module: ChargeBee::NativeRequest

Defined in:
lib/chargebee/native_request.rb

Class Method Summary collapse

Class Method Details

.backoff_delay(delay_ms, attempt) ⇒ Object



125
126
127
128
# File 'lib/chargebee/native_request.rb', line 125

def self.backoff_delay(delay_ms, attempt)
  jitter = rand(100)
  (delay_ms * (2 ** (attempt - 1)) + jitter) / 1000.0
end

.beautify_headers(headers) ⇒ Object



200
201
202
203
204
205
# File 'lib/chargebee/native_request.rb', line 200

def self.beautify_headers(headers)
  headers.each_with_object({}) do |(key, value), out|
    key_sym = key.tr('-', '_').downcase.to_sym
    out[key_sym] = key.downcase == 'set-cookie' ? value : value.join(', ')
  end
end

.build_http_request(method, uri, custom_headers, is_json) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/chargebee/native_request.rb', line 80

def self.build_http_request(method, uri, custom_headers, is_json)
  request_class = {
                    get: Net::HTTP::Get,
                    post: Net::HTTP::Post,
                    put: Net::HTTP::Put,
                    delete: Net::HTTP::Delete
                  }[method.to_s.downcase.to_sym] || raise(Error.new("Unsupported HTTP method: #{method}"))

  content_type = is_json ? "application/json;charset=UTF-8" : "application/x-www-form-urlencoded"

  headers = {
    "User-Agent" => ChargeBee.user_agent,
    "Accept" => "application/json",
    "Lang-Version" => RUBY_VERSION,
    "OS-Version" => RUBY_PLATFORM,
    "Content-Type" => content_type
  }.merge(custom_headers)

  request_class.new(uri, headers)
end

.build_payload(method, params, is_json) ⇒ Object



72
73
74
75
76
77
78
# File 'lib/chargebee/native_request.rb', line 72

def self.build_payload(method, params, is_json)
  if %i[get head delete].include?(method.to_s.downcase.to_sym)
    nil
  else
    is_json ? params.to_json : URI.encode_www_form(params || {})
  end
end

.build_uri(method, url, params) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/chargebee/native_request.rb', line 64

def self.build_uri(method, url, params)
  uri = URI(url)
  if %i[get head delete].include?(method.to_s.downcase.to_sym) && params
    uri.query = URI.encode_www_form(params)
  end
  uri
end

.configure_http_client(uri, env) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/chargebee/native_request.rb', line 101

def self.configure_http_client(uri, env)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.open_timeout = env.connect_timeout
  http.read_timeout = env.read_timeout
  if ChargeBee.verify_ca_certs?
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    http.ca_file = ChargeBee.ca_cert_path
  else
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end
  http
end

.decompress_gzip(rbody) ⇒ Object



151
152
153
154
155
156
157
158
# File 'lib/chargebee/native_request.rb', line 151

def self.decompress_gzip(rbody)
  gz = Zlib::GzipReader.new(StringIO.new(rbody))
  begin
    gz.read
  ensure
    gz.close
  end
end

.extract_retry_delay(response, delay_ms, attempt) ⇒ Object



115
116
117
118
119
120
121
122
123
# File 'lib/chargebee/native_request.rb', line 115

def self.extract_retry_delay(response, delay_ms, attempt)
  retry_after = response['Retry-After']
  retry_after_delay = Integer(retry_after) rescue 0
  if retry_after_delay > 0
    retry_after_delay
  else
    backoff_delay(delay_ms, attempt)
  end
end

.handle_for_error(rcode, rbody) ⇒ ChargeBee::Error

Handle errors returned by the ChargeBee API.

Parameters:

  • rcode (Integer)

    HTTP status code.

  • rbody (String)

    HTTP response body.

Returns:



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/chargebee/native_request.rb', line 176

def self.handle_for_error(rcode, rbody)
  return Error.new("No response returned by ChargeBee API. HTTP status code: #{rcode}") if rcode == 204
  return ForbiddenError.new("Access forbidden. You do not have permission to access this resource.") if rcode == 403
  begin
    error_obj = JSON.parse(rbody)
    error_obj = Util.symbolize_keys(error_obj)
  rescue Exception => e
    return Error.new("Error response not in JSON format. The http status code is #{rcode} \n #{rbody.inspect}", e)
  end

  case error_obj[:type]
  when "payment"
    PaymentError.new(rcode, error_obj)
  when "operation_failed"
    OperationFailedError.new(rcode, error_obj)
  when "invalid_request"
    InvalidRequestError.new(rcode, error_obj)
  when "ubb_batch_ingestion_invalid_request"
    UbbBatchIngestionInvalidRequestError.new(rcode, error_obj)
  else
    APIError.new(rcode, error_obj)
  end
end

.handle_json_error(rbody, e) ⇒ Object



160
161
162
163
164
165
166
167
168
# File 'lib/chargebee/native_request.rb', line 160

def self.handle_json_error(rbody, e)
  if rbody.include?("503")
    raise Error.new("Sorry, the server is currently unable to handle the request due to a temporary overload or scheduled maintenance. Please retry after sometime. \n type: internal_temporary_error, \n http_status_code: 503, \n error_code: internal_temporary_error,\n content: #{rbody.inspect}", e)
  elsif rbody.include?("504")
    raise Error.new("The server did not receive a timely response from an upstream server, request aborted. If this problem persists, contact us at support@chargebee.com. \n type: gateway_timeout, \n http_status_code: 504, \n error_code: gateway_timeout,\n content:  #{rbody.inspect}", e)
  else
    raise Error.new("Sorry, something went wrong when trying to process the request. If this problem persists, contact us at support@chargebee.com. \n type: internal_error, \n http_status_code: 500, \n error_code: internal_error,\n content:  #{rbody.inspect}", e)
  end
end

.handle_response(response, headers) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/chargebee/native_request.rb', line 130

def self.handle_response(response, headers)
  rcode = response.code.to_i
  rbody = response.body
  rheaders = beautify_headers(response.to_hash)

  if rheaders[:content_encoding] == 'gzip' && rbody && !rbody.empty?
    rbody = decompress_gzip(rbody)
  end

  if rcode >= 200 && rcode < 300
    begin
      resp = rcode == 204 ? rbody : JSON.parse(rbody)
    rescue JSON::ParserError => e
      raise handle_json_error(rbody, e)
    end
    return Util.symbolize_keys(resp), rheaders, rcode
  else
    raise handle_for_error(rcode, rbody)
  end
end

.request(method, url, env, params = nil, headers = {}, subdomain = nil, isJsonRequest = false, options = {}) ⇒ Object

Raises:



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/chargebee/native_request.rb', line 10

def self.request(method, url, env, params = nil, headers = {}, subdomain = nil, isJsonRequest = false, options={})
  raise Error.new('No environment configured.') unless env
  api_key = env.api_key

  uri = build_uri(method, env.api_url(url, subdomain), params)

  payload = build_payload(method, params, isJsonRequest)
  request = build_http_request(method, uri, headers, isJsonRequest)
  request.body = payload if payload
  request.basic_auth(api_key, nil)
  http = configure_http_client(uri, env)

  retry_config = env.retry_config || {}
  retry_enabled = retry_config[:enabled] == true
  max_retries = retry_enabled ? (retry_config[:max_retries] || 3) : 0
  delay_ms = retry_enabled ? (retry_config[:delay_ms] || 500) : 0
  retry_on = retry_config[:retry_on] || [500, 502, 503, 504]
  enable_debug = env.enable_debug_logs

  attempts = 0
  response = nil

  while attempts <= max_retries
    begin
      attempts += 1
      if attempts > 0
        request["X-CB-Retry-Attempt"] = attempts.to_s
        if options[:isIdempotent] && request["chargebee-idempotency-key"].nil?
          request["chargebee-idempotency-key"] = SecureRandom.uuid
        end
      end
      response = http.request(request)

      break unless retry_enabled && retry_on.include?(response.code.to_i) && attempts <= max_retries

      retry_delay = extract_retry_delay(response, delay_ms, attempts)
      puts "[ChargeBee] Retrying request (status #{response.code}) attempt #{attempts} after #{retry_delay.round(2)}s" if enable_debug
      sleep(retry_delay)
    rescue => e
      puts "[ChargeBee] HTTP request failed on attempt #{attempts}: #{e}" if enable_debug

      if retry_enabled && attempts <= max_retries
        retry_delay = backoff_delay(delay_ms, attempts)
        sleep(retry_delay)
        next
      else
        raise IOError.new("IO Exception when trying to connect to ChargeBee with URL #{uri} . Reason: #{e}", e)
      end
    end
  end

  handle_response(response, headers)
end