From b0ddf23b85217f6bc99d46bcfa9f4efe14ce929e Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Sat, 16 May 2026 13:00:49 -0700 Subject: [PATCH] fix(native): allow posting with attached image Unable to connect. PLease check your internet connection and try again. (Error probably started after Upload images in WebP format 0bd74eaf7a30c485c5b03fc82bd1193a5530a74b?) (look into Fix RN does not allow Blob from ArrayBuffer caa14e93f0545b915da48ac3c0ac4e55b2158f23?) --- src/lib/api/upload-blob.ts | 96 ++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts index 260ba770b..8daf7e912 100644 --- a/src/lib/api/upload-blob.ts +++ b/src/lib/api/upload-blob.ts @@ -1,7 +1,14 @@ -import {copyAsync} from 'expo-file-system/legacy' +import { + cacheDirectory, + copyAsync, + EncodingType, + writeAsStringAsync, +} from 'expo-file-system/legacy' import {type BskyAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api' +import {nanoid} from 'nanoid/non-secure' import {safeDeleteAsync} from '#/lib/media/manip' +import {IS_NATIVE} from '#/env' /** * @param encoding Allows overriding the blob's type @@ -22,7 +29,9 @@ export async function uploadBlob( } if (typeof input === 'string' && input.startsWith('data:')) { - const blob = await fetch(input).then(r => r.blob()) + const blob = IS_NATIVE + ? await dataUriToBlobOnNative(input) + : await dataUriToBlobViaFetch(input) return agent.uploadBlob(blob, {encoding}) } @@ -35,23 +44,84 @@ export async function uploadBlob( async function asBlob(uri: string): Promise { return withSafeFile(uri, async safeUri => { - // Note - // Android does not support `fetch()` on `file://` URIs. for this reason, we - // use XMLHttpRequest instead of simply calling: - - // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob()) - - return await new Promise((resolve, reject) => { + // Android does not support fetch() on file:// URIs; use XHR. RN sometimes + // fires onerror or omits status even when the blob is readable, so we use + // onloadend and only check the response. + let xhrUrl = safeUri + try { + xhrUrl = decodeURI(safeUri) + } catch { + /* keep safeUri */ + } + return await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() - xhr.onload = () => resolve(xhr.response) - xhr.onerror = () => reject(new Error('Failed to load blob')) xhr.responseType = 'blob' - xhr.open('GET', safeUri, true) - xhr.send(null) + xhr.onloadend = () => { + const blob = xhr.response + if (blob instanceof Blob && blob.size > 0) { + resolve(blob) + } else { + // Message deliberately avoids substrings matched by isNetworkError() + reject(new Error('Failed to read local image file for upload.')) + } + } + xhr.open('GET', xhrUrl, true) + xhr.send() }) }) } +/** + * RN fetch() on large data: URIs (e.g. full-screen PNG screenshots) often fails; + * DCIM JPEGs more often use the file path after compression. Write to cache and + * reuse XHR like other local files. + */ +async function dataUriToBlobOnNative(dataUri: string): Promise { + const comma = dataUri.indexOf(',') + if (comma === -1 || !dataUri.startsWith('data:')) { + throw new Error('Failed to read image data for upload.') + } + const header = dataUri.slice(5, comma) + const payload = dataUri.slice(comma + 1) + const isBase64 = /(^|;)\s*base64\s*(;|$)/i.test(header) + if (!isBase64) { + return dataUriToBlobViaFetch(dataUri) + } + const baseDir = cacheDirectory + if (!baseDir) { + return dataUriToBlobViaFetch(dataUri) + } + const tmpUri = joinPath(baseDir, `upload-blob-${nanoid()}.bin`) + try { + await writeAsStringAsync(tmpUri, payload, { + encoding: EncodingType.Base64, + }) + return await asBlob(tmpUri) + } finally { + await safeDeleteAsync(tmpUri) + } +} + +function joinPath(dir: string, name: string) { + if (dir.endsWith('/')) { + return name.startsWith('/') ? dir.slice(0, -1) + name : dir + name + } + return name.startsWith('/') ? dir + name : `${dir}/${name}` +} + +async function dataUriToBlobViaFetch(dataUri: string): Promise { + try { + const response = await fetch(dataUri) + const blob = await response.blob() + if (!(blob instanceof Blob) || blob.size === 0) { + throw new Error('empty') + } + return blob + } catch { + throw new Error('Failed to read image data for upload.') + } +} + // HACK // React native has a bug that inflates the size of jpegs on upload // we get around that by renaming the file ext to .bin -- 2.51.2