Quick answer: On Android, StreamingAssets live inside the APK (a zip), not on the filesystem. Standard .NET file APIs do not work. Use UnityWebRequest.Get(Application.streamingAssetsPath + "/file") and yield on the request.

Here is how to fix Unity StreamingAssets not loading on Android. In the editor you call File.ReadAllBytes(Application.streamingAssetsPath + "/config.json") and it works. You build an APK, install it, launch it, and the file read throws FileNotFoundException. The file is definitely in the APK — you can extract the APK with unzip and see it. The problem is that Android’s filesystem view of StreamingAssets is not a real filesystem. It is a path inside a zip archive, and only Unity’s own I/O layer knows how to read it.

The Symptom

On Android, operations like File.ReadAllText, File.ReadAllBytes, File.Exists, or StreamReader(path) throw FileNotFoundException or IOException when pointed at a StreamingAssets path. The exception message includes a path that looks like jar:file:///data/app/com.studio.game/base.apk!/assets/config.json. Everything works in the editor, everything works on Standalone builds, everything works on iOS — just not Android.

Sometimes a more subtle variant: File.Exists returns false for a file you can see inside the APK, so you assume the build left it out. The file is there; the API just cannot see it.

What Causes This

Android packages StreamingAssets inside the APK. On Android, the APK is essentially a zip file. Everything in your StreamingAssets folder ends up inside base.apk/assets/. There is no disk-level file at /storage/sdcard/Android/data/... for each asset. The path you get from Application.streamingAssetsPath on Android is a special Java-style URI (jar:file://) that only Android’s resource APIs can open.

.NET File APIs do not understand jar URIs. They expect /path/to/file style paths. When handed a jar:file:// URI, they treat the whole thing as a literal filename, find no matching file, and throw.

Only UnityWebRequest (and a few other Unity APIs) handle the jar path. Unity provides UnityWebRequest specifically to abstract this platform difference. It also handles ContentProvider lookups, asset decompression, and the platform-specific plumbing.

APK compression. On some Unity versions and some file extensions, Android may compress StreamingAssets inside the APK. Compressed entries cannot be read as raw files even with the correct path; they must go through a decompression-aware API. UnityWebRequest handles this too.

The Fix

Step 1: Replace File.ReadAllBytes with UnityWebRequest. Use a coroutine to await the load asynchronously.

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class ConfigLoader : MonoBehaviour
{
    IEnumerator Start()
    {
        string path = System.IO.Path.Combine(
            Application.streamingAssetsPath, "config.json");

        using (var req = UnityWebRequest.Get(path))
        {
            yield return req.SendWebRequest();

            if (req.result == UnityWebRequest.Result.Success)
            {
                string json = req.downloadHandler.text;
                Debug.Log($"Config loaded: {json.Length} chars");
            }
            else
            {
                Debug.LogError($"Config load failed: {req.error}");
            }
        }
    }
}

This works on every platform: Android reads from the APK, iOS reads from the bundle, desktop reads from disk. UnityWebRequest does the right thing for each.

Step 2: Use async/await if your code base uses it. UnityWebRequestAsyncOperation works with Unity’s awaitable extensions.

public async Task<string> LoadConfigAsync()
{
    string path = System.IO.Path.Combine(
        Application.streamingAssetsPath, "config.json");

    using var req = UnityWebRequest.Get(path);
    await req.SendWebRequest();

    if (req.result != UnityWebRequest.Result.Success)
        throw new Exception(req.error);

    return req.downloadHandler.text;
}

You may need a small extension method for UnityWebRequestAsyncOperation.GetAwaiter; the UnityEngine docs have a canonical implementation.

Step 3: Write a cross-platform wrapper. Keep one helper that encapsulates the platform check. Callers should not have to worry about Android vs. desktop.

public static class StreamingAssetsLoader
{
    public static IEnumerator Load(string relativePath,
        System.Action<byte[]> onLoaded,
        System.Action<string> onError = null)
    {
        string path = System.IO.Path.Combine(
            Application.streamingAssetsPath, relativePath);

        using (var req = UnityWebRequest.Get(path))
        {
            yield return req.SendWebRequest();

            if (req.result == UnityWebRequest.Result.Success)
                onLoaded?.Invoke(req.downloadHandler.data);
            else
                onError?.Invoke(req.error);
        }
    }
}

Step 4: Consider moving to Addressables. If you have many runtime assets, Addressables handles the StreamingAssets problem for you (and provides remote delivery, async loading, and memory management). StreamingAssets was the go-to pattern circa 2018; Addressables is the modern approach.

What About Write Access?

StreamingAssets is read-only everywhere. You cannot write to it at runtime on any platform. For files users or your game modify at runtime, use Application.persistentDataPath. A common pattern: ship defaults in StreamingAssets, copy them to persistentDataPath on first launch, read and write from persistentDataPath thereafter.

string destPath = Path.Combine(Application.persistentDataPath, "config.json");

if (!File.Exists(destPath))
{
    // Copy from StreamingAssets via UnityWebRequest
    yield return StreamingAssetsLoader.Load("config.json",
        data => File.WriteAllBytes(destPath, data));
}

// From now on, read/write directly with File APIs
string json = File.ReadAllText(destPath);

Debugging on Device

If UnityWebRequest still fails on device, check logcat for the exact error. Common errors: Cannot resolve destination host means the path is being treated as a URL (double-check that you used Path.Combine with the streamingAssetsPath correctly), Permission denied means the APK is damaged or resigned, and 404 Not Found means the file is not in the APK at all — re-check Build Settings for asset inclusion.

“On Android, StreamingAssets are inside a zip. No amount of File API usage changes that. UnityWebRequest is not a workaround — it is the API.”

Related Issues

For general Android build issues, see Unity Build Crashes on Android. For asset bundle and addressables patterns, Unity Addressables Failed to Load covers alternative asset delivery.

UnityWebRequest everywhere. No more File.ReadAllBytes on StreamingAssets.