Skip to content

fix: resolve font URLs dynamically via GitHub API - #60

Merged
Oaklight merged 2 commits into
masterfrom
worktree-fix+font-url-resolution
Sep 24, 2026
Merged

Oaklight merged 2 commits into
masterfrom
worktree-fix+font-url-resolution

Conversation

@Oaklight

Copy link
Copy Markdown
Owner

Summary

  • Replace hardcoded jsDelivr font URLs with dynamic GitHub Contents API resolution
  • Fonts defined by (owner, repo, path, glob) — actual filename discovered at runtime via API
  • Switch from jsDelivr gh CDN (returning 403 for large files) to raw.githubusercontent.com
  • Resolved URLs cached in-memory; falls back to hardcoded raw GitHub URLs if API unreachable

Fixes:

  • noto-color-emoji 404 (moved from googlefonts/noto-emoji to google/fonts, renamed to NotoColorEmoji-Regular.ttf)
  • noto-sans-arabic/thai/devanagari filename changes (gained [wdth,wght] axis)
  • jsDelivr 403 on all large font files (variable fonts are multi-MB)
  • lxgw-wenkai jsDelivr 403

Test plan

  • ruff check and ruff format pass
  • ty check passes
  • All 9 fonts resolve correctly via GitHub API (verified filenames match current repo state)
  • Emoji font downloads successfully (25MB, NotoColorEmoji-Regular.ttf)
  • Fallback URLs are set for API failures

Replace hardcoded jsDelivr font URLs with GitHub Contents API
resolution. Fonts are defined by (owner, repo, path, glob) and
the actual filename is discovered at runtime, making downloads
resilient to upstream file renames and directory reorganizations.

Fixes noto-color-emoji 404 (file moved and renamed), arabic/thai/
devanagari filename changes ([wdth,wght] axis added), and jsDelivr
403 errors on large font files by switching to raw.githubusercontent.

Resolved URLs are cached in-memory for the process lifetime. Falls
back to hardcoded raw GitHub URLs if the API is unreachable.

@milo-oaklight milo-oaklight Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid fix — dynamic resolution with a fallback layer is the right approach when upstream repos keep renaming files. The _GitHubFontSpec dataclass is clean and the in-memory cache avoids repeated API calls within a process. Approved.

Three notes (first one is a concrete suggestion, others are non-blocking):

  1. Use entry["download_url"] from the API response instead of constructing the URL manually. The current code builds {_RAW_GH}/{owner}/{repo}/main/{path}/{filename} — this hardcodes the main branch. The Contents API already returns a download_url field with the correct raw URL, including the right branch. Using it directly is simpler and avoids the assumption. Same applies to the fallback URL construction in _gf() if you keep that as-is.

  2. GitHub API rate limit: 60 req/hour unauthenticated. With 9 fonts, each cold start burns 9 calls (one per directory path). Frequent restarts, multiple workers, or dev/test loops could hit the limit. Worth considering either: (a) batching — fonts sharing the same (owner, repo, path) could share a single API call, or (b) supporting an optional GITHUB_TOKEN env var for authenticated requests (5000/hr).

  3. Fallback results get permanently cached. When the API is temporarily unreachable, the stale fallback URL is cached in _resolved_url_cache for the entire process lifetime. If that fallback URL is itself broken (which is the whole reason for dynamic resolution), the font stays broken until restart. Consider either not caching fallback results or using a short TTL so the API is retried on the next request.

@clementine-oaklight clementine-oaklight Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change — structured specs + runtime resolution is much more resilient than hardcoded jsDelivr URLs that break on every upstream rename.

One substantive suggestion and two minor notes:

Use entry["download_url"] from the API response instead of constructing the URL manually

The current code builds the download URL by hand:

url = f"{_RAW_GH}/{spec.owner}/{spec.repo}/main/{spec.path}/{filename}"

Two issues with this:

  1. Hardcoded main branch — if any upstream repo uses master or another default, the constructed URL 404s. The GitHub Contents API response already includes download_url with the correct branch.
  2. URL encoding — filename comes from entry["name"] (raw, e.g. NotoSansSC[wght].ttf) but square brackets need percent-encoding in URLs. The fallback URLs already use %5B/%5D, so the dynamic path would produce different URLs than the fallback for the same file. entry["download_url"] is already properly encoded.

