Skip to content

iOS: file:// URI paths not percent-decoded in RNFAppleFilamentProxy, causing silent load failure #335

Description

@GilHo-Roh

Bug Description

RNFAppleFilamentProxy.mm strips the file:// prefix from URIs using substringFromIndex:7 but does not percent-decode the remaining path. This causes silent asset loading failures on iOS production/release builds when the file path contains spaces (encoded as %20).

Root Cause

In RNFAppleFilamentProxy.mm, the file:// handling code:

if ([filePath hasPrefix:@"file://"]) {
    filePath = [filePath substringFromIndex:7];  // strips "file://" but leaves %20 intact
    NSData* bufferData = [NSData dataWithContentsOfFile:filePath ...];
}

Image.resolveAssetSource() returns URL-encoded file:// URIs in iOS production builds. The iOS app data path includes Application Support (with a space), which becomes Application%20Support in the URI:

file:///var/mobile/Containers/Data/Application/.../Library/Application%20Support/ExponentExperienceData/...

After stripping file://, the path still contains %20, so NSData dataWithContentsOfFile: receives a path with literal %20 — which doesn't exist on the filesystem. The load fails silently (no error propagated to JS).

Impact

  • All assets loaded via require() + useBuffer/useModel fail silently on iOS production when the resolved path passes through Application Support (which is the standard Expo/React Native data directory)
  • Works fine in __DEV__ because Metro serves assets via HTTP URLs (no file:// path involved)
  • Affects .glb, .filamat, and any other asset types loaded through FilamentProxy.loadAsset()

Reproduction

  1. Use Expo managed workflow with react-native-filament
  2. Load a .filamat or .glb file via require():
    const material = require('./assets/material.filamat');
    const buffer = useBuffer({ source: material });
  3. Works in development (npx expo start)
  4. Build a release/production IPA (eas build)
  5. Asset fails to load — useBuffer never resolves, useModel stays in loading state

Suggested Fix

Replace manual string slicing with proper NSURL path extraction, which automatically handles percent-decoding:

// Before (bug)
if ([filePath hasPrefix:@"file://"]) {
    filePath = [filePath substringFromIndex:7];
}

// After (fix) - Option A: NSURL (idiomatic)
if ([filePath hasPrefix:@"file://"]) {
    NSURL* fileURL = [NSURL URLWithString:filePath];
    filePath = [fileURL path];  // automatically percent-decodes
}

// After (fix) - Option B: explicit decoding
if ([filePath hasPrefix:@"file://"]) {
    filePath = [[filePath substringFromIndex:7] stringByRemovingPercentEncoding];
}

Current JS Workaround

We are currently working around this in JS by manually decoding the URI before passing it:

const source = useMemo(() => {
  if (__DEV__) return MaterialAsset;
  const resolved = Image.resolveAssetSource(MaterialAsset);
  if (resolved?.uri) {
    return { uri: decodeURIComponent(resolved.uri) };
  }
  return MaterialAsset;
}, []);

Related

Environment

  • react-native-filament: 1.9.0
  • Expo SDK: 52
  • iOS: 18.x
  • React Native: 0.76.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions