fix(client): profile picture doesn't render on android #256
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#256
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
After a profile picture is successfully uploaded, the universal client's avatar component on Android shows nothing (or falls back to initials) — the image never renders. Web displays it correctly.
The avatar is served by
GET /api/profile/picture, which is an authenticated route. PR #248 deliberately renders the avatar via<Image source={{ uri, headers }}>so the bearer Authorization header is attached on the off-origin fetch — that's the right shape. The failure modes most likely to bite:<Image>component'sheadersprop quietly stops being honored on certain RN versions / Hermes builds. Worth verifying it actually fires the GET with the header.uriis relative (/api/profile/picture), and<Image>doesn't run through the off-origin URL rewriter that the typed client uses — so it tries to resolve against an undefined origin and fails. The avatar needs the absolute URL spliced in bygetCachedServerUrl()before being passed to<Image>.?v=<pictureUploadedAt>to invalidate the cache after re-upload. On native, the?v=query interacts with the server response in a way the typed client handles but<Image>may not.image/webp. RN's<Image>supports WebP on Android 14+ natively but older Androids need explicit decoder libs. Less likely than #2, but worth checking.Source
User-reported (June 2026), observed after #248 shipped and pictures can actually be uploaded (once #253 lands).
Approach
Most likely fix: ensure the
<Image>uriis the absolute URL with the off-origin server base spliced in, with the Authorization header attached. Both must travel together: relative URL + auth header is the broken combo because<Image>doesn't run the URL rewriter middleware.Implementation surface:
apps/client/lib/profile/pictureSrc.ts(or similar): builds the{ uri, headers }shape on demand, splicing the runtime server URL ahead of/api/profile/picture?v=<…>on native and falling back to relative on PWA.{ uri, headers }inline.Scope
getProfilePictureSource(pictureUploadedAt):{ uri: \/api/profile/picture?v=${ts}` }`. Same-origin → relative is correct.{ uri: \${serverUrl}/api/profile/picture?v=${ts}`, headers: { Authorization: `Bearer ${token}` } }`.useEffects it wheneverpictureUploadedAtchanges (becausegetAuthHeader()is async).Acceptance criteria
getAuthHeader()returns null (logged out, somehow), the avatar falls back to initials instead of showing a broken-image icon.Out of scope
<picture>element with multiple format candidates.Composes with