Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
indraraj26 opened this issue Jan 23, 2020 · 24 comments
Open

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

indraraj26 opened this issue Jan 23, 2020 · 24 comments

Comments

@indraraj26
Copy link

indraraj26 commented Jan 23, 2020

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
@silkimen
Copy link
Owner

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.

@indraraj26
Copy link
Author

indraraj26 commented Jan 24, 2020

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.

@silkimen
Copy link
Owner

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.

@indraraj26
Copy link
Author

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.

@silkimen
Copy link
Owner

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.

@sarathi0333
Copy link

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

@silkimen
Copy link
Owner

silkimen commented Feb 2, 2020

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

@sarathi0333
Copy link

Please find below the screenshots.

error

@silkimen
Copy link
Owner

silkimen commented Mar 4, 2020

@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?

@Pericle82
Copy link

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...

@JamieMcDonnell
Copy link

JamieMcDonnell commented May 1, 2020

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

@RomanRobot
Copy link

RomanRobot commented May 10, 2020

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
Copy link

RomanRobot commented May 13, 2020

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

@BenjaminDish
Copy link

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 ?

@HardikDG
Copy link

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

@silkimen
Copy link
Owner

@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?

@bsbechtel
Copy link

bsbechtel commented Jul 2, 2020

@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.

@RomanRobot
Copy link

RomanRobot commented Jul 28, 2020

@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. FileReader API not firing ionic-team/capacitor#1564 (comment)
  2. FileReader API not firing ionic-team/capacitor#1564 (comment)

@RomanRobot
Copy link

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.

@bsbechtel
Copy link

@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);
  }
}

@RomanRobot
Copy link

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

@greatwhiz
Copy link

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

@maudvd
Copy link

maudvd commented Mar 2, 2022

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.

@Mimateam
Copy link

Mimateam commented Jul 26, 2023

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.

I have the exact same issue on Android (Ionic 7, Capacitor 5, Angular 16). In my case I need FormData, which I send it through post method to client's API. Client's API accepts only Blob. When I add Blob to my FormData, nothing happens in the request. Just like in your case.

I've tried angular/common/http which works great on my dev env., but in production I get hit by client's CORS policy. So I need to use plugin which uses native libraries, like this one.

We really need this to be added asap. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests