cordova-plugin-advanced-http icon indicating copy to clipboard operation
cordova-plugin-advanced-http copied to clipboard

[Bug] [iOS] formData with blob is not sending http request

Open indraraj26 opened this issue 5 years ago • 24 comments

Describe the bug It is not sending any request when i am sending blob but it is working fine when i am just sending text like this form.append("name", "indraraj")

System info

  • affected HTTP plugin version: [e.g. 2.3.1] HTTP : 2.3.1
  • affected platform(s) and version(s): [e.g. iOS 12.2] iOS: 12.1
  • affected device(s): [e.g. iPhone 8] iPAD 5th generation
  • cordova version: [e.g. 6.5.0] cordova
  • cordova platform version(s): [e.g. android 7.0.0, browser 5.0.3]

Are you using ionic-native-wrapper?

  • ionic-native-wrapper version: [e.g. 5.8.0] No
  • did you check ionic-native issue tracker for your problem? Yes Capacitor CLI : 1.3.0 @capacitor/core : 1.4.0

Minimum viable code to reproduce If applicable, add formatted sample coding to help explain your problem.

e.g.:

import { HTTP } from "@ionic-native/http/ngx";
declare const cordova: any;

 const form = new cordova.plugin.http.ponyfills.FormData()
    form.append('profile_pic', customerDetail.profile_pic);
    form.append('first_name', customerDetail.first_name);
    form.append('mobile', customerDetail.mobile);
    console.log(form, "formDataforFile")
    this._http.setDataSerializer("multipart");
    return this._http.post(
      `${environment.apiUrl}/saveWalkForm`,
      form,
      { "content-type": "application/json" }
    );

console before sending actual request

FormData

__items: Array (3)
0 Array (2)
0 "profile_pic"
1 Blob

lastModifiedDate: Thu Jan 23 2020 19:47:12 GMT+0530 (IST)

name: ""

size: 496658

type: "image/jpeg"

Blob Prototype

Array Prototype
1 Array (2)
0 "first_name"
1 "dfff"

Array Prototype
2 Array (2)
0 "mobile"
1 "3434343455"

Array Prototype

Array Prototype

FormData Prototype

Screenshots If applicable, add screenshots to help explain your problem. ionic info


Ionic:

   Ionic CLI                     : 5.2.3 (/usr/local/lib/node_modules/ionic)
   Ionic Framework               : @ionic/angular 4.11.8
   @angular-devkit/build-angular : 0.801.3
   @angular-devkit/schematics    : 8.1.3
   @angular/cli                  : 8.1.3
   @ionic/angular-toolkit        : 2.0.0

Capacitor:

   Capacitor CLI   : 1.3.0
   @capacitor/core : 1.4.0

Cordova:

   Cordova CLI       : 8.1.2 ([email protected])
   Cordova Platforms : none
   Cordova Plugins   : no whitelisted plugins (0 plugins total)

Utility:

   cordova-res : not installed
   native-run  : 0.2.8 (update available: 0.3.0)

System:

   ios-deploy : 1.9.4
   ios-sim    : 8.0.2
   NodeJS     : v12.13.0 (/usr/local/bin/node)
   npm        : 6.12.0
   OS         : macOS High Sierra
   Xcode      : Xcode 10.1 Build version 10B61

indraraj26 avatar Jan 23 '20 14:01 indraraj26

Hi indraraj, please add a „.catch()“ handler in your code and check which error message you receive. And I see that you‘re overriding the multipart mimetype. You are basically telling your server that you gonna transmit a JSON object, but you do transmit a multipart request. This will definitely fail on the server side. Did you also check server logs? Please use StackOverflow for this kind of questions.

silkimen avatar Jan 24 '20 00:01 silkimen

Hi silkimen,

When you add blob to request it is not making any request we are temporary logging all the request and saving in temp column, But with blob it is not making any request to backend. I already have catch block i just don't get any error.

Okay btw let me remove the header part { "content-type": "application/json" } and test

Edit: Tested with blob and changed content-type to multipart it is not making any request, enabled the debug level at Apache. Also I had catch block so there i had no error.

indraraj26 avatar Jan 24 '20 05:01 indraraj26

