Filesystem readFile convert to blob, not uploading to S3

I have a Capacitor application, which I am taking an Image from the camera, and uploading as a Blob to S3.

When using the “web” platform and thus the “webPath”, the upload works fine.

When using a device (Android in my case) and converting the “filePath” to a Blob, the Blob won’t upload, with AWS returning an error.

Is there something wrong with the Blob I am creating from the “filePath”?

export async function uploadImageToAws(fileUri, fileName) {

    const blob = fileUri.startsWith('blob') 
                                            ? await convertImageWebPathToBlob(fileUri)
                                            : await convertImagePathToBlob(fileUri);

    const formData = new FormData();
    formData.append('bucket', s3URI);
    formData.append('key', fileName);
    formData.append('Content-Type', 'image/jpeg');
    formData.append('AWSAccessKeyId', awsKey);
    formData.append('Policy', policyBase64);
    formData.append('Signature', signature);
    formData.append('file', blob, fileName);

    const options = {url: s3URI, data: formData};
    
    return await CapacitorHttp.post(options);
}

async function convertImageWebPathToBlob(imageWebPath) {
  const response = await fetch(imageWebPath);
  return await response.blob();
}
async function convertImagePathToBlob(imagePath) {
  var file = await Filesystem.readFile({ path: imagePath })

  const rawData = atob(file.data);
  const bytes = new Array(rawData.length);
  for (let x = 0; x < rawData.length; x++) {
    bytes[x] = rawData.charCodeAt(x);
  }
  const arr = new Uint8Array(bytes);
  return new Blob([arr], { type: 'image/jpeg' });
}

Again, the Blob created from “convertImageWebPathToBlob” uploads perfectly.

The Bloc created from “convertImagePathToBlob” does not upload, and Amazon returns the error:

<Error>
    <Code>PreconditionFailed</Code>
    <Message>At least one of the pre-conditions you specified did not hold</Message>
    <Condition>Bucket POST must be of the enclosure-type multipart/form-data</Condition>
    <RequestId>J5MS6JMB128DJQ</RequestId>
    <HostId>Hpp1bKq104X24wwkwplS6dUNg1/G17LoJfPT5+/Sudg/sXni2q/rI=</HostId>
</Error>

Try setting dataType in the HttpOptions to formData - Http Capacitor Plugin API | Capacitor Documentation

It would be working on web because the Capacitor HTTP plugin would be using web APIs. On native, it would be using native APIs which it does some manipulation to the request. Is there a reason you are using Capacitor HTTP instead of just the web APIs like XMLHttpRequest or fetch?

1 Like

Thank you for the response, I have just tried setting the dataType in the HttpOptions as suggested, but still the same issue.

I perhaps should have mentioned, when using an Android device, I can pass the image.webPath to the uploadImageToAws (instead of image.path) and this uploads perfectly fine too.

However, if I pass the image.path and attempt to create a Blob from the local Filesystem, then I get the error.

I have tested and the base64 being returned from file.data

var file = await Filesystem.readFile({ path: imagePath })

Is valid and reconstructs my image when I upload it to a Base64 converter (e.g. Base64 to Image | Base64 Decode | Base64 Converter | Base64)

Which leads me to believe there is a difference in the two Blobs, is there anything else I can try?

Is there a reason you are using Capacitor HTTP instead of just the web APIs like XMLHttpRequest or fetch?

There is no particular reason, when starting the Capacitor project I browsed through the plugins and thought it would be better to use it than not.

My mantra is only jump to the native layer when absolutely necessary. Using the Capacitor HTTP plugin is going to be slower because it has to convert data between native and web.

Otherwise, sadly, I am not sure on your issue. Hopefully someone else can chime in. I would though post a full example of your code.

1 Like

@kirkhawksworthchecke Why don’t you also use fetch(...) on Android and iOS?

Example:

import { Filesystem } from '@capacitor/filesystem';
import { Capacitor } from '@capacitor/core';

const getFileAsBlob = async () => {
  const webPath = Capacitor.convertFileSrc(”file:///…”); 
  const response = await fetch(webPath);
  return response.blob();
};

Check out this File Handling Guide for more information.

2 Likes

@robingenz

Thank you for the response, that is indeed helpful. I wasn’t aware of the Capacitor.convertFileSrc method, which does make handling the different source types far easier.

@twestrick

Using the Capacitor HTTP plugin is going to be slower

I hadn’t appreciated that, I assumed using Web would be slower, as the Web would ultimately have to call native. I have changed my code to use XMLHttpRequest

I have fixed my issue and both of your answers pointed me in the correct direction.

Unfortunately when I stated in my earlier response that the “image.webPath” uploads correctly from the Android device, I was mistaken. Images didn’t load in either webPath or path on the device.

It seems the native version wasn’t uploading because the following doesn’t get set on the request header;

Content-Type", "multipart/form-data; boundary=----------------

However, on the upload from the web version, that does get set.

I chnaged my code to this:

  const xhr = new XMLHttpRequest();
  xhr.open('POST', s3URI, true);
  const formData = new FormData();

  formData.append('bucket', s3URI);
  formData.append('key', fileName);
  formData.append('acl', 'public-read');
  formData.append('Content-Type', 'image/jpeg');

  formData.append('AWSAccessKeyId', awsKey);
  formData.append('Policy', policyBase64);
  formData.append('Signature', signature);
  formData.append('file', blob, fileName);

  if (platform != 'web') {
    formData.append("enctype", "multipart/form-data");
    xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------1442673354144381250634699865");
  }
  
  xhr.onload = function () {
    console.log(this.status)
    console.log(this.response)
  };
  xhr.send(formData);

And it uploads images either native or via web.

I am not currently certain why multipart/form-data; boundary=--- is required to be manually set on native, but not on web.

Also whether it is proper to hard code the boundary as I have done here (or whether the boundary requires setting based on the context of the data being uploaded).

Those aren’t questions specific to Capacitor/Ionic however, so I will mark this as solved, and thanks again for your help.

I am of course happy to be educated if someone does know why the boundary needs to be handled in this way.

1 Like

From my quick searching, it seems the boundary can be anything. I would guess the browser handles this for you whereas doing a XMLHttpRequest, the developer needs to specify.

Resources

1 Like

Also, I use the Axios library in my app (it adds a little more sugar on top of XMLHttpRequest).

It supports Multipart Bodies and looking at the source code appears to generate the boundary for you if not specified.

1 Like

Note the “note” for the Filesystem API:

The representation of the data contained in the file
Note: Blob is only available on Web. On native, the data is returned as a string.

I was recommended to check the Capawesome blog for some tips, they have some special plugins.

Currently looking into this, as I want to read the files as binary, as they need to be decrypted first. Then I want to feed the decrypted data to a PDF viewer. It’s gonna be fun :wink:

1 Like