Class: Clacky::PlatformHttpClient
- Inherits:
-
Object
- Object
- Clacky::PlatformHttpClient
- Defined in:
- lib/clacky/platform_http_client.rb
Overview
PlatformHttpClient provides a resilient HTTP client for all calls to the OpenClacky platform API (www.openclacky.com and its fallback domain).
Features:
- Automatic retry with exponential back-off on transient failures
- Transparent domain failover: if the primary domain times out or returns a
5xx error, the request is automatically retried against the fallback domain
- Unified large-file download entry point (#download_file) that reuses the
same primary → fallback failover policy as API calls
- Override via CLACKY_LICENSE_SERVER env var (used in development)
Usage:
client = Clacky::PlatformHttpClient.new
result = client.post("/api/v1/licenses/activate", payload)
# result => { success: true, data: {...} }
# or { success: false, error: "...", data: {} }
Defined Under Namespace
Classes: RetryableNetworkError
Constant Summary collapse
- PRIMARY_HOST =
Primary CDN-accelerated endpoint
"https://www.openclacky.com"- FALLBACK_HOST =
Direct fallback — bypasses EdgeOne, used when the primary times out
"https://openclacky.up.railway.app"- ATTEMPTS_PER_HOST =
Number of attempts per domain (1 = no retry within the same domain)
2- INITIAL_BACKOFF =
Initial back-off between retries within the same domain (seconds)
0.5- OPEN_TIMEOUT =
Connection / read timeouts (seconds) for API calls
8- READ_TIMEOUT =
15- DOWNLOAD_READ_TIMEOUT =
Read timeout for streaming large file downloads (seconds)
120- DOWNLOAD_MAX_REDIRECTS =
Max HTTP redirects followed by #download_file per host attempt
10- API_ERROR_MESSAGES =
API error code → human-readable message table (shared across all callers)
{ "invalid_proof" => "Invalid license key — please check and try again.", "invalid_signature" => "Invalid request signature.", "nonce_replayed" => "Duplicate request detected. Please try again.", "timestamp_expired" => "System clock is out of sync. Please adjust your time settings.", "license_revoked" => "This license has been revoked. Please contact support.", "license_expired" => "This license has expired. Please renew to continue.", "device_limit_reached" => "Device limit reached for this license.", "device_revoked" => "This device has been revoked from the license.", "invalid_license" => "License key not found. Please verify the key.", "device_not_found" => "Device not registered. Please re-activate." }.freeze
Instance Method Summary collapse
-
#delete(path, headers: {}) ⇒ Object
Send a DELETE request (no body).
-
#download_file(url, dest, read_timeout: DOWNLOAD_READ_TIMEOUT) ⇒ Hash
Stream a remote URL to a local file path, with automatic primary → fallback host failover.
-
#get(path, headers: {}) ⇒ Hash
Send a GET request and return a normalised result hash.
-
#initialize(base_url: nil) ⇒ PlatformHttpClient
constructor
A new instance of PlatformHttpClient.
-
#multipart_patch(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) ⇒ Object
Send a multipart/form-data PATCH.
-
#multipart_post(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) ⇒ Hash
Send a multipart/form-data POST.
-
#patch(path, payload, headers: {}) ⇒ Object
Send a PATCH request.
-
#post(path, payload, headers: {}) ⇒ Hash
Send a POST request with a JSON body and return a normalised result hash.
Constructor Details
#initialize(base_url: nil) ⇒ PlatformHttpClient
Returns a new instance of PlatformHttpClient.
59 60 61 62 63 64 65 66 |
# File 'lib/clacky/platform_http_client.rb', line 59 def initialize(base_url: nil) if base_url # Developer / test override: single host, no failover @hosts = [base_url] else @hosts = [PRIMARY_HOST, FALLBACK_HOST] end end |
Instance Method Details
#delete(path, headers: {}) ⇒ Object
Send a DELETE request (no body).
94 95 96 |
# File 'lib/clacky/platform_http_client.rb', line 94 def delete(path, headers: {}) request_with_failover(:delete, path, nil, headers) end |
#download_file(url, dest, read_timeout: DOWNLOAD_READ_TIMEOUT) ⇒ Hash
Stream a remote URL to a local file path, with automatic primary → fallback host failover.
This is the unified entry point for all large-file downloads (brand skill ZIPs, platform-hosted assets, etc.). Callers should NOT build their own Net::HTTP loops — failover, retry, redirects, and timeouts are handled here.
Host failover policy:
- If +url+'s host matches PRIMARY_HOST and the request fails with a
retryable error (timeout, connection reset, SSL, 5xx), the URL is
rewritten to FALLBACK_HOST (same path/query) and retried.
- Both hosts serve the same Rails backend and share +secret_key_base+,
so ActiveStorage signed_ids resolve identically on either.
- Third-party hosts (e.g. S3 presigned URLs reached via redirect) are
fetched as-is without host rewriting.
Each host gets ATTEMPTS_PER_HOST attempts with exponential back-off. Up to DOWNLOAD_MAX_REDIRECTS redirects are followed per attempt.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/clacky/platform_http_client.rb', line 143 def download_file(url, dest, read_timeout: DOWNLOAD_READ_TIMEOUT) candidate_urls = [url] # Only auto-add a fallback candidate when the URL is on our primary host. # External hosts (S3, CDNs, user-provided URLs) are fetched as-is. if primary_host_url?(url) candidate_urls << swap_to_fallback_host(url) end last_error = nil FileUtils.mkdir_p(File.dirname(dest)) tmp_dest = "#{dest}.part" candidate_urls.each_with_index do |candidate, host_index| ATTEMPTS_PER_HOST.times do |attempt| begin bytes = stream_download(candidate, tmp_dest, read_timeout: read_timeout) File.rename(tmp_dest, dest) return { success: true, bytes: bytes, error: nil } rescue RetryableNetworkError => e last_error = e backoff = INITIAL_BACKOFF * (2**attempt) Clacky::Logger.debug( "[PlatformHTTP] DOWNLOAD #{candidate} attempt #{attempt + 1} failed: " \ "#{e.} — retrying in #{backoff}s" ) sleep(backoff) end end if host_index + 1 < candidate_urls.size Clacky::Logger.debug( "[PlatformHTTP] Primary host exhausted for download, switching to fallback: " \ "#{candidate_urls[host_index + 1]}" ) end end FileUtils.rm_f(tmp_dest) { success: false, bytes: 0, error: "Download failed: #{last_error&. || "unknown"}" } end |
#get(path, headers: {}) ⇒ Hash
Send a GET request and return a normalised result hash. Query string parameters should be appended to path by the caller.
84 85 86 |
# File 'lib/clacky/platform_http_client.rb', line 84 def get(path, headers: {}) request_with_failover(:get, path, nil, headers) end |
#multipart_patch(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) ⇒ Object
Send a multipart/form-data PATCH. Same contract as #multipart_post.
112 113 114 115 116 |
# File 'lib/clacky/platform_http_client.rb', line 112 def multipart_patch(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" } request_with_failover(:multipart_patch, path, body_bytes, headers, read_timeout_override: read_timeout) end |
#multipart_post(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) ⇒ Hash
Send a multipart/form-data POST.
105 106 107 108 109 |
# File 'lib/clacky/platform_http_client.rb', line 105 def multipart_post(path, body_bytes, boundary, read_timeout: READ_TIMEOUT) headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" } request_with_failover(:multipart_post, path, body_bytes, headers, read_timeout_override: read_timeout) end |
#patch(path, payload, headers: {}) ⇒ Object
Send a PATCH request. Same contract as #post.
89 90 91 |
# File 'lib/clacky/platform_http_client.rb', line 89 def patch(path, payload, headers: {}) request_with_failover(:patch, path, payload, headers) end |
#post(path, payload, headers: {}) ⇒ Hash
Send a POST request with a JSON body and return a normalised result hash.
74 75 76 |
# File 'lib/clacky/platform_http_client.rb', line 74 def post(path, payload, headers: {}) request_with_failover(:post, path, payload, headers) end |