Class: CloudPDF::Internal::Http::RawClient Private
- Inherits:
-
Object
- Object
- CloudPDF::Internal::Http::RawClient
- 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
-
#base_url ⇒ String
readonly
private
The base URL for requests.
Instance Method Summary collapse
-
#add_jitter(delay) ⇒ Float
private
Adds random jitter to a delay value.
-
#build_http_request(url:, method:, headers: {}, body: nil, auth_headers: {}) ⇒ HTTP::Request
private
The HTTP request.
-
#build_url(request) ⇒ URI::Generic
private
The URL.
-
#connect(url) ⇒ Net::HTTP
private
The HTTP connection.
-
#encode_query(query) ⇒ String?
private
The encoded query.
-
#initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, overridable_headers: [], auth_provider: nil) ⇒ RawClient
constructor
private
A new instance of RawClient.
- #inspect ⇒ String private
-
#parse_retry_after(value) ⇒ Float?
private
Parses the Retry-After header value.
-
#protected_header_keys ⇒ Array<Symbol, String>
private
The client-level header names that
additional_headersmust not replace: every default header except the API's global headers, which are overridable per request. -
#resolve_auth_headers ⇒ Hash
private
Resolves the auth headers to send with the next request.
-
#retry_delay(response, attempt) ⇒ Float
private
Calculates the delay before the next retry attempt using exponential backoff with jitter.
-
#send(request) ⇒ HTTP::Response
private
The HTTP response.
-
#should_retry?(response, attempt) ⇒ Boolean
private
Determines if a request should be retried based on the response status code.
-
#validate_https!(url) ⇒ Object
private
Raises if the URL uses http:// for a non-localhost host, which would send authentication credentials in plaintext.
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.
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_url ⇒ String (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.
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.
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.
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.
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.
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.
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 |
#inspect ⇒ 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.
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.
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_keys ⇒ Array<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.
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_headers ⇒ Hash
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.
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.
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.
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.
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.
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 |