Skip to content

[OK-33404] fix: read Android bundled assets as strings#12550

Open
huhuanming wants to merge 4 commits into
xfrom
codex/ok-33404-android-image-uri
Open

[OK-33404] fix: read Android bundled assets as strings#12550
huhuanming wants to merge 4 commits into
xfrom
codex/ok-33404-android-image-uri

Conversation

@huhuanming

@huhuanming huhuanming commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Issue

OK-33404 reports that bundled images loaded with React Native require() cannot be read as Base64 in Android production builds.

The affected flow is equivalent to:

const source = require('./blank.png');
const uri = Image.resolveAssetSource(source).uri;

await FileSystem.readAsStringAsync(uri, {
  encoding: 'base64',
});

Observed behavior:

  • Android development builds work.
  • iOS, web, and desktop builds work in both development and production.
  • Android production resolves the image, but the resulting value is not a complete file URI and Expo FileSystem rejects it when reading as Base64.

Expected behavior: callers should be able to pass either a normal URI or the result of require() to one string-reading API and receive the requested content.

Root cause

React Native require('./image.png') does not return a filesystem path. On native platforms it returns a numeric Metro asset ID, which is resolved by Image.resolveAssetSource().

The resolved Android value differs by build mode:

  • In development, Metro generally provides an HTTP URI.
  • In production, Android packages the image into the APK resource table. React Native resolves the asset ID to a scheme-less drawable resource name such as packages_shared_src_assets_blank.

That drawable name is intentional and cannot be converted into a normal absolute path because the image is an APK resource rather than a standalone file.

The legacy Android implementation in expo-file-system then takes different native paths depending on the requested encoding:

  • Non-Base64 readAsStringAsync reads a null-scheme resource through openResourceInputStream.
  • Base64 readAsStringAsync uses getInputStream, which supports URI-backed inputs but rejects the scheme-less drawable name.
  • copyAsync has a resource-aware null-scheme branch and can copy the packaged drawable to a regular file URI.

As a result, the failure is specific to the combination of Android production, a numeric require() asset, a scheme-less resolved resource name, and Base64 encoding.

Solution

This PR adds a shared readAsStringAsync(source, options) wrapper that accepts both normal URI strings and React Native image sources.

The wrapper resolves the source first and performs the Android resource-copy path only when all four known conditions are true:

platformEnv.isNativeAndroid &&
isNumber(source) &&
!uri.includes(':') &&
options?.encoding === 'base64'

Behavior by path:

  1. Normal URIs and all non-Base64 reads are passed directly to Expo FileSystem.
  2. An Android production require() drawable requested as Base64 is copied to the native cache directory with copyAsync.
  3. The copied file:// URI is read with Expo's Base64 reader.
  4. The temporary file is deleted with an idempotent, best-effort cleanup.

The temporary filename contains the Metro asset ID, timestamp, and random suffix. This prevents the main and background JS runtimes, which have separate JS heaps but share the native filesystem, from overwriting or deleting each other's in-flight files.

The existing required-image Base64 conversion now uses this wrapper, so callers receive the same raw Base64 content before the image helper adds the data:image/...;base64, prefix.

Scope and compatibility

  • Android URI-backed reads are unchanged.
  • Android non-Base64 bundled-resource reads continue using Expo's direct resource-aware path.
  • iOS, web, and desktop behavior is unchanged.
  • Non-Android errors are propagated directly rather than entering the Android fallback.
  • A repository-wide audit found no other production readAsStringAsync callers to migrate. The other Expo FileSystem integrations only write CSV files or create log upload tasks.

Validation

  • yarn agent:check --profile commit
    • worktree TypeScript lint
    • formatting
    • agent context
    • staged lint
    • staged TypeScript checks
  • Type-aware Oxlint and Oxfmt checks passed.
  • Built and installed an Android API 35 prodRelease APK through ADB.
  • Called the new API directly with require('./blank.png') and encoding: 'base64'.
  • Compared the entire returned Base64 string against the source PNG:
    • exact Base64 equality: true
    • source size: 74 bytes
    • Base64 length: 100
    • SHA-256: 2117bbb5576526b3ebfb7f54cfe783c30388558ae32116a282c75f96256ebf86

Related issue

@huhuanming huhuanming changed the title fix: read Android bundled assets as strings [OK-33404] fix: read Android bundled assets as strings Jul 19, 2026
@huhuanming
huhuanming marked this pull request as ready for review July 19, 2026 07:19
@huhuanming
huhuanming enabled auto-merge (squash) July 19, 2026 07:20
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.

1 participant