Skip to content

fix: add ETag caching to UpdateChecker to avoid GitHub rate limits - #115

Merged
jatinkrmalik merged 1 commit into
mainfrom
fix/update-checker-etag-caching
Apr 16, 2026
Merged

fix: add ETag caching to UpdateChecker to avoid GitHub rate limits#115
jatinkrmalik merged 1 commit into
mainfrom
fix/update-checker-etag-caching

Conversation

@jatinkrmalik

Copy link
Copy Markdown
Member

Problem

Every launch logs:

[ERROR] [UpdateChecker] Update check failed: Update check failed (HTTP 403)

Root Causes

1. No conditional request caching

Every update check consumed one of GitHub's 60 unauthenticated requests/hour per IP. On shared networks (NAT, VPN, office, CI runners) the pool is trivially exhausted — especially since the nightly build CI also hits the same API.

2. Bare User-Agent string

The request sent User-Agent: VocaMac instead of User-Agent: VocaMac/0.5.0. GitHub recommends including the app version so they can identify and contact clients that misbehave.

Fix

ETag conditional caching:

  • On 200 OK: persist the ETag response header and raw JSON body to UserDefaults
  • On subsequent requests: send If-None-Match: <cached-etag>
  • On 304 Not Modified: decode and return the cached JSON — 304s don't count against the rate limit and are served from GitHub's CDN edge
  • On 304 with missing cache (UserDefaults cleared): evict the ETag so the next check gets a fresh 200 response

Versioned User-Agent: VocaMac/<bundle-version> resolved at runtime.

Result

Scenario Before After
First check 1 request consumed 1 request consumed
Subsequent checks (same release) 1 request consumed 0 requests consumed (304 served from CDN)
New release published 1 request consumed 1 request consumed
Shared IP / rate limited HTTP 403 Served from local cache

Testing

All 163 tests pass.

Two problems caused the HTTP 403 Update check failed error:

1. No conditional request caching: every update check consumed one of
   GitHub's 60 unauthenticated requests/hour per IP. On shared networks
   (NAT, VPN, office) the pool is easily exhausted. Fixed by persisting
   the ETag from each 200 response and sending it as If-None-Match on
   subsequent requests. GitHub returns 304 Not Modified when the release
   hasn't changed — 304s are rate-limit free and served from CDN edge.

2. Bare User-Agent: 'VocaMac' instead of 'VocaMac/<version>'. GitHub
   recommends including the app version to help them identify clients.
   Fixed to 'VocaMac/0.5.0' (resolved at runtime from bundle).

Caching strategy:
- On 200 OK: persist ETag and raw JSON body to UserDefaults
- On 304 Not Modified: decode and return the cached JSON body
- On 304 with missing cache (UserDefaults cleared): evict ETag and
  surface an error so the next check gets a fresh 200 response
- ETag and cache body are keyed separately so they can be cleared
  independently (e.g. ETag eviction on cache miss)
@jatinkrmalik
jatinkrmalik merged commit 1a9ace4 into main Apr 16, 2026
3 checks passed
@jatinkrmalik
jatinkrmalik deleted the fix/update-checker-etag-caching branch August 22, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant