Class: Payabli::Internal::Http::RawClient Private

Inherits:
Object
  • Object
show all
Defined in:
lib/payabli/internal/http/raw_client.rb

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Constant Summary collapse

RETRYABLE_STATUSES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Default HTTP status codes that trigger a retry

[408, 429, 500, 502, 503, 504, 521, 522, 524].freeze
INITIAL_RETRY_DELAY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Initial delay between retries in seconds

0.5
MAX_RETRY_DELAY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Maximum delay between retries in seconds

60.0
JITTER_FACTOR =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Jitter factor for randomizing retry delays (20%)

0.2
LOCALHOST_HOSTS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

%w[localhost 127.0.0.1 [::1]].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}) ⇒ RawClient

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of RawClient.

Parameters:

  • base_url (String)

    The base url for the request.

  • max_retries (Integer) (defaults to: 2)

    The number of times to retry a failed request, defaults to 2.

  • timeout (Float) (defaults to: 60.0)

    The timeout for the request, defaults to 60.0 seconds.

  • headers (Hash) (defaults to: {})

    The headers for the request.



24
25
26
27
28
29
30
31
32
33
# File 'lib/payabli/internal/http/raw_client.rb', line 24

def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {})
  @base_url = base_url
  @max_retries = max_retries
  @timeout = timeout
  @default_headers = {
    "X-Fern-Language": "Ruby",
    "X-Fern-SDK-Name": "payabli",
    "X-Fern-SDK-Version": "0.0.1"
  }.merge(headers)
end

Instance Attribute Details

#base_urlString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The base URL for requests.

Returns:

  • (String)

    The base URL for requests



18
19
20
# File 'lib/payabli/internal/http/raw_client.rb', line 18

def base_url
  @base_url
end

Instance Method Details

#add_jitter(delay) ⇒ Float

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Adds random jitter to a delay value.

Parameters:

  • delay (Float)

    The base delay in seconds.

Returns:

  • (Float)

    The delay with jitter applied.



118
119
120
121
# File 'lib/payabli/internal/http/raw_client.rb', line 118

def add_jitter(delay)
  jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2
  [delay + jitter, 0].max
end

#build_http_request(url:, method:, headers: {}, body: nil) ⇒ HTTP::Request

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The HTTP request.

Parameters:

  • url (URI::Generic)

    The url to the resource.

  • method (String)

    The HTTP method to use.

  • headers (Hash) (defaults to: {})

    The headers for the request.

  • body (String, nil) (defaults to: nil)

    The body for the request.

Returns:

  • (HTTP::Request)

    The HTTP request.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/payabli/internal/http/raw_client.rb', line 164

def build_http_request(url:, method:, headers: {}, body: nil)
  request = Net::HTTPGenericRequest.new(
    method,
    !body.nil?,
    method != "HEAD",
    url
  )

  request_headers = @default_headers.merge(headers)
  request_headers.each { |name, value| request[name] = value }
  request.body = body if body

  request
end

#build_url(request) ⇒ URI::Generic

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The URL.

Parameters:

Returns:

  • (URI::Generic)

    The URL.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/payabli/internal/http/raw_client.rb', line 127

def build_url(request)
  encoded_query = request.encode_query

  # If the path is already an absolute URL, use it directly
  if request.path.start_with?("http://", "https://")
    url = request.path
    url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
    parsed = URI.parse(url)
    validate_https!(parsed)
    return parsed
  end

  path = request.path.start_with?("/") ? request.path[1..] : request.path
  base = request.base_url || @base_url
  url = "#{base.chomp("/")}/#{path}"
  url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
  parsed = URI.parse(url)
  validate_https!(parsed)
  parsed
end

#connect(url) ⇒ Net::HTTP

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The HTTP connection.

Parameters:

  • url (URI::Generic)

    The url to connect to.

Returns:

  • (Net::HTTP)

    The HTTP connection.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/payabli/internal/http/raw_client.rb', line 187

def connect(url)
  is_https = (url.scheme == "https")

  port = if url.port
           url.port
         elsif is_https
           Net::HTTP.https_default_port
         else
           Net::HTTP.http_default_port
         end

  http = Net::HTTP.new(url.host, port)
  http.use_ssl = is_https
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
  # NOTE: We handle retries at the application level with HTTP status code awareness,
  # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
  http.max_retries = 0
  http
