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 (auto-detected, 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- KNOWN_ERROR_CODES =
API error code → human-readable message table (shared across all callers) Server error codes that have a localized message under the "platform.error.
" i18n key (see locales/en.rb & zh.rb). %w[ invalid_proof invalid_signature nonce_replayed timestamp_expired license_revoked license_expired device_limit_reached device_revoked invalid_license device_not_found contributor_required missing_device_token invalid_device_token device_token_revoked device_token_expired owner_user_not_found ].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 ⇒ PlatformHttpClient
constructor
Auto-detects the target host(s): - When CLACKY_LICENSE_SERVER is set → single host (dev override, no failover) - Otherwise → [PRIMARY_HOST, FALLBACK_HOST].
-
#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 ⇒ PlatformHttpClient
Auto-detects the target host(s):
- When CLACKY_LICENSE_SERVER is set → single host (dev override, no failover)
- Otherwise → [PRIMARY_HOST, FALLBACK_HOST]
57 58 59 60 61 62 63 |
# File 'lib/clacky/platform_http_client.rb', line 57 def initialize if (override = ENV["CLACKY_LICENSE_SERVER"]) && !override.empty? @hosts = [override] else @hosts = [PRIMARY_HOST, FALLBACK_HOST] end end |
Instance Method Details
#delete(path, headers: {}) ⇒ Object
Send a DELETE request (no body).
91 92 93 |
# File 'lib/clacky/platform_http_client.rb', line 91 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.
140 141 142 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 |
# File 'lib/clacky/platform_http_client.rb', line 140 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.
81 82 83 |
# File 'lib/clacky/platform_http_client.rb', line 81 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.
109 110 111 112 113 |
# File 'lib/clacky/platform_http_client.rb', line 109 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.
102 103 104 105 106 |
# File 'lib/clacky/platform_http_client.rb', line 102 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.
86 87 88 |
# File 'lib/clacky/platform_http_client.rb', line 86 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.
71 72 73 |
# File 'lib/clacky/platform_http_client.rb', line 71 def post(path, payload, headers: {}) request_with_failover(:post, path, payload, headers) end |