fix(client): profile picture upload fails on android (fetch() doesn't read file:// URIs) #253
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#253
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
Uploading a profile picture from the universal client works on web but fails on Android with the generic "Upload failed. Try again." surfaced by the screen's error path.
Root cause:
apps/client/lib/profile/pictureUpload.ts→fetchPickedAsBlob(uri)doesfetch(uri).then(r => r.blob())to convert the picker URI to a Blob, thenuseUpdateProfilePicture(packages/api-client/src/hooks/picture.ts) appends the Blob to FormData.This works on web because
expo-image-pickerreturns ablob:ordata:URL there, andfetch()happily reads either. On Android the picker returns afile:///storage/emulated/0/...URI, and React Native'sfetchdoes NOT natively readfile://URIs — it throws a network error. The thrown error is caught by the screen's genericsetError("uploadFailed")branch, so the user sees no useful diagnostic.The RN-idiomatic upload pattern doesn't go through
fetchat all on native: you build FormData with a file-descriptor object{ uri, name, type }and append it directly. RN'swhatwg-fetchpolyfill recognises that shape and streams the file at send time without ever materialising a JS Blob.Source
User-reported (June 2026) after PR #248 shipped. PR #248's local gates all passed because the new tests construct FormData with web-style Blobs and the agent had no Android device to exercise the picker URI path.
Scope
apps/client/lib/profile/pictureUpload.ts(or a sibling) grows abuildPictureFormData()helper that branches onPlatform.OS:fetchPickedAsBlob→form.append("file", blob, name)path.form.append("file", { uri, name, type } as any)— the cast is necessary because RN's ambient FormData typings don't expose the file-descriptor shape.useUpdateProfilePicture'smutationFnsignature changes from(file: Blob | File)to(form: FormData). The screen builds the FormData viabuildPictureFormData(picked).apps/client/app/(app)/profile.tsx) calls the new helper instead offetchPickedAsBlobdirectly.Platform.OS.Acceptance criteria
Out of scope
idea.md.sharppipeline handles resize.Composes with