Okay, I see. I've tried to reproduce your problem on my device (iPhone XR running iOS 13.2) and also on a simulator (iPad 5th generation running iOS 12.4). I'm using the e2e specs suite which you can find here: test/e2e-specs.js lines 802-847 being the relevant ones. The tests are running without any problems. But I don't have an iPad with iOS 12.1 available. Did you also try to pass the third argument during form.append()? It's the filename argument.

silkimen avatar Jan 27 '20 00:01 silkimen

I see you have this in code value.name = filename || ''; as per this it is not compulsory to pass file name. Anyway let me pass filename as well and test. I have tested in ipad 5th generation simulator it is failed to make a request with backend. For the moment i have used upload file interface combine with json post to deliver the project. I may close this issue after testing with filename as it is not reproducible at your side.

indraraj26 avatar Jan 27 '20 05:01 indraraj26

Okay, that‘s interesting. Did you try to run exactly the code from my e2e specs? I‘d like to know what‘s going wrong here.

silkimen avatar Jan 27 '20 22:01 silkimen

I am facing similar issue,

I followed the same procedure which is in test/e2e-specs.js. I am trying to upload images to aws s3, using blob.

Xcode debugger crashes throwing error NSInvalidArgumentException

sarathi0333 avatar Jan 31 '20 14:01 sarathi0333

@sarathi0333 Can you please attach a Xcode screenshot? I'd like to know where exactly this problem occurs.

silkimen avatar Feb 02 '20 16:02 silkimen

Please find below the screenshots.

error

sarathi0333 avatar Feb 03 '20 05:02 sarathi0333

@sarathi0333 Unfortunately I can't reproduce your problem. Please attach an extract of your JS code. And please try to run EXACTLY the code you can find in the e2e specs file (running against httpbin.org). I'd like to find out if it's related to something received from the server. Which device are you using? Which iOS version?

silkimen avatar Mar 04 '20 20:03 silkimen

Same problem. API works from Postman but not in angular. The plugin seems not working when a try to make a put request in multipart and formData. It never return any response or error...

Pericle82 avatar Apr 15 '20 16:04 Pericle82

Hi guys, I have the same issue and have traced it to the FormData.append method from new window.top.cordova.plugin.http.ponyfills.FormData().

Some context - I am overriding the uploadData method of dropzone to work using cordova-plugin-advanced-http on my capacitor app.

Regardless of whether I am sending a blob or file object, the append method receives only the type string, e.g. "[Object blob]", so is checking if "[Object blob]" instanceof "Global.Blob", which clearly it isn't.

This returns it as an unknown type and so is rejected as an incorrect data type for dataSerializer multipart.

A sample of my code:

dz_uploadData = function(files, dataBlocks) {
    var _this16 = this;

    var url = this.resolveOption(this.options.url, files);

    var formData = new window.top.cordova.plugin.http.ponyfills.FormData();
    var dto = {
      uuid: files[0].upload.uuid,
      name: files[0].name,
      chunkIndex: dataBlocks[0].chunkIndex,
      totalChunkCount: files[0].upload.totalChunkCount
    };

    for (var key in dto) {
      var value = dto[key];
      formData.append(key, value);
    }

    for (var i = 0; i < dataBlocks.length; i++) {
      var dataBlock = dataBlocks[i];
      formData.append(dataBlock.name, dataBlock.data, dataBlock.filename);
    }

    window.top.cordova.plugin.http.setDataSerializer("multipart");
    window.top.cordova.plugin.http.setRequestTimeout(extendedTimeout);
    let requestOptions = {
      data: formData,
      headers: {
        "Content-Type": "multipart/form-data;"
      }
    };
    window.plenty_admin.REST.postOne(
      url,
      requestOptions,
      function(response) {
        window.top.cordova.plugin.http.setDataSerializer("json");
        window.top.cordova.plugin.http.setRequestTimeout(standardTimeout);
        console.log(response);
        _this16._finishedUploading(files, xhr, e);
      },
      function(error) {
        window.top.cordova.plugin.http.setDataSerializer("json");
        window.top.cordova.plugin.http.setRequestTimeout(standardTimeout);
        console.error(error);
        _this16._handleUploadError(files, xhr);
      }
    );
  }; 

I hope this can help to identify the problem? Thanks for your help once again silkimen. Jamie

--EDIT--

The reason for this comes from the FormData.append ponyfill for sure:

