Skip to content

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

@GilHo-Roh

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions