Show artwork up to 20 MB, and remember and log what failed (#169)

Five Glass Cannon Network feeds name Patreon covers of about 7 MB, over the
5 MB limit on /api/art. Each page load fetched them all again, refused them,
and logged a 502 per cover, about 84 a day, with nothing in the log to say
which image or why: the reason went only to the browser. The limit is now
20 MB, a failure is answered from memory for an hour, and the fetch failure
is logged with its address and reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:10:18 +00:00
parent c37bcf816b
commit 1e7a858e9c
3 changed files with 62 additions and 3 deletions

View File

@@ -1893,10 +1893,11 @@ struct ArtQuery {
/// time it is fetched from the publisher and kept on disk (`art`, up to `art_cache_mb`), so
/// later it is fast, the publisher is not asked on every visit, and it outlives the publisher's
/// server. Only an address some feed or item names as its artwork, and only an image up to
/// 5 MB, so the route cannot be pointed at anything else. It began as a way round http-only
/// 20 MB, so the route cannot be pointed at anything else. It began as a way round http-only
/// artwork on the https page (#90).
async fn art(State(state): State<WebState>, Query(q): Query<ArtQuery>) -> Response {
const MAX: usize = 5 << 20;
// 5 MB turned away the Glass Cannon Network's covers, 7 MB PNGs (#169).
const MAX: usize = 20 << 20;
let web = q.u.starts_with("http://") || q.u.starts_with("https://");
if !web || !state.ctx.db.names_image(&q.u).await.unwrap_or(false) {
return (StatusCode::NOT_FOUND, "no feed names that artwork").into_response();
@@ -1906,6 +1907,9 @@ async fn art(State(state): State<WebState>, Query(q): Query<ArtQuery>) -> Respon
if keep && let Some((kind, body)) = crate::art::read(&file) {
return art_response(kind, body);
}
if let Some(why) = crate::art::failed(&q.u) {
return (StatusCode::BAD_GATEWAY, why).into_response();
}
let got = async {
let mut r = state.ctx.client.get(&q.u).timeout(crate::feed::FEED_TIMEOUT).send().await?.error_for_status()?;
let kind = r.headers().get(header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("").to_owned();
@@ -1924,7 +1928,13 @@ async fn art(State(state): State<WebState>, Query(q): Query<ArtQuery>) -> Respon
}
art_response(kind, body)
}
Err(e) => (StatusCode::BAD_GATEWAY, format!("{e:#}")).into_response(),
Err(e) => {
let why = format!("{e:#}");
// The access line says only that it failed, not which image or why (#169).
tracing::warn!(url = %q.u, error = %why, "could not fetch artwork");
crate::art::fail(&q.u, &why);
(StatusCode::BAD_GATEWAY, why).into_response()
}
}
}