From 95a8633877c4f339ea92a1912a7b1761c638c08b Mon Sep 17 00:00:00 2001 From: rays Date: Tue, 29 Sep 2026 12:41:57 +0000 Subject: [PATCH] A site icon that is missing is not used (#79) site_icon took the icon a site's page names in its 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 --- CHANGELOG.md | 2 ++ src/feed.rs | 25 +++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d22cd..df004a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 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. diff --git a/src/feed.rs b/src/feed.rs index c347255..3680e1d 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -377,22 +377,31 @@ pub async fn site_icon(client: &reqwest::Client, site: &str) -> Option { // 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. 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() && let Ok(page) = resp.bytes().await && let Some(href) = page_icon(&page) && let Ok(url) = base.join(&href) + && is_image(client, &url).await { return Some(url.into()); } let ico = base.join("/favicon.ico").ok()?; - let resp = client.get(ico.clone()).timeout(timeout).send().await.ok()?; - let is_image = resp - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .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()) + is_image(client, &ico).await.then(|| ico.into()) +} + +/// 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() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .is_some_and(|t| t.starts_with("image/")) } /// The icon a web page names in its `` tags, the larger apple-touch-icon first: a plain