Skip to content

User avatar never renders in Header β€” reads authenticatedUser.avatar, a field the platform has never providedΒ #673

Description

@brian-smith-tcril

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:

  1. Wrong field β€” MFEs that do hydrate (e.g. frontend-app-account, frontend-app-profile) have authenticatedUser.profileImage available, but the header reads the nonexistent .avatar.
  2. Not hydrated β€” MFEs that don't opt in (e.g. frontend-app-learner-dashboard) never fetch profileImage at 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): original SiteHeader.jsx destructured avatar from @edx/frontend-base's AuthenticationContext. That context never supplied avatar β€” 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 destructured avatar into avatar: authenticatedUser.avatar β€” this is where the current form was introduced.
  • 57e05c4 (2019-11-04, Modernize build and fix the anonymous headerΒ #26): added the !== null guard only.
  • Likely origin of the field name: frontend-app-learning once contained a design doc docs/api-strawperson.js (since deleted) sketching a proposed user: { …, 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 under profileImage.

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-account and frontend-app-profile previously rendered their own vendored headers that read the image correctly from state.userAccount.profileImage β€” and regressed to a missing avatar when they adopted @edx/frontend-component-header.
  • The real image data is consistently sourced from profileImage.imageUrlMedium everywhere in the ecosystem (e.g. frontend-app-learner-portal-enterprise's AvatarDropdown), making this component's avatar read the lone outlier.

Suggested direction (fix deferred)

  1. In src/Header.jsx, fall back to the hydrated profile image, mirroring the Studio fix and guarding on hasImage:
    avatar: authenticatedUser
      ? (authenticatedUser.avatar
         || (authenticatedUser.profileImage?.hasImage
             ? authenticatedUser.profileImage.imageUrlMedium
             : null))
      : null,
  2. Document that consuming MFEs must call initialize({ hydrateAuthenticatedUser: true }) for the image to be available (relevant to frontend-app-learner-dashboard and any other full-Header consumer 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 the avatar prop.
  • Reference for the correct pattern: src/studio-header/StudioHeader.tsx (fixed in 38a68e5).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions