Class: Nordlet::Internal::Http::RawClient Private

Inherits:
Object
  • Object
show all
Defined in:
lib/nordlet/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.

Defined Under Namespace

Modules: DecodeContent

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: {}, auth_provider: nil) ⇒ 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.

  • auth_provider (Object, nil) (defaults to: nil)

    An optional auth provider responding to auth_headers. When present its headers are resolved on every request so token-based schemes (e.g. OAuth) can refresh an expired token mid-session.



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/nordlet/internal/http/raw_client.rb', line 27

def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, auth_provider: nil)
  @base_url = base_url
  @max_retries = max_retries
  @timeout = timeout
  @auth_provider = auth_provider
  @default_headers = {
    "X-Fern-Language": "Ruby",
    "X-Fern-SDK-Name": "nordlet",
    "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/nordlet/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.



126
127
128
129
# File 'lib/nordlet/internal/http/raw_client.rb', line 126

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, auth_headers: {}) ⇒ 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.

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

    The auth headers resolved for this request. These take precedence over the static default headers but not over per-request headers, mirroring the previous baked-header precedence.

Returns:

  • (HTTP::Request)

    The HTTP request.



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

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

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

  # Net::HTTP disables its transparent gzip/deflate decoding as soon as an
  # Accept-Encoding header is set explicitly on the request. Re-enable it so
  # that compressed response bodies are still inflated.
  request.extend(DecodeContent) if request_headers.keys.any? { |name| name.to_s.casecmp("accept-encoding").zero? }

  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.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/nordlet/internal/http/raw_client.rb', line 135

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.



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/nordlet/internal/http/raw_client.rb', line 225

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.



219
220
221
# File 'lib/nordlet/internal/http/raw_client.rb', line 219

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)


246
247
248
# File 'lib/nordlet/internal/http/raw_client.rb', line 246

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.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/nordlet/internal/http/raw_client.rb', line 108

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

#resolve_auth_headersHash

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.

Resolves the auth headers to send with the next request. Delegates to the configured auth provider (if any) on every call so that token-based providers (e.g. OAuth client-credentials) can refresh an expired token before the request is sent. Returns an empty hash when no provider is set, which keeps the api-key / basic / bearer / no-auth paths unchanged.

Returns:

  • (Hash)

    The auth headers for the current request.



174
175
176
177
178
# File 'lib/nordlet/internal/http/raw_client.rb', line 174

def resolve_auth_headers
  return {} if @auth_provider.nil?

  @auth_provider.auth_headers
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.



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/nordlet/internal/http/raw_client.rb', line 92

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.



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
67
68
69
70
71
72
73
74
# File 'lib/nordlet/internal/http/raw_client.rb', line 41

def send(request)
  url = build_url(request)
  # Resolve auth headers once per request (not per retry) so token-based
  # providers refresh at most once here; static providers are cheap.
  auth_headers = resolve_auth_headers
  attempt = 0
  response = nil

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

    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.



80
81
82
83
84
85
# File 'lib/nordlet/internal/http/raw_client.rb', line 80

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)


159
160
161
162
163
164
165
166
# File 'lib/nordlet/internal/http/raw_client.rb', line 159

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