FormData.prototype.append = function(name, value, filename) {
    if (global.File && value instanceof global.File) {
      // nothing to do
    } else if (global.Blob && value instanceof global.Blob) { // it is an instance of Blob, not Global.Blob...
      // mimic File instance by adding missing properties
      value.lastModifiedDate = new Date();
      value.name = filename || '';
    } else {
      value = String(value);
    }

    this.__items.push([ name, value ]);
  };

The patch I have working for this file is:

FormData.prototype.append = function(name, value, filename) {
    if (global.File && value instanceof global.File) {
      // nothing to do
    } else if (global.Blob && value instanceof global.Blob || value.constructor.name === "Blob") {
      // mimic File instance by adding missing properties
      value.lastModifiedDate = new Date();
      value.name = filename || '';
    } else {
      value = String(value);
    }

    this.__items.push([ name, value ]);
  };

plugins/cordova-plugin-advanced-http/www/helpers.js also needs patching to replace:

if (entry.value[1] instanceof global.Blob || entry.value[1] instanceof global.File) {
      var reader = new global.FileReader();

      reader.onload = function() {
        result.buffers.push(base64.fromArrayBuffer(reader.result));
        result.names.push(entry.value[0]);
        result.fileNames.push(entry.value[1].name || 'blob');
        result.types.push(entry.value[1].type || '');
        processFormDataIterator(iterator, textEncoder, result, onFinished);
      };

      return reader.readAsArrayBuffer(entry.value[1]);
    }

with

if (
      entry.value[1] instanceof global.Blob  ||
      entry.value[1].constructor.name === "Blob" ||
      entry.value[1] instanceof global.File ||
      entry.value[1].constructor.name === "File"
      ) {
      var reader = new global.FileReader();

      reader.onload = function() {
        result.buffers.push(base64.fromArrayBuffer(reader.result));
        result.names.push(entry.value[0]);
        result.fileNames.push(entry.value[1].name || 'blob');
        result.types.push(entry.value[1].type || '');
        processFormDataIterator(iterator, textEncoder, result, onFinished);
      };

      return reader.readAsArrayBuffer(entry.value[1]);
    }

If anyone can explain why instanceof no longer works to identify the object I would appreciate an explanation! Cheers Jamie

JamieMcDonnell avatar May 01 '20 07:05 JamieMcDonnell

I'm running into the same issue. Here is some more information:

FormData Append Code Logs Promise Fulfills Server Receives
formData.append('file', new Blob([fileContent]), fileName); Nothing Never Nothing
formData.append('file', new Blob([fileContent])); Nothing Never Nothing
formData.append('file', new File([fileContent], fileName)); Nothing Yes String (pasted below)
formData.append('file', new File([fileContent], fileName), fileName); Yes (pasted below) Never Nothing
formData.append('file', new File([fileContent], fileName, {type: 'application/octet-stream'})); Nothing Yes String (pasted below)
formData.append('file', new File([fileContent], fileName, {type: 'text/html'})); Nothing Yes String (pasted below)

fileContent is just a plain UTF-8 string.

Expected POST body:

--00content0boundary00
Content-Disposition: form-data; name="file"; filename="fileName.html"
Content-Type: application/octet-stream

<p>File Content</p>
--00content0boundary00--

None send File with filename and content other than [object Object]. Seems to be interpretting Files as strings.

formData.append('file', new File([fileContent], fileName)); POST body:

--00content0boundary00
Content-Disposition: form-data; name="file"

[object Object]
--00content0boundary00--

My fileContent string does not contain "[object Object]" lol

formData.append('file', new File([fileContent], fileName), fileName); Exception:

