Large video upload crashes on Android with CapacitorHttp enabled, but works on iOS

Hello,

I am experiencing a platform-specific issue when uploading large videos in an Ionic Angular application using Capacitor 7.

Environment

  • Ionic Angular
  • Angular 20
  • Capacitor 7.6.x
  • Android 13 physical device: Xiaomi Redmi Note 11 Pro
  • Android API 36 emulator
  • iPhone with iOS 18
  • Files are selected using:
<input
  type="file"
  accept="video/*"
  (change)="onVideoSelected($event)"
>

The selected video is received as a JavaScript File.

Upload flow

The application first requests a signed Google Cloud Storage URL from the backend:

const signedUrlResponse = await fetch(
  `${apiUrl}/api-v2/document/upload?filename=${file.name}&contentType=${file.type}`,
  {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
    },
  }
);

Then it uploads the file directly to Google Cloud Storage:

await fetch(uploadTarget.url, {
  method: 'PUT',
  headers: {
    'Content-Type': file.type,
  },
  body: file,
});

My Capacitor configuration contains:

{
  "plugins": {
    "CapacitorHttp": {
      "enabled": true
    }
  }
}

Behaviour on iOS

With CapacitorHttp enabled, the upload works correctly on iOS.

I successfully uploaded a video with the following size:

File: IMG_0005.mov
Type: video/quicktime
Size: 120,265,640 bytes
Size: 114.69 MB

The native HTTP request returns:

status: 200
ok: true

Behaviour on Android

With the same code and CapacitorHttp enabled, large video uploads cause an OutOfMemoryError or kill the WebView renderer.

On an Android emulator with API level 36 android 16, the logs show:

DIRECT STORAGE UPLOAD

followed by:

Renderer process crash detected
Render process kill (OOM or update)

On a Xiaomi Redmi Note 11 Pro running Android 13, the logs show:

OutOfMemoryError
Failed to allocate a large byte array

The crash happens before receiving the Google Storage response.

During my tests, the direct upload started becoming unstable at approximately 25–30 MB on Android.

Test without CapacitorHttp interception

When I remove:

"CapacitorHttp": {
  "enabled": true
}

The Android application no longer crashes.

But on iOS crache for a large direct upload to fail.

Legacy backend upload

I also have an older multipart upload endpoint:

const formData = new FormData();
formData.append('content', file, file.name);

await fetch(`${apiUrl}/api-v2/document`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
  },
  body: formData,
});

With CapacitorHttp disabled, this method works on Android for videos below approximately 50 MB.

Above 50 MB, the backend correctly returns:

413 Payload Too Large

Therefore, my temporary workaround is:

  • iOS: direct upload to Google Storage using CapacitorHttp.
  • Android videos below 50 MB: legacy multipart backend upload.
  • Android videos above 50 MB: ask the user to compress or split the video.

==> However, to do that, ChatGPT advised me to replace capacitor.config.json with capacitor.config.ts so I could enable CapacitorHttp on iOS and disable it on Android.

Questions

Is it possible to enable the global CapacitorHttp fetch patch only for iOS and disable it for Android?

Is there a recommended way to upload a large File on Android without copying the entire file through the JavaScript/native bridge?

Would @capacitor/file-transfer be the recommended solution for uploading a file URI directly to a Google Cloud Storage signed URL?

Is there a supported way to obtain the original Android content:// URI when the file is selected through an HTML file input?

My assumption is that the Android crash occurs because the large File body is copied or serialized in memory by CapacitorHttp, while iOS handles the same request differently.

Any advice or recommended architecture would be appreciated.

Thank you.

Yes. See Http Capacitor Plugin API | Capacitor Documentation. file-transfer is meant for large files.

Thank you, Tom, for your reply. I really appreciate your help.

My current flow is:

  • The user selects a video using a standard HTML input:

    <input type="file" accept="video/*">
    
  • This gives me a JavaScript File.

  • I request a signed Google Cloud Storage URL from my backend.

  • Then I upload the file directly to that signed URL.

On iOS this works correctly with CapacitorHttp, even for videos larger than 100 MB.

On Android, however, using CapacitorHttp causes an OutOfMemory crash with large videos. Without CapacitorHttp, the upload fails because of CORS on iOS.

My question is: how would you recommend using @capacitor/file-transfer in this scenario?

Since I currently only have a JavaScript File, not a native content:// or file:// URI, is there a recommended way to obtain a native file URI that can be passed to @capacitor/file-transfer?

Or would you recommend changing the file selection flow entirely?

Thank you!

Maybe use Capacitor File Picker Plugin for Android, iOS & Web - Capawesome to pick the file and upload?

Otherwise, maybe you’ll need to save the file to a tmp location using @capacitor/filesystem and then pass that location to @capacitor/file-transfer.

TBH, I haven’t worked with files so just my preliminary ideas.

Thank you very much, Tom. I’ll test it and then come back here to let you know whether it works.

Thank you very much, Tom.

Your suggestion was really helpful and pointed me in the right direction.

I ended up using the Capawesome File Picker together with the native file transfer approach for the video upload flow.

While integrating it, I also found an Android issue in the File Picker plugin when selecting media from Google Photos. Some ContentProviders do not expose COLUMN_LAST_MODIFIED, which caused the plugin to try to read cursor.getLong(-1).

I reported the issue here:

The Capawesome maintainer reproduced the problem and fixed it in the plugin. Since my project is still on Capacitor 7, I applied the fix locally using patch-package.

After that, the large video upload flow is now working correctly on Android as well.

Thanks again for taking the time to answer my post and for suggesting this direction, it really helped me solve the issue.