Using entry["download_url"] directly is simpler, correct, and avoids both problems.

Minor (non-blocking):

  • Fallback results are cached for process lifetime — if the API is transiently down, the fallback URL gets cached and sticks until restart. Not a problem in practice (the fallback URLs are reasonable), just means a restart is needed to pick up renames after a transient API failure.
  • _download_font iterates all _KNOWN_FONT_KEYS resolving each via API to match a filename — on cold start with a cache miss this resolves up to 5 fonts. Fine at this scale, but a lazy reverse-map would be cleaner if the list grows.

@elena-oaklight elena-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved ✅ — the move from hardcoded jsDelivr URLs to dynamic resolution is the right fix, and the glob + fallback design is solid.

Three suggestions worth considering:

1. Use entry["download_url"] from the API response

The resolved URL is currently constructed manually:

url = f"{_RAW_GH}/{spec.owner}/{spec.repo}/main/{spec.path}/{filename}"

This hardcodes the main branch and requires manual URL encoding. The GitHub Contents API response already includes a download_url field with the correct raw URL, proper encoding, and correct default branch:

url = entry["download_url"]

Simpler and more robust (e.g. if lxgw/LxgwWenKai ever changes its default branch).

2. Fallback caching prevents retry

_resolved_url_cache caches fallback results too. If the GitHub API is unreachable at startup (transient network blip), the fallback URL gets cached permanently — subsequent calls never retry the API. Consider either:

  • Not caching fallback results (so the next call retries the API), or
  • Adding a TTL / distinguishing cached-from-api vs cached-from-fallback

3. _download_font is O(n) with potential blocking

The old _KNOWN_FONTS dict gave O(1) filename→URL lookup. The new code iterates all _KNOWN_FONT_KEYS, calling _resolve_github_font_url for each to check if resolved_name == filename. On a cold cache in a request handler, this could make up to 5 synchronous HTTP calls (10s timeout each), blocking the event loop.

Two options:

  • Build a reverse cache (filename → url) after resolution, or
  • Resolve all known fonts eagerly at startup (they're needed anyway for install)

- Use entry["download_url"] from GitHub API instead of constructing
  raw URLs manually (avoids hardcoding branch name and URL encoding)
- Batch API calls via _fetch_github_dir() cached per directory path,
  so fonts sharing the same repo directory use a single API call
- Support GITHUB_TOKEN env var for authenticated API requests (5000/hr
  vs 60/hr unauthenticated)
- Don't cache fallback results so transient API failures are retried
  on subsequent calls
@Oaklight

Copy link
Copy Markdown
Owner Author

Thanks for the reviews! All notes addressed in dabc2f4:

Shared feedback (all 3 reviewers):

  • Now uses entry["download_url"] from the GitHub API response instead of constructing URLs manually — avoids hardcoded main branch and URL encoding issues.

@milo-oaklight:

  1. download_url — done
  2. API rate limit — added GITHUB_TOKEN env var support (5000/hr authenticated). Also added _fetch_github_dir() with per-directory caching so fonts sharing the same (owner, repo, path) reuse a single API call (all 8 google/fonts fonts share only 8 unique directories, not 8 separate calls, and the cache avoids re-fetching on subsequent _resolve_github_font_url calls for the same dir).
  3. Fallback caching — fallback results are no longer cached. API is retried on subsequent calls.

@clementine-oaklight:

  1. download_url — done
  2. Fallback caching — not cached, retried on next call
  3. _download_font O(n) — acknowledged, acceptable at current scale (5 keys)

@elena-oaklight:

  1. download_url — done
  2. Fallback caching — not cached
  3. _download_font O(n) — same as above, will revisit if list grows

@Oaklight
Oaklight merged commit c6a2b8f into master Sep 24, 2026
2 checks passed
@Oaklight
Oaklight deleted the worktree-fix+font-url-resolution branch September 24, 2026 01:22
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