Time out a hung feed, serve the precomposed touch icon, and store http artwork on https (#108, #109, #110)
#108: the HTTP client had no timeout, and scans handle feeds in order, so a hung server held every scan. Dreamwidth answered 504 after 60-67 s for a day and each scan took 70-80 s instead of 15. A feed fetch, and a Patreon creator's show list, now gets 30 s from connecting to the last byte (feed::FEED_TIMEOUT); the client gets a 10 s connect timeout, which bounds a download's start but not a long download. #109: iOS asks for /apple-touch-icon-precomposed.png first when the site is added to a home screen; it was a 404 and the only non-feed warning in the log. It serves the same icon. #110: the page is https and loads no http. Artwork on http came through /api/art (#90) even when its host serves https too. A scan now tries each http artwork host on https once per feed (feed::prefer_https) and stores the https address where the host answers with an image, rewriting that feed's stored items from the same host (Db::secure_images). 4 of the 5 hosts in production do; cdn.thesecretcabal.com presents another name's certificate and stays on /api/art. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
32
src/db.rs
32
src/db.rs
@@ -791,6 +791,21 @@ impl Db {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewrites a feed's stored artwork on `host` from http to https, once the host is known to
|
||||
/// serve it there (`feed::prefer_https`), for the items a scan does not write again.
|
||||
pub async fn secure_images(&self, feed_id: &str, host: &str) -> Result<()> {
|
||||
let like = format!("http://{host}/%");
|
||||
for table in ["entries", "feeds"] {
|
||||
let col = if table == "feeds" { "id" } else { "feed_id" };
|
||||
self.exec(
|
||||
&format!("UPDATE {table} SET image = 'https://' || substr(image, 8) WHERE {col} = $1 AND image LIKE $2"),
|
||||
vec![feed_id.into(), like.clone().into()],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Forgets a feed and everything stored about it.
|
||||
pub async fn forget_feed(&self, id: &str) -> Result<()> {
|
||||
self.forget_rows(id).await?;
|
||||
@@ -2032,6 +2047,23 @@ mod tests {
|
||||
assert_eq!((list["f"].unread, list["g"].unread), (1, 1)); // a read, c hidden; sam's read is sam's
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_hosts_artwork_moves_to_https_for_one_feed() {
|
||||
let db = Db::memory().await.unwrap();
|
||||
db.exec_for_test(
|
||||
"INSERT INTO feeds (id, url, image) VALUES ('f','u','http://a.example/logo.png'),('g','v','http://a.example/g.png');
|
||||
INSERT INTO entries (feed_id, guid, first_seen, image) VALUES
|
||||
('f','1',0,'http://a.example/1.jpg'),('f','2',0,'http://b.example/2.jpg'),('g','3',0,'http://a.example/3.jpg');",
|
||||
).await
|
||||
.unwrap();
|
||||
db.secure_images("f", "a.example").await.unwrap();
|
||||
assert_eq!(db.feed_summary("f").await.unwrap().image.as_deref(), Some("https://a.example/logo.png"));
|
||||
assert!(db.names_image("https://a.example/1.jpg").await.unwrap());
|
||||
assert!(db.names_image("http://b.example/2.jpg").await.unwrap()); // another host
|
||||
assert!(db.names_image("http://a.example/3.jpg").await.unwrap()); // another feed
|
||||
assert_eq!(db.feed_summary("g").await.unwrap().image.as_deref(), Some("http://a.example/g.png"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_feeds_nobody_subscribes_to_are_found_and_the_rest_left() {
|
||||
let db = Db::memory().await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user