2020-05-10 18:08:52.174 20406-20406/io.ionic.starter E/Capacitor/Console: File: http://localhost/vendor-es2015.js - Line 43427 - Msg: ERROR Error: Uncaught (in promise): TypeError: Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'.
    TypeError: Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'.
        at SynologyService.<anonymous> (http://localhost/main-es2015.js:1577:22)
        at Generator.next (<anonymous>)
        at http://localhost/polyfills-es2015.js:3200:71
        at new ZoneAwarePromise (http://localhost/polyfills-es2015.js:4226:29)
        at Module.__awaiter (http://localhost/polyfills-es2015.js:3196:12)
        at SynologyService.upload (http://localhost/main-es2015.js:1550:63)
        at HomePage.<anonymous> (http://localhost/main-es2015.js:1121:37)
        at Generator.next (<anonymous>)
        at http://localhost/polyfills-es2015.js:3200:71
        at new ZoneAwarePromise (http://localhost/polyfills-es2015.js:4226:29)

I get that append taking fileName third is for Blobs and not Files but just trying everything here.

formData.append('file', new File([fileContent], fileName, {type: 'application/octet-stream'})); POST body:

--00content0boundary00
Content-Disposition: form-data; name="file"

[object Object]
--00content0boundary00--

formData.append('file', new File([fileContent], fileName, {type: 'text/html'})); POST body:

--00content0boundary00
Content-Disposition: form-data; name="file"

[object Object]
--00content0boundary00--

How I'm POSTing

let formData = new FormData();
formData.append('file', ...);

this.mobileHttp.setDataSerializer('multipart');
return this.mobileHttp.post(url, formData, {})

The same exact FormData can instead be passed to Angular's HttpClient and it will serialize and POST to the server just fine. I think the issue is in how the FormData is serialized when using advanced-http. I tried serializing it myself but then I ran into other issues.

System info

  • "@ionic-native/http": "^5.25.0"
  • "cordova-plugin-advanced-http": "^2.4.1"
  • affected platform(s) and version(s): Android 10 (Build number: QQ2A.200405.005)
  • affected device(s): Google Pixel 3
  • cordova version: "@capacitor/android": "^2.0.1", "@capacitor/core": "2.0.1",
  • cordova platform version(s): minSdkVersion = 21 compileSdkVersion = 29 targetSdkVersion = 29 androidxAppCompatVersion = '1.1.0' androidxCoreVersion = '1.2.0' androidxMaterialVersion = '1.1.0-rc02' androidxBrowserVersion = '1.2.0' androidxLocalbroadcastmanagerVersion = '1.0.0' firebaseMessagingVersion = '20.1.2' playServicesLocationVersion = '17.0.0' junitVersion = '4.12' androidxJunitVersion = '1.1.1' androidxEspressoCoreVersion = '3.2.0' cordovaAndroidVersion = '7.0.0'

RomanRobot avatar May 10 '20 22:05 RomanRobot

I wasn't able to get it working tonight but I got a bit further by stepping through things. One of the issues was that I was using FormData. It passes the dependencyValidator.checkFormDataInstance(data); check in processFormData because it has an entries function, but the iterator that it returns reads the File as a string. So when it does the instanceof check here it's already a string type with the value [object Object]. To fix that I switched to the ponyfills.FormData mentioned in the documentation, even though the FormData I was using already had an entries function.

declare var cordova;
...
new cordova.plugin.http.ponyfills.FormData()

The issue I am running into now is that when I am debugging, the File constructor from cordova-plugin-file is different from the standard one. It doesn't accept the file content as a string.

/**
 * Constructor.
 * name {DOMString} name of the file, without path information
 * fullPath {DOMString} the full path of the file, including the name
 * type {DOMString} mime type
 * lastModifiedDate {Date} last modified date
 * size {Number} size of the file in bytes
 */

var File = function (name, localURL, type, lastModifiedDate, size) {
    this.name = name || '';
    this.localURL = localURL || null;
    this.type = type || null;
    this.lastModified = lastModifiedDate || null;
    // For backwards compatibility, store the timestamp in lastModifiedDate as well
    this.lastModifiedDate = lastModifiedDate || null;
    this.size = size || 0;

    // These store the absolute start and end for slicing the file.
    this.start = 0;
    this.end = this.size;
};

So I tried passing the file as a Blob again

formData.append('file', new Blob([fileContent]), fileName);

or

formData.append('file', new Blob([fileContent], { type: 'text/html' }), fileName);

or

const textEncoder = new TextEncoder();
formData.append('file', new Blob([textEncoder.encode(fileContent).buffer]), fileName);

but FileReader.onload is never triggered in processFormDataIterator when it's a Blob type so it doesn't push the file entry for serialization.

While debugging I noticed that window.File has that weird constructor I pasted above but window.Blob doesn't have a constructor. Not sure if that has to do with it but that is as far as I got tonight. Empty Blob constructor

RomanRobot avatar May 13 '20 03:05 RomanRobot

Same problem for me on Android. I'm currently unable to send a POST request with a Blob or File in the formData. No request is sent to the server (I also checked Apache logs) and the Promise remains pending without throwing any error.

I searched for days how to make it works but I finally surrendered, it seems it is impossible to send a file with this Plugin.

Anyone else facing the same issue ? Does someone has a workaround ?

BenjaminDish avatar Jun 09 '20 14:06 BenjaminDish

I am also facing the same issue for the iOS with sending the multipart request with Blob object

HardikDG avatar Jun 23 '20 14:06 HardikDG

@RomanRobot I've checked your app, but that's not what I meant as MVP. 😅

I have a similar issue, but in my case the Blob entry I appended to FormData is already a string type by the time it gets to that check.

Please check MDN reference of FormData/append. It says:

value The field's value. This can be a USVString or Blob (including subclasses such as File). If none of these are specified the value is converted to a string.

So, I think your Blob instance is not recognized as a Blob. That could be the reason why it's serialized as string representation and not read correctly by FileReader in helpers.js#L443. The question is why?

Do you have set up some polyfills or alternative implementations of Blob or FileReader?

silkimen avatar Jun 25 '20 04:06 silkimen

@silkimen I believe this is due to an ongoing issue with ionic/capacitor apps and Angular's Zone library. I implemented this suggested solution in your library, and it fixed my issue. Please let me know if you would like me to submit a PR. Thanks

Edit: The fix actually needs to listen to onloadend as well, instead of onload. Otherwise the file result doesn't get saved.

bsbechtel avatar Jul 02 '20 12:07 bsbechtel

@bsbechtel How would someone apply this fix themselves? I tried updating the code in node_modules\cordova-plugin-advanced-http\www\helpers.js to

...
var reader = new global.FileReader();
const zoneOriginalInstance = (reader as any)["__zone_symbol__originalInstance"];
reader = zoneOriginalInstance || reader;

reader.onloadend = function() {
  result.buffers.push(base64.fromArrayBuffer(reader.result));
  ...

but while it compiles, I seem to get this in my output window after submitting any HTTP requests:

io.ionic.starter W/Capacitor/Console: File: http://localhost/vendor-es2015.js - Line 103956 - Msg: Native: tried calling HTTP.get, but Cordova is not available. Make sure to include cordova.js or run in a device/simulator
io.ionic.starter E/Capacitor/Console: File: http://localhost/vendor-es2015.js - Line 43427 - Msg: ERROR Error: Uncaught (in promise): cordova_not_available

If I revert those changes to helpers.js it goes back to working just as it was, submitting HTTP requests just fine, just the formData POST bug.

It's my first time trying to change a library like this. New to Angular.


Something to note, while I am excited to potentially have a working solution to this issue I'm worried about some reports of performance issues reported below that solution you linked:

  1. https://github.com/ionic-team/capacitor/issues/1564#issuecomment-567857794
  2. https://github.com/ionic-team/capacitor/issues/1564#issuecomment-623529277

RomanRobot avatar Jul 28 '20 01:07 RomanRobot

Also, to answer your question @silkimen

Do you have set up some polyfills or alternative implementations of Blob or FileReader?

I don't think so. The only one I see is the default one in the src folder generated by Ionic I think it was. It just contains

import './zone-flags';
import 'zone.js/dist/zone';

and a bunch of comments.

RomanRobot avatar Jul 28 '20 01:07 RomanRobot

@RomanRobot I ended up overwriting the prototype method in a bit of a hacky way until the library can be updated. I put this in main.ts:

const originalReadAsArrayBufferMethod = FileReader.prototype.readAsArrayBuffer;

FileReader.prototype.readAsArrayBuffer = function(args) {
  const originalFileReader = this['__zone_symbol__originalInstance'];
  if (originalFileReader) {
    originalFileReader.onloadend = () => {
      this.result = originalFileReader.result;
      this.onload();
    }
    originalFileReader.readAsArrayBuffer(args);
  } else {
    originalReadAsArrayBufferMethod(args);
  }
}

bsbechtel avatar Jul 28 '20 12:07 bsbechtel

@bsbechtel Freakin' awesome! That got me unblocked on that finally. Thank you!

RomanRobot avatar Jul 28 '20 13:07 RomanRobot

@bsbechtel thanks for the hacky way after I had spent days on it and saw this eventually. It deserves a PR.

greatwhiz avatar Aug 22 '21 10:08 greatwhiz

I'm having the same issue with Ionic + Capacitor + Angular on an Android device. Sending FormData works well but the second I add a Blob to the FormData, the http request never gets send and i have no error.

maudvd avatar Mar 02 '22 10:03 maudvd