artist art: a provider's API being up is no promise its CDN is
On 2026-07-28 `webservice.fanart.tv` answered every request normally while `assets.fanart.tv` refused TCP on both :443 and :80. The two are different machines, and the hero chain only ever asked the first one anything. Every gate in that chain tests whether a provider RETURNED URLS, never whether those URLs load. So Fanart.tv handed back its usual eight banners and four logos, `banners.is_empty()` was false, and the two fallbacks that exist for exactly this situation -- TheAudioDB, then Bing -- were both skipped. The page then fell back to a blown-up square portrait. The tell that this was a provider outage rather than a regression in the hero code is that it was artist-shaped: every artist Fanart.tv covers lost its hero, and the ones it does not cover (Tame Impala) were untouched. Worth recording that browsing fanart.tv proves nothing here -- that is a third host, on Cloudflare, and it was up throughout. The check is a one-byte ranged GET, so it is proof the host serves image bytes without pulling a 4K background to find out. Any HTTP answer counts as alive: a 404 on one image is not an outage, so only a transport failure or a 5xx marks a host dead. Cached per host for 5 minutes, which is long enough that a dead CDN costs one probe rather than one per artist, and short enough that the hero comes back within minutes of the host recovering -- no restart, no cache clearing. The probe races the page, hence the 3s ceiling. It has to be applied in three places, and the third is the one that is easy to miss: - Discovery, after Fanart and again after TheAudioDB, so an unreachable CDN reads downstream as "this provider had nothing" -- precisely the state the fallback chain already knows how to handle. - The persisted manifest, because a manifest outlives the CDN behind it. A stored answer short-circuits discovery entirely, so replaying one produces a hero that never loads AND no path to the fallback that would have worked. - The "keep previous banners as a floor" clause. Restoring unreachable banners would undo the whole point of falling through: discovery correctly dropped a dead CDN, and the floor would put it straight back. Note for the next time this looks like a renderer bug: a 1x1 hero canvas is a SYMPTOM, not the cause. It means no image ever decoded, so the GPU renderer never sized itself.
This commit is contained in:
1 parent
0cc01328a4
commit
6d6968e9b1
2 files changed
+131
-10
No files matched your search
+30
-10
@@ -1443,8 +1443,19 @@ pub async fn catalog_artist_art(
|
||||
// address the already-known hero immediately instead of rediscovering Bing.
|
||||
let stored = read_artist_art_manifest(&app, artist_id).await;
|
||||
let mut art = match stored {
|
||||
// Fresh, current recipe: serve it, no network.
|
||||
Some(stored) if stored.current => stored.art,
|
||||
// Fresh, current recipe, and the host is still serving the images:
|
||||
// serve it, no network. The liveness check matters because a manifest
|
||||
// outlives the CDN behind it -- measured 2026-07-28, Fanart.tv's API
|
||||
// stayed up while `assets.fanart.tv` refused every connection, so every
|
||||
// stored manifest naming it replayed a hero that could never load and
|
||||
// short-circuited the discovery that would have found a working one.
|
||||
// One probe per host per 5 minutes, so the hero returns on its own once
|
||||
// the CDN does.
|
||||
Some(stored)
|
||||
if stored.current && state.metadata.art_is_servable(&stored.art).await =>
|
||||
{
|
||||
stored.art
|
||||
}
|
||||
// Either nothing on disk, or something stale/older-recipe. Try to do
|
||||
// better, but keep what we have as a floor: the discovery tier fails
|
||||
// per-artist-name (Bing collapses rare names onto popular entities and
|
||||
@@ -1452,14 +1463,23 @@ pub async fn catalog_artist_art(
|
||||
// a normal event, not a reason to show an artist with no hero.
|
||||
other => {
|
||||
let resolved = state.metadata.catalog_artist_art(artist_id).await;
|
||||
// Only banners we already had count as a floor. Note that
|
||||
// `catalog_artist_art` returns Some as soon as the Deezer *profile*
|
||||
// resolves, so "found nothing" arrives as Some with an empty banner
|
||||
// list -- an emptiness check, not a None check, is what detects a
|
||||
// failed hero discovery.
|
||||
let previous = other
|
||||
.map(|stored| stored.art)
|
||||
.filter(|art| !art.banners.is_empty());
|
||||
// Only banners we already had AND can still show count as a floor.
|
||||
// Restoring unreachable ones would undo the whole point of falling
|
||||
// through: discovery correctly dropped a dead CDN, and the floor
|
||||
// would put it straight back. Note that `catalog_artist_art`
|
||||
// returns Some as soon as the Deezer *profile* resolves, so "found
|
||||
// nothing" arrives as Some with an empty banner list -- an
|
||||
// emptiness check, not a None check, is what detects a failed hero
|
||||
// discovery.
|
||||
let previous = match other.map(|stored| stored.art) {
|
||||
Some(art)
|
||||
if !art.banners.is_empty()
|
||||
&& state.metadata.art_is_servable(&art).await =>
|
||||
{
|
||||
Some(art)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match (resolved, previous) {
|
||||
(Some(mut resolved), Some(previous)) if resolved.banners.is_empty() => {
|
||||
log::info!(
|
||||
|
||||
Reference in new issue
Block a user