Class: CloudPDF::Internal::Http::RawClient Private

Inherits:
Object
  • Object
show all
Defined in:
lib/CloudPDF/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: {}, overridable_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.

  • overridable_headers (Array<String>) (defaults to: [])

    The names of the client-level headers a request may replace via additional_headers. Holds the API's global headers; SDK metadata and auth headers are absent from it and so stay protected.

  • 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.



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 30

def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, overridable_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": "cloudpdf",
    "X-Fern-SDK-Version": "0.0.1"
  }.merge(headers)
  @overridable_headers = overridable_headers.to_set { |name| name.to_s.downcase }
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/CloudPDF/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.



139
140
141
142
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 139

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.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 201

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.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 148

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.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 238

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.



232
233
234
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 232

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)


259
260
261
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 259

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.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 121

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

#protected_header_keysArray<Symbol, 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.

The client-level header names that additional_headers must not replace: every default header except the API's global headers, which are overridable per request.

Returns:

  • (Array<Symbol, String>)

    The protected header names.



83
84
85
86
87
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 83

def protected_header_keys
  return @default_headers.keys if @overridable_headers.empty?

  @default_headers.keys.reject { |name| @overridable_headers.include?(name.to_s.downcase) }
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.



187
188
189
190
191
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 187

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.



105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 105

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.



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
75
76
77
78
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 45

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: protected_header_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.



93
94
95
96
97
98
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 93

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)


172
173
174
175
176
177
178
179
# File 'lib/CloudPDF/internal/http/raw_client.rb', line 172

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