Skip to content

Judge favicon expiry on the icon the watch actually has, not whatever glob lists first - #4434

Open
dgtlmoon wants to merge 1 commit into
masterfrom
fix/favicon-expiry-picks-stale-file
Open

dgtlmoon wants to merge 1 commit into
masterfrom
fix/favicon-expiry-picks-stale-file

Conversation

@dgtlmoon

Copy link
Copy Markdown
Owner

The bug

bump_favicon() names the saved file after the icon's type (favicon.png, favicon.ico, …) and did not remove a previous save under a different extension. So a watch whose icon changed type holds more than one favicon.*.

favicon_is_expired() then picked next(iter(glob.glob("favicon.*"))). glob order is os.scandir order — stable, but decided by filename hash — so for some extension pairs the stale file wins every single time, its age always exceeds the 24h FAVICON_RESAVE_THRESHOLD_SECONDS, and the favicon is refetched on every check, forever, silently.

Measured across realistic pairs, 3 of 10 lose:

  old=.png  new=.ico   -> glob lists favicon.png first  -> always "expired"
  old=.ico  new=.svg   -> glob lists favicon.ico first  -> always "expired"
  old=.png  new=.webp  -> glob lists favicon.png first  -> always "expired"

The cost of a false "expired" isn't nothing: it's an in-page fetch() through the watch's proxy on every check, plus favicon-fetcher.js's sequential per-icon 2s timeouts, for an icon that is already saved and current.

The fix

Both ends of the ambiguity:

  • favicon_is_expired() picks the newest candidate by mtime instead of the first glob match.
  • bump_favicon() removes superseded favicon.* files, so the situation stops arising.

Also tidied, same function

  • os.path.isfile(None) raised TypeError whenever the module-level filename cache said yes after the file had been removed — which the broad except turned into a logger.critical() for an entirely benign condition. The empty case is now handled explicitly.
  • The broad except stays deliberately (this runs inline on the check path, as an argument to fetcher.run(), so it must never be the reason a watch fails) but logs at warning, since the only cost of being wrong here is one extra favicon fetch.

Tests

Four tests added to TestFaviconExpiry. Two of them fail on master:

test_bump_favicon_removes_the_superseded_file
  AssertionError: Lists differ: ['favicon.ico', 'favicon.png'] != ['favicon.png']

test_fresh_favicon_wins_over_a_stale_one_of_another_extension
  a favicon saved seconds ago must not count as expired just because an older one
  of a different extension is also present

Both pass with the change.

🤖 Generated with Claude Code

… glob lists first

bump_favicon() names the saved file after the icon's type (favicon.png, favicon.ico, ...)
and did not remove a previous save under a different extension, so a watch whose icon
changed type ends up holding more than one favicon.*.

favicon_is_expired() then picked next(iter(glob.glob("favicon.*"))). glob order is
os.scandir order - stable, but decided by filename hash - so for some extension pairs the
stale file wins every single time, its age always exceeds the 24h threshold, and the
favicon is refetched on every check forever. Measured across realistic pairs, 3 of 10
lose that way:

  old=.png new=.ico   -> glob lists favicon.png first  -> always expired
  old=.ico new=.svg   -> glob lists favicon.ico first  -> always expired
  old=.png new=.webp  -> glob lists favicon.png first  -> always expired

That is an in-page network fetch through the watch's proxy on every check, plus the
favicon fetcher's own sequential per-icon timeouts, for a favicon that is already saved
and current.

Fixed at both ends: pick the newest candidate by mtime, and have bump_favicon() drop
superseded files so the ambiguity stops arising.

Also tidied two things in the same function:

- os.path.isfile(None) raised a TypeError when the module-level filename cache said yes
  after the file had been removed, which the broad except turned into a logger.critical()
  for an entirely benign condition. The empty case is now handled explicitly.
- the broad except stays (this runs inline on the check path as an argument to
  fetcher.run(), so it must never fail a watch) but logs at warning, since the only cost
  of getting this wrong is one extra favicon fetch.

Tests fail on master (favicon.ico left behind; fresh icon reported expired) and pass here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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