Skip to content

fix(mobile): send photos, files and pasted images again - #2444

Merged
DoodleBears merged 2 commits into
mainfrom
claude/project-thread-6klipi
Oct 21, 2026
Merged

DoodleBears merged 2 commits into
mainfrom
claude/project-thread-6klipi

Conversation

@DoodleBears

Copy link
Copy Markdown
Contributor

Requested by Vain · project thread

Why the change

The phone app could not send any attachment: photos shared from Photos, photos picked with + → Photos, and pasted images all stayed on "Not sent · Tap to retry".

Issue / Discussion: Refs #2422 (comment)

Special things to note

  • Root cause. Since Expo SDK 58 (chore(mobile): upgrade to Expo SDK 58 with required bundle exports #2024), the app's global fetch is expo/fetch. When the body is a Blob, expo/fetch sends the Blob's own type as Content-Type and drops the header the caller set (normalizeBodyInitAsync → overrideHeaders in expo/src/winter/fetch). A photo read from file:// is an image/jpeg Blob, so the upload went out as image/jpeg. The device gateway only routes application/octet-stream POSTs to the attachment handler, so the upload never reached it, upload() threw, and channelSend was never called. A probe device sending real octet-stream succeeds, which matches the QA finding. This was not caused by fix(mobile): Android fixes from the real-device QA of #2364, #2380 and #2401 #2422, feat(mobile): share text, links and files to a Bot from any app #2401 or feat(mobile): chat QoL — pull up to type, paste images, double-tap 👍, peek at chats #2364. No recent mobile change touched upload() or deliver().
  • Fix 1. upload() re-wraps the Blob as new Blob([data], { type: 'application/octet-stream' }), so the header that goes on the wire is octet-stream whatever fetch does. The same JS runs on iOS and Android.
  • Fix 2 (Android paste). On Android, expo/fetch only accepts http(s) and file:// URLs. It cannot open the data: URI a clipboard image uses, so a pasted image could not even be read. fromClipboardImage now writes the base64 data to a cache file, reads that file, and deletes it.
  • Merge risk: low. The change covers only the upload body and how a paste is read. To revert, revert this one commit.
  • Verification:
    • Automated: the fake Host now behaves like expo/fetch plus the real gateway, sending the Blob type as Content-Type and refusing anything that is not octet-stream. The new test uploads a typed photo as octet-stream so the Host accepts it and the two existing upload tests fail without the fix and pass with it. A new clipboard test checks that a paste is read from a cache file, not from the data: URI. pnpm vitest run apps/mobile gives 982 passed and 9 skipped, and the mobile typecheck passes.
    • Runtime: on a live isolated Host built from main, I sent an image/png content type straight to /botharness-device/api/botharness/attachment/upload and got 502 host-session-unavailable. An octet-stream upload got 200 and an attachment back. attachments.live.test.ts passes against that Host.
    • Not run: native iOS or Android builds. There was no simulator or device in this environment. Please re-run the QA share / + → Photos / paste send on iOS and on Android.
  • CI test plan: affected (mobile sources and tests only). No new config files or test directories.

Change outline

  • apps/mobile/src/host/client.ts: the upload body is always an octet-stream Blob.
  • apps/mobile/src/host/pick-files.ts: a pasted data: image is read through a temporary cache file.
  • apps/mobile/test/fake-host.ts: the upload content type now comes from the Blob type, as expo/fetch sends it, and non-octet-stream uploads are refused.
  • Tests: conversation.test.ts, clipboard-image.test.tsx, plus an expo-file-system mock in incoming-share.test.ts.

No UI changes.


Generated by Claude Code

The app's global fetch is expo/fetch (Expo SDK 58). It sends a Blob body
with the Blob's own type as Content-Type, overriding the octet-stream
header the upload set, so the Host turned every photo upload away and the
message stayed "Not sent". Uploads now go out as an octet-stream Blob.
On Android expo/fetch cannot open the data: URI a pasted image uses, so
the paste is read back from a cache file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKDsNqHnFbKdw4gcR9K5Yk
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9e8a147d-45fd-430b-88bc-048a9168f833
📥 Commits

Reviewing files that changed from the base of the PR and between a32b62b and ca9c7a8.

📒 Files selected for processing: 7
  • apps/mobile/src/host/client.ts
  • apps/mobile/src/host/pick-files.ts
  • apps/mobile/test/clipboard-image.test.tsx
  • apps/mobile/test/conversation.test.ts
  • apps/mobile/test/fake-host.ts
  • apps/mobile/test/incoming-share.test.ts
  • changes/2444-mobile-attachment-upload.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKDsNqHnFbKdw4gcR9K5Yk
@DoodleBears DoodleBears self-assigned this Oct 21, 2026
@DoodleBears
DoodleBears merged commit 00b84ee into main Oct 21, 2026
18 of 20 checks passed
@DoodleBears
DoodleBears deleted the claude/project-thread-6klipi branch October 21, 2026 05:13

Copy link
Copy Markdown
Contributor Author

iOS 27 Simulator at ca9c7a8: all 6 pass. This ran on a Release Simulator build, with the QA Host on main a32b62b. Every send showed as sent, never "Not sent". The Host's channel messages confirmed each one arrived, and the Bot replied describing each image.

Path DM Channel
Share 2 photos from Photos ✅ ✅
+ → Photos ✅ ✅
Paste an image ✅ ✅

A nit that doesn't block: a HEIC photo picked with + → Photos shows as a file chip in the app instead of an image preview. The Bot still read it.


Generated by Claude Code

DoodleBears added a commit that referenced this pull request Oct 21, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Android phone result: PASS (Xiaomi, Android 16 / HyperOS 3, release build from main c67d231 with this PR merged, isolated QA Host)

Screenshots: 2444-android/phone (commit 090d949)

Path Files Arrived on the Host
Share 2 photos from the gallery into the app 1-send-to-two-images-light.png, 2-gallery-share-sent-light.png qa-2.png (5181 bytes) and qa-1.png (5179 bytes), the same size as the originals
+ → Photos, pick 1 3-picker-photo-tile-light.png, 4-picker-photo-sent-light.png 1000044169.png (4610 bytes). The name is the media ID and the file is smaller than the original; the system photo picker re-encodes it, with "不含位置信息" checked by default.
Copy an image in Chrome, paste into the composer 5-pasted-tile-light.png, 6-pasted-sent-light.png pasted-4.jpg (9222 bytes)

The Bot described the content of each image in its reply, which confirms that every image reached it.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants