A site icon that is missing is not used (#79)

site_icon took the icon a site's page names in its <link> tags without
asking for it, so a dead one was stored and /favicon.ico never tried.
antirez.com names /images/favicon.png, which is a 404, while its
/favicon.ico is there; the feed showed no artwork, and since the lookup
happens once, never would. The named icon now has to answer with an
image, as /favicon.ico already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 12:41:57 +00:00
parent 00f6293b95
commit 95a8633877
2 changed files with 19 additions and 8 deletions

View File

@@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- A feed whose website names an icon that is missing shows the site's `/favicon.ico` instead of
no artwork.
- The read button shows whether an item is read, as the pin beside it shows whether it is - The read button shows whether an item is read, as the pin beside it shows whether it is
pinned: a tick when read, an envelope when not. It used to show the opposite. pinned: a tick when read, an envelope when not. It used to show the opposite.
- In Directory and Popular, Subscribe is a plus again, not the tick that marks a feed you have. - In Directory and Popular, Subscribe is a plus again, not the tick that marks a feed you have.

View File

@@ -377,22 +377,31 @@ pub async fn site_icon(client: &reqwest::Client, site: &str) -> Option<String> {
// Relative to where the page ended up, not where it was asked for: a site that redirects // Relative to where the page ended up, not where it was asked for: a site that redirects
// to /en/ would otherwise have its icon looked for in the wrong place. // to /en/ would otherwise have its icon looked for in the wrong place.
let base = resp.url().clone(); let base = resp.url().clone();
// The icon a page names can be gone: antirez.com names /images/favicon.png, a 404, while its
// /favicon.ico is there. Stored unchecked, it was a broken image that was never looked up again.
if resp.status().is_success() if resp.status().is_success()
&& let Ok(page) = resp.bytes().await && let Ok(page) = resp.bytes().await
&& let Some(href) = page_icon(&page) && let Some(href) = page_icon(&page)
&& let Ok(url) = base.join(&href) && let Ok(url) = base.join(&href)
&& is_image(client, &url).await
{ {
return Some(url.into()); return Some(url.into());
} }
let ico = base.join("/favicon.ico").ok()?; let ico = base.join("/favicon.ico").ok()?;
let resp = client.get(ico.clone()).timeout(timeout).send().await.ok()?; is_image(client, &ico).await.then(|| ico.into())
let is_image = resp }
/// Whether `url` answers with an image. A site with no favicon often answers 200 with its home
/// page, which is not an icon.
async fn is_image(client: &reqwest::Client, url: &reqwest::Url) -> bool {
let timeout = std::time::Duration::from_secs(20);
let Ok(resp) = client.get(url.clone()).timeout(timeout).send().await else { return false };
resp.status().is_success()
&& resp
.headers() .headers()
.get(reqwest::header::CONTENT_TYPE) .get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.is_some_and(|t| t.starts_with("image/")); .is_some_and(|t| t.starts_with("image/"))
// A site with no favicon often answers 200 with its home page, which is not an icon.
(resp.status().is_success() && is_image).then(|| ico.into())
} }
/// The icon a web page names in its `<link>` tags, the larger apple-touch-icon first: a plain /// The icon a web page names in its `<link>` tags, the larger apple-touch-icon first: a plain