Skip to content

fix(web): stop a single image error from permanently blanking an avatar - #14586

Merged
dylanjeffers merged 1 commit into
mainfrom
fix/avatar-error-latch
Sep 4, 2026
Merged

fix(web): stop a single image error from permanently blanking an avatar#14586
dylanjeffers merged 1 commit into
mainfrom
fix/avatar-error-latch

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

Problem

Reported in Slack: "some profile images aren't showing in search for people that have them" — RAC, MR•CAR/\ACK and Critical Music all rendering the gray silhouette.

Reproduced on audius.co/search/profiles?query=beats: @lofibeat renders the placeholder, but their profile picture (QmY685fLjPvG3Ve75TZ9FaMsbVpDb5voRnTzkF5VqRytEf) returns 200 from all four hosts the API lists for it. The image is fine — the client blanked it.

Cause

Avatar latched image failures in a boolean that only reset when userId changed:

const [hasError, setHasError] = useState(false)
useEffect(() => { setHasError(false) }, [userId])
const finalImageSrc = hasError ? imageProfilePicEmptyNew : image

useImageSize renders the primary url optimistically, then preloads it and swaps in a working mirror if it fails. The two race: if the mounted <img> fires onError first, hasError flips and the mirror useImageSize subsequently resolves is thrown away for the rest of the mount. The mirror machinery works; Avatar just discarded its result.

UserArtCard on the explore page has the mirror-image bug — it passes no onError at all, so useImageSize's own retry path never fires there either. Left for a follow-up; it isn't the reported surface.

What sets it off

The API hands out content nodes that answer /health_check but 502/503 on the blob. Six registered nodes currently fail on 100% of content I sampled (cn1/cn3/cn4.mainnet.audiusindex.org, validator.stuffisup.com, audius-nodes.com, audius.zeogrid.com), and 38 of 719 sampled profile-picture primaries (5.3%) pointed at one. rendezvous.Select re-shuffles the primary per request, so the same artist is broken for some sessions and fine for others — RAC draws validator.stuffisup.com as primary in roughly 1 in 6 responses. A transient failure under a burst of cards (as with @lofibeat, whose hosts are all healthy) does it too.

A separate api change to blacklist those nodes is worth doing, but this fix is what makes the client survive a bad host at all.

Fix

Track the failing src rather than a boolean, so a later mirror url renders normally, and forward the error to useImageSize's onError so it advances to the next mirror. Once every mirror has failed the placeholder sticks — and since it's a data uri it can't error and re-enter the retry path, so this terminates.

Testing

  • New UserCard regression test: fails on main (× retries a mirror when the primary host fails to render), passes with the fix. Full file 5/5.
  • tsc clean, eslint clean on the touched files.
  • Ran a local build of this branch against prod and re-ran the beats search that reproduced the bug: 24 cards, zero placeholders, including @lofibeat.

🤖 Generated with Claude Code

Avatar tracked image failures in a boolean `hasError` that only reset when
`userId` changed, so the first <img> error swapped in the empty-avatar
placeholder for the life of the mount. useImageSize does resolve a working
mirror after a failure, but Avatar discarded it — the placeholder had already
won — which is why verified artists with perfectly good profile pictures
render a gray silhouette in search.

The trigger is the API handing out a content node that 502s for the blob
(~5% of profile-picture primaries at the moment; the shuffle in
rendezvous.Select re-rolls the primary per request, so the same user shows
up broken for some sessions and fine for others). A transient failure under
a burst of cards does it too.

Track the failing src instead of a boolean, so a later mirror url renders
normally, and forward the error to useImageSize.onError so it advances to
the next mirror rather than leaving the image stranded on a dead host. Once
every mirror has failed the placeholder sticks, and since it is a data uri
it cannot error and re-enter the retry path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e283fa3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dylanjeffers
dylanjeffers merged commit 8aecb03 into main Sep 4, 2026
17 checks passed
@dylanjeffers
dylanjeffers deleted the fix/avatar-error-latch branch September 4, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant