Skip to content

fix: stop re-requesting bot avatars that GitHub has no record of - #111

Merged
austenstone merged 2 commits into
mainfrom
fix/avatar-negative-cache
Aug 20, 2026
Merged

fix: stop re-requesting bot avatars that GitHub has no record of#111
austenstone merged 2 commits into
mainfrom
fix/avatar-negative-cache

Conversation

@austenstone

Copy link
Copy Markdown
Owner

Found by reading the browser console while verifying #110 renders, not by any test or check.

Loading the sample data fires two identical requests for the same user ~200ms apart:

[ERROR] 404  https://api.github.com/users/lisa-huang%5Bbot%5D
[ERROR] 404  https://api.github.com/users/david-liu%5Bbot%5D
[ERROR] 404  https://api.github.com/users/david-liu%5Bbot%5D    <- again
[ERROR] 404  https://api.github.com/users/lisa-huang%5Bbot%5D   <- again

Why

resolveBotAvatar caches on success only:

if (url) {
  botAvatarCache.set(username, url);
  persistAvatarCache();
}

A 404 leaves the cache untouched. The in-flight pendingFetches map does collapse concurrent calls, but it's cleared in .finally(), so it does nothing for the next pass. preloadBotAvatars then filters on !botAvatarCache.has(u) and re-selects the exact same unresolvable bots every time it runs.

There was already a test for concurrent dedup, which is why this survived — it covered the case that worked and not the one that didn't.

Why it matters

These requests are unauthenticated: 60 per hour per IP. A real report with bots that no longer resolve (deleted app, renamed org, GHES users who don't exist on github.com) spends that budget on repeats and then every avatar fails, including the ones that would have resolved fine.

Fix

Track unresolved usernames in a module-level Set and check it in resolveBotAvatar and the preloadBotAvatars filter.

Deliberately not persisted. A localStorage entry would be permanent, so a bot created or renamed later could never resolve. In-memory means it stops the hammering within a session and a reload retries.

Verification

Four new tests. Confirmed load-bearing — reverting the guard fails exactly the two that should fail:

× does not re-request a bot the API had no avatar for
× does not re-request a bot whose lookup threw
Tests  2 failed | 15 passed (17)

Full suite 290 passed, lint and typecheck clean, build succeeds.

Note

Stacked on #110#109. The console also shows five Highcharts warning #33 entries about invalid config attributes; those are pre-existing and unrelated, not touched here.

resolveBotAvatar only cached successful lookups, so a username the API
returned 404 for was requested again on every dataset change. The
in-flight map deduplicates concurrent calls but is cleared on settle,
so nothing prevented the next pass from asking again.

These calls are unauthenticated, which means 60 per hour per IP. A
report containing bots that no longer resolve would spend that budget
on repeats and then fail to load any avatars at all, including the
ones that would have worked.

Track unresolved usernames in memory and skip them in both
resolveBotAvatar and the preloadBotAvatars filter. Deliberately not
persisted: a bot created or renamed later still resolves after a
reload, which a localStorage entry would prevent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cf40db0-fafb-4793-92c6-3fe2b92d70bd
@austenstone
austenstone changed the base branch from chore/dep-minors to main August 20, 2026 17:52
@austenstone
austenstone merged commit 5372583 into main Aug 20, 2026
3 checks passed
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