Homebrew RSS reader server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fetcher: fix site_url extraction for atom feeds

* prefer link with rel="alternate" over first link
* add resolve_url helper for relative URL handling
* resolves favicon 404s caused by using feed URL as site URL

Atom feeds can have multiple <link> elements - the "self" link points
to the feed itself while "alternate" points to the HTML site. Using
the first link (often "self") caused favicon fetches to construct
invalid URLs like feed.xml/favicon.ico.

+24 -7
+4 -1
.claude/settings.local.json
··· 11 11 "Bash(rustc:*)", 12 12 "Bash(cargo:*)", 13 13 "WebFetch(domain:github.com)", 14 - "Bash(./target/debug/slurp auth:*)" 14 + "Bash(./target/debug/slurp auth:*)", 15 + "WebFetch(domain:feedafever.com)", 16 + "Bash(jj diff:*)", 17 + "Bash(jj log:*)" 15 18 ] 16 19 } 17 20 }
+1
Cargo.lock
··· 1776 1776 "toml_edit", 1777 1777 "tracing", 1778 1778 "tracing-subscriber", 1779 + "url", 1779 1780 ] 1780 1781 1781 1782 [[package]]
+1
Cargo.toml
··· 22 22 opml = "1.1" 23 23 chrono = "0.4" 24 24 anyhow = "1" 25 + url = "2.5.8"
+18 -6
src/fetcher.rs
··· 3 3 use sqlx::SqlitePool; 4 4 use std::time::Duration; 5 5 use tracing::{info, warn}; 6 + use url::Url; 6 7 7 8 use crate::db; 9 + 10 + fn resolve_url(base: &str, url: &str) -> Option<String> { 11 + Url::parse(url) 12 + .or_else(|_| Url::parse(base).and_then(|b| b.join(url))) 13 + .ok() 14 + .map(|u| u.to_string()) 15 + } 8 16 9 17 pub async fn run(pool: SqlitePool, interval_minutes: u64) { 10 18 let mut interval = tokio::time::interval(Duration::from_secs(interval_minutes * 60)); ··· 46 54 let title = parsed.title.map(|t| t.content).unwrap_or_default(); 47 55 let site_url = parsed 48 56 .links 49 - .first() 57 + .iter() 58 + .find(|l| l.rel.as_deref() == Some("alternate")) 59 + .or_else(|| parsed.links.first()) 50 60 .map(|l| l.href.clone()) 51 61 .unwrap_or_default(); 52 62 ··· 68 78 }); 69 79 70 80 if let Some(url) = icon_url { 71 - match fetch_favicon(client, &url).await { 72 - Ok(data) => { 73 - let favicon_id = db::insert_favicon(pool, &data).await?; 74 - db::set_feed_favicon(pool, feed.id, favicon_id).await?; 81 + if let Some(resolved) = resolve_url(&feed.url, &url) { 82 + match fetch_favicon(client, &resolved).await { 83 + Ok(data) => { 84 + let favicon_id = db::insert_favicon(pool, &data).await?; 85 + db::set_feed_favicon(pool, feed.id, favicon_id).await?; 86 + } 87 + Err(e) => warn!(feed_id = feed.id, "favicon fetch failed: {e:#}"), 75 88 } 76 - Err(e) => warn!(feed_id = feed.id, "favicon fetch failed: {e:#}"), 77 89 } 78 90 } 79 91 }