end

#encode_query(query) ⇒ String?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The encoded query.

Parameters:

  • query (Hash)

    The query for the request.

Returns:

  • (String, nil)

    The encoded query.



181
182
183
# File 'lib/payabli/internal/http/raw_client.rb', line 181

def encode_query(query)
  query.to_h.empty? ? nil : URI.encode_www_form(query)
end

#inspectString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (String)


208
209
210
# File 'lib/payabli/internal/http/raw_client.rb', line 208

def inspect
  "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
end

#parse_retry_after(value) ⇒ Float?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parses the Retry-After header value.

Parameters:

  • value (String)

    The Retry-After header value (seconds or HTTP date).

Returns:

  • (Float, nil)

    The delay in seconds, or nil if parsing fails.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/payabli/internal/http/raw_client.rb', line 100

def parse_retry_after(value)
  # Try parsing as integer (seconds)
  seconds = Integer(value, exception: false)
  return seconds.to_f if seconds

  # Try parsing as HTTP date
  begin
    retry_time = Time.httpdate(value)
    delay = retry_time - Time.now
    delay.positive? ? delay : nil
  rescue ArgumentError
    nil
  end
end

#retry_delay(response, attempt) ⇒ Float

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Calculates the delay before the next retry attempt using exponential backoff with jitter. Respects Retry-After header if present.

Parameters:

  • response (Net::HTTPResponse)

    The HTTP response.

  • attempt (Integer)

    The current retry attempt (0-indexed).

Returns:

  • (Float)

    The delay in seconds before the next retry.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/payabli/internal/http/raw_client.rb', line 84

def retry_delay(response, attempt)
  # Check for Retry-After header (can be seconds or HTTP date)
  retry_after = response["Retry-After"]
  if retry_after
    delay = parse_retry_after(retry_after)
    return [delay, MAX_RETRY_DELAY].min if delay&.positive?
  end

  # Exponential backoff with jitter: base_delay * 2^attempt
  base_delay = INITIAL_RETRY_DELAY * (2**attempt)
  add_jitter([base_delay, MAX_RETRY_DELAY].min)
end

#send(request) ⇒ HTTP::Response

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The HTTP response.

Parameters:

Returns:

  • (HTTP::Response)

    The HTTP response.



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
63
64
65
66
# File 'lib/payabli/internal/http/raw_client.rb', line 37

def send(request)
  url = build_url(request)
  attempt = 0
  response = nil

  loop do
    http_request = build_http_request(
      url:,
      method: request.method,
      headers: request.encode_headers(protected_keys: @default_headers.keys),
      body: request.encode_body
    )

    conn = connect(url)
    conn.open_timeout = @timeout
    conn.read_timeout = @timeout
    conn.write_timeout = @timeout
    conn.continue_timeout = @timeout

    response = conn.request(http_request)

    break unless should_retry?(response, attempt)

    delay = retry_delay(response, attempt)
    sleep(delay)
    attempt += 1
  end

  response
end

#should_retry?(response, attempt) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Determines if a request should be retried based on the response status code.

Parameters:

  • response (Net::HTTPResponse)

    The HTTP response.

  • attempt (Integer)

    The current retry attempt (0-indexed).

Returns:

  • (Boolean)

    Whether the request should be retried.



72
73
74
75
76
77
# File 'lib/payabli/internal/http/raw_client.rb', line 72

def should_retry?(response, attempt)
  return false if attempt >= @max_retries

  status = response.code.to_i
  RETRYABLE_STATUSES.include?(status)
end

#validate_https!(url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Raises if the URL uses http:// for a non-localhost host, which would send authentication credentials in plaintext.

Parameters:

  • url (URI::Generic)

    The parsed URL.

Raises:

  • (ArgumentError)


151
152
153
154
155
156
157
# File 'lib/payabli/internal/http/raw_client.rb', line 151

def validate_https!(url)
  return if url.scheme != "http"
  return if LOCALHOST_HOSTS.include?(url.host)

  raise ArgumentError, "Refusing to send request to non-HTTPS URL: #{url}. " \
                       "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL."
end