Repository navigation
User avatar never renders in Header β reads authenticatedUser.avatar, a field the platform has never providedΒ #673
Description
Important
π€ Generated by Claude
This issue β the investigation, evidence, and write-up β was produced by Claude (Claude Code), reviewed by a human before posting. Commit references, dates, and file paths were gathered programmatically from git history and the source repos; please still sanity-check specifics before acting on them.
Summary
The user avatar in the desktop and mobile user-menu toggles (DesktopUserMenuToggle / MobileUserMenuToggle) never displays a real image β it always falls back to the placeholder AvatarIcon.
The root cause is a single field-name mismatch: src/Header.jsx reads authenticatedUser.avatar, but no layer of the Open edX stack has ever populated an avatar field on the authenticated user. The profile image has always lived at authenticatedUser.profileImage.imageUrlMedium (populated by hydrateAuthenticatedUser()).
The Studio header hit the identical bug and was fixed in commit 38a68e5 ("fix: show hydrated profile avatars in Studio header"). The same fix was never applied to the main desktop/mobile Header.
Note: this issue is a write-up of the investigation only. It intentionally does not include a fix PR yet.
Symptom & scope
| Header | Avatar source | Status |
|---|---|---|
Header β DesktopHeader β DesktopUserMenuToggle |
authenticatedUser.avatar |
β broken |
Header β MobileHeader β MobileUserMenuToggle |
authenticatedUser.avatar |
β broken |
StudioHeader β UserMenu |
authenticatedUser.avatar || profileImage.imageUrlMedium |
β
fixed (38a68e5) |
LearningHeader |
renders no avatar (username only) | β n/a |
Root cause
The plumbing inside this repo is correct end-to-end:
Header.jsx β DesktopHeaderSlot β DesktopHeader β DesktopUserMenuToggleSlot β DesktopUserMenuToggle β Avatar src={avatar}
The break is the source value at the top:
// src/Header.jsx
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,authenticatedUser.avatar is effectively always undefined, so Avatar renders the placeholder icon.
Why avatar is never set β evidence across the whole stack
| Layer | What it actually provides | avatar? |
|---|---|---|
edx-platform accounts API /api/user/v1/accounts/{username} |
profile_image: { has_image, image_url_full/large/medium/small } β shape stable since March 2015 (46b63164a) |
β never |
@edx/frontend-platform hydrateAuthenticatedUser() |
camelCases the above β profileImage: { hasImage, imageUrlMedium, β¦ } via setAuthenticatedUser({ ...user, ...camelCaseObject(response.data) }) |
β never |
@edx/frontend-platform JWT-decoded user |
{ email, userId, username, roles, administrator, name } |
β never |
@edx/frontend-auth (legacy) userAccount model |
profileImage: { imageUrlMedium, imageUrlLarge } |
β never |
@edx/frontend-base v1.1.0 / v2.0.0 (the versions this repo pinned in 2019) |
getAuthenticatedUser returns exactly { userId, username, roles, administrator } (explicit field pick β no spread, so a JWT claim couldn't leak through either) |
β never |
A live sample of the accounts endpoint today confirms it β profile_image.image_url_medium present, has_image: true, and no avatar key.
The correct pattern
Mirror the Studio header fix (src/studio-header/StudioHeader.tsx):
const authenticatedUserAvatar = authenticatedUser?.avatar
|| (profileImage?.hasImage ? profileImage.imageUrlMedium : null);Note the profileImage.hasImage guard β when the user has no uploaded image the API still returns profile_image with has_image: false and default-image URLs, so the guard matters.
Two failure modes across consumers
Profile-image data is only present if the MFE opts into hydration β initialize({ hydrateAuthenticatedUser: true }), which defaults to false. So consumers fail in one of two ways:
- Wrong field β MFEs that do hydrate (e.g.
frontend-app-account,frontend-app-profile) haveauthenticatedUser.profileImageavailable, but the header reads the nonexistent.avatar. - Not hydrated β MFEs that don't opt in (e.g.
frontend-app-learner-dashboard) never fetchprofileImageat all, so even a corrected field read would need hydration enabled.
Historical provenance (why the code looks like this)
afe6705(2019-09-13, initial commit): originalSiteHeader.jsxdestructuredavatarfrom@edx/frontend-base'sAuthenticationContext. That context never suppliedavatarβ and the author's own example/test mocks only set{ userId, username, administrator }.f9d1734(2019-09-18, "upgrading to frontend-base 2.0.0"): the frontend-base 2.0 migration converted the destructuredavatarintoavatar: authenticatedUser.avatarβ this is where the current form was introduced.57e05c4(2019-11-04, Modernize build and fix the anonymous headerΒ #26): added the!== nullguard only.- Likely origin of the field name:
frontend-app-learningonce contained a design docdocs/api-strawperson.js(since deleted) sketching a proposeduser: { β¦, avatar: String }API shape that was never implemented. The header appears to have been written against that aspirational contract, while the shipped data always lived underprofileImage.
So the avatar read has been dead on arrival since the repo's first commit; no dependency bump ever made it live.
Ecosystem audit (for completeness)
A full git-history pickaxe (git log --all -S avatar) across all 13 frontend-app-* consumers of this package found that no MFE has ever injected an avatar field onto authenticatedUser (no manual AppContext.Provider, no custom auth/hydration handler) β in current code or anywhere in history. Every avatar occurrence is incidental (Paragon <Avatar>, CSS class names, forum/API profile-image data, package-lock noise).
Two notable findings:
frontend-app-accountandfrontend-app-profilepreviously rendered their own vendored headers that read the image correctly fromstate.userAccount.profileImageβ and regressed to a missing avatar when they adopted@edx/frontend-component-header.- The real image data is consistently sourced from
profileImage.imageUrlMediumeverywhere in the ecosystem (e.g.frontend-app-learner-portal-enterprise'sAvatarDropdown), making this component'savatarread the lone outlier.
Suggested direction (fix deferred)
- In
src/Header.jsx, fall back to the hydrated profile image, mirroring the Studio fix and guarding onhasImage:avatar: authenticatedUser ? (authenticatedUser.avatar || (authenticatedUser.profileImage?.hasImage ? authenticatedUser.profileImage.imageUrlMedium : null)) : null,
- Document that consuming MFEs must call
initialize({ hydrateAuthenticatedUser: true })for the image to be available (relevant tofrontend-app-learner-dashboardand any other full-Headerconsumer that doesn't hydrate today).
Affected files
src/Header.jsxβ the buggy read (feeds both desktop and mobile).src/desktop-header/DesktopUserMenuToggle.jsx,src/mobile-header/MobileUserMenuToggle.jsxβ consume theavatarprop.- Reference for the correct pattern:
src/studio-header/StudioHeader.tsx(fixed in38a68e5).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Review
Activity
brian-smith-tcril commentedon Jul 6, 2026
Important
π€ Generated by Claude (Claude Code), human-reviewed before posting.
Relationship to #670 (Studio header fix)
The Studio header was fixed for exactly this bug in #670 ("fix: show hydrated profile avatars in Studio header", merged 2026-05-07), which added the
profileImage.imageUrlMediumfallback that this issue recommends generalizing to the desktop/mobileHeader. That PR explicitly scopes the LMS header out as future work:That "LMS header" is the desktop/mobile
Headerpath this issue covers β so this issue can serve as the tracking issue for #670's deferred work.One correction to carry forward
#670's description frames keeping
authenticatedUser.avatarfirst as preserving "the legacy header avatar contract." Our investigation (detailed above) found there is no legacy avatar contract βauthenticatedUser.avatarhas never been populated by any layer of the stack at any point in history:@edx/frontend-base(v1.1.0 / v2.0.0),@edx/frontend-auth, or@edx/frontend-platformhas ever set it.edx-platformaccounts API has never returned it (the field has always beenprofile_image, since 2015).frontend-app-*consumers found no MFE ever injected it ontoauthenticatedUser.So the
authenticatedUser?.avatar ||branch (in both the merged Studio fix and any future desktop/mobile fix) is effectively dead code in the standard Open edX stack β harmless as the first term of an||, but it cannot be truthy unless a consumer hand-injects the field, which none do. TheprofileImage.imageUrlMediumfallback does 100% of the real work. Worth keeping in mind so the desktop/mobile fix isn't justified on the same "preserving a working contract" premise β there's nothing working to preserve.bradenmacdonald commentedon Jul 21, 2026
#670 and openedx/frontend-app-authoring#3043 fix the issue in the Authoring MFE, but there are some remaining CSS issues to work through, because the image is not properly scaled and cropped when it appears.
arbrandes commentedon Aug 7, 2026
@bradenmacdonald, did #678 fix the remaining issues (in frontend-component-header)?
Either way, we're going to have to fore-port this to the frontend-base header.
bradenmacdonald commentedon Aug 7, 2026
@arbrandes I'm trying to test it now but getting some weird React error, probably another side effect of
npm linkresulting in multiple versions of react being installed. If you publish a release to NPM, I can test it out.arbrandes commentedon Aug 11, 2026
What, this isn't publishing to NPM? Something's not right.