Ionic-native: file reader plugin events are not firing

Created on 29 Aug 2016  路  50Comments  路  Source: ionic-team/ionic-native

_From @comfortme on August 27, 2016 16:43_

Short description of the problem:

while using file plugin with ionic-native found out that readAsArrayBuffer(path, file) promise never resolves!
i did some digging and turns out it's an fixed angular 2 zone issue and here is a detailed closed ionic issue about it. https://github.com/driftyco/ionic/issues/6020
it looks like it's never been fixed for ionic 2

What behavior are you expecting?

file promises to be resolved

Steps to reproduce:

  1. setup a new project
  2. install camera and file plugin
  3. getpicture with camera and try to read it with readAsArrayBuffer
Camera.getPicture(options).then((imageData) => {
File.readAsArrayBuffer(imageData).then((res)=>{
//never resolves.
});
});

Other information: (e.g. stacktraces, related issues, suggestions how to fix, stackoverflow links, forum links, etc)
https://github.com/angular/zone.js/issues/137
https://github.com/angular/angular/issues/2533

Which Ionic Version? 1.x or 2.x
Cordova CLI: 6.0.0
Ionic Framework Version: 2.0.0-beta.11
Ionic CLI Version: 2.0.0-beta.36
Ionic App Lib Version: 2.0.0-beta.19
OS: Windows 8.1
Node Version: v4.4.3

_Copied from original issue: driftyco/ionic#7891_

Most helpful comment

So, I've found a fix (I think) is to re-order the index.html of your app so that the polyfills.js (which includes zone.js and a few other things) is included before cordova.js. This seems to make it work reliably for me. Presumably this gives some clue as to what's going on, something to do with how zone.js patches things?

All 50 comments

Currently the only possible workaround is:

const WrappedFileReader = window.FileReader
window.FileReader = function OriginalFileReader(...args) {
  WrappedFileReader.apply(this, args)
  const originalInstance = this[Zone.__symbol__('originalInstance')] // eslint-disable-line

  return originalInstance || this
}

I have had this issue, too. After updating from 1.3.17 to 1.3.21 everything worked again.

@comfortme Can you try with the latest version v2.2.2 and report back if you still have any issue? Thanks.

@ihadeed I am still seeing this issue for ios only. I am using zone.js v0.6.25 at the moment, with ionic-angular v2.0.0-rc.1 and ionic-native v2.2.3

I tried the workaround and the events are now firing on ios, but then I recieve and have to workaround this error on android.

Uncaught TypeError: i.readAsArrayBuffer is not a function

This is still an open issue for ionic-native.

I also have this issue with ionic-native 2.2.10 and strange is that it doesn't happen always and it's very hard to tell how and why exactly does it happen. When I modify other parts of my code and build the app, File.readAsArrayBuffer sometimes just stops working and its promise never resolves or rejects.

@pdrosos Please create a new issue with steps to reproduce this. Also, make sure you're waiting for deviceready event to fire or wrapping your code with this.platform.ready().then(() => { ... }).

@ihadeed @pdrosos

I still need the workarround suggested by @stalniy to manage my photos loading on ios and android, but it is strange since it works perfectly on ios emulator without the workarround.

@comfortme @mmazloum

Sorry I just had another look at the original issue here.

You are having problems because you are passing imageData directly to readAsArrayBuffer. You need to split the file URI into path and file name, then pass those to the readAsArrayBuffer method.

Here is an example: (or here)

        Camera.getPicture()
          .then(imageData => {
            const fileName = imageData.split('/').pop();
            const path = imageData.replace(fileName, '');
            return File.readAsArrayBuffer(path, fileName);
          })
          .then(response => {
            console.log('File response is: ', response);
          })
          .catch(console.error.bind(console));

@ihadeed

We are still seeing this issue.
The fileReader.onloadend callback never fires.

We are using

  • @angular 2.4.1 (with ng-upgrade)
  • angular 1.5.8
  • zone.js 0.7.2
  • cordova plugin file 4.3.0
  • ionic.native 2.2.14

@dbpieter can you share the code so I can have a look?

@ihadeed the last solution you suggested worked on ios, but the promise still doesn't resolve on android.

here is a code sample :

  //             const fileName2 = fileUri.split('/').pop();
  //             const path = fileUri.replace(fileName2, '');
  //             console.log('fileName2: ', fileName2);
  //             console.log('path: ', path);
  //             File.readAsDataURL(path, fileName2).then((res) => {
  //               //console.log(res);
  //               //let base64img = res.target.result;
  //               this.imgSrc=res as string;
  //               this.changeDetector.detectChanges();
  //               console.log("image base64 loaded");
  //               this.uploading = true;
  //               let blob = this.dataURItoBlob(this.imgSrc);

@ihadeed
my code is similar to @mmazloum 's code. The issue only occurs on Android.
Loading zone.js before cordova.js also fixes it.

There seems to be a problem with the patching of FileReader (both zone and cordova-plugin-file do this)
It appears the reader.onloadend callback gets never executed.

I currently have this problem with:

  • angular 2.2.1
  • ionic 2.0.0-rc4
  • ionic-native 2.2.11
  • cordova plugin file 4.3.0
  • zone.js 0.6.26

I can't get any of the workarounds to work. @dbpieter what do you mean by "Loading zone.js before cordova.js also fixes it." and if that is a fix, could you give some more details?

My app is currently completely dead in the water because of this :(

@joewoodhouse
The zone.js dist script must be run before the cordova.js file so your index.html eventually look like this (whatever buildsystem you are using)

  • (zone js needs es6 polyfill)

The workaround suggested by @stalniy also works for us. However this seems like a slightly cleaner fix.

Hmm I'm using a pretty standard ionic2 app setup, and I have no zone.js script - is it not bundled up into the main.js?

@joewoodhouse It's probably bundled into your main.js yes (which is the problem). We created a separate bundle with zone and core that gets loaded before cordova.js. (we use webpack as our build system). Our main.js (excluding core and zone) gets loaded after cordova.js

@dbpieter I'm not sure but this is my vision of how it works when you put zonejs before cordovajs:

  • zone is loaded and trying to patch FileReader
  • html5 FileReader is patched if found
  • Cordova-file-plugin is initialized (by cordovajs)
  • Cordova-fileplugin overwrites html5 FileReader
  • we have zone free FileReader provided by Cordova-file-plugin

In this case it works as you want but there may be issues with other plugins and zonejs

@stalniy does the cordova-file-plugin overwrite the HTML5 FileReader or is it patched? I thought it would work as following:

On Load

  • zone.js patches the HTML5 FileReader
  • cordova-file-plugin patches the already patched by zone.js FileReader

On Execute

  • FileReader executes cordova-file-plugin code
  • FileReader executes zone.js code
  • FileReader executes native code

I had a bit more success with this by wrapping everything in a this.platform.ready().then(()=>{}). It now seems to reliably call the onloadend callback.

However I'm not really sure why I need to do this. My use of FileReader is not anywhere near to app startup time so there should be no issue with the platform being ready or not. Perhaps this.platform.ready actually does something more complicated than the documentation eludes to? From looking at the source NgZone does seem to be involved, so perhaps that is the issue/solution?

@joewoodhouse I did this to and kept on having the problem, but I remember it looked solved on the first trials because I hadn't rebuilt my android app properly.

@BjornRombaut as you can see from source https://github.com/apache/cordova-plugin-file/blob/master/www/FileReader.js it actually overwrites (or maybe better say wraps FileReader) and as a result FileReader api is not even called everytime. For local files cordova-file-plugin uses different strategy (https://github.com/apache/cordova-plugin-file/blob/master/www/FileReader.js#L268) and maybe this is the root cause of integration issue between cordova-file-plugin and zone.js.

Here is the working example for reading image in base64 format,
residing in device using FILE (FILE.readAsDataURL) ..
(https://github.com/apache/cordova-plugin-file) (From documentation) ..
I hope It helps..
@ihadeed @jgw96

                       let fileName = path.split('/').pop();
                       let filePath = path.replace(fileName, '');;

                      fileName = fileName.split('?')[0];   // --> This Line is added to make it work in case of Image...
                       File.readAsDataURL(filePath, fileName).then((data) => {
                         this.final_Image = data;
                       }).catch(err => { });

So, I've found a fix (I think) is to re-order the index.html of your app so that the polyfills.js (which includes zone.js and a few other things) is included before cordova.js. This seems to make it work reliably for me. Presumably this gives some clue as to what's going on, something to do with how zone.js patches things?

@joewoodhouse this works for me too, nothing else was needed. Thanks!

It would be good if someone from Ionic could tell us if this is a safe change to make. Presumably there was a reason why that order of script tags was chosen. I've done some very light testing but would be good to get the official word.

@joewoodhouse it might be a good idea to ask your question here: https://github.com/driftyco/ionic2-app-base

@joewoodhouse Your workaround saved my day. It works well. Thanks.

file.readAsText doesn't resolve either.... changing the order of the libs load in the index.html is useless , everytime you do ionic serve will go back to the early stage, so, anyone have an idea about how to fix this????

@albe-rosado

File plugin doesn't work with ionic serve.

Test your app on a device using latest version of Ionic Native and the plugin. If you are still experiencing any issues, please open a new issue.

Also it heppens with me in android project, but only in release...

@joewoodhouse thanks for workaround

@dbpieter @joewoodhouse thank you! this issue took away 3 days of my life :(

hi, all
many thanks to @joewoodhouse for the workaround,
Unfortunately, my problems are not over ...

not all files selected from "fileChooser / Download" are found by the code below...
specifically only those in /sdcard/Download for the others it seems my var pathFile is wrong
is this a bug?
is there a mistake in my code?

this.fileChooser.open()
.then(uri => {
console.log(uri)
this.filePath.resolveNativePath(uri)
.then(filePath => {
console.log(filePath)
let pathFile = filePath.substr(0, filePath.lastIndexOf('/') + 1)
let nomefile = filePath.substr(filePath.lastIndexOf('/') + 1, 200)
this.file.readAsDataURL(pathFile, nomefile)
.then((imageData) => {
this.imageFileName = imageData;
this.uploadFile()
})
.catch(err => {
console.log(err)
});
})
.catch(err => {
console.log(err)
});
})
.catch(e => console.log(e));

Is there anyone who has reorder the polyfill.js before cordova.js still doesn't get the solution? If yes can you please respond what steps did you do to solve the problem

Also if by reordering if you are able to resolve can you please paste here your version of package.json

It鈥檚 a zone.js issue. To fix it, edit the index.html and move the cordova.js (from head) after polyfills.js (body) like

<script src="build/polyfills.js"></script>
<script src="cordova.js"></script>
<script src="build/vendor.js"></script>
<script src="build/main.js"></script>

Cordova must included AFTER Polyfills

But is there an official solution?

"@angular/core": "5.2.10",
"ionic-angular": "3.9.2",
"ionic": "^4.2.1",
"zone.js": "0.8.26"

Update: The placement of cordova after polyfills will produce new problem in prod build. Maybe load cordova twice (head and body after polyfills)? 馃槃

How can this problem be 100% reproduced? Does anyone have an idea? It's hard to test it with this sporadic behavior.

I'm currently testing the solution of @stalniy I placed it in index.html on bottom after all other scripts. Thanks for this solution. But I'm not yet sure if this works. Because this poor reproducibility of this bug.

@stalniy Your solution with WrappedFileReader does not work for me. I get the error fileReader.readAsArrayBuffer is not a function.

Still getting this error and none of the solutions work.

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

---------------EDIT-----------------
Since I've posted the above code, I've had to make one small fix. If the file you are reading is __not__ a __real__ file (i.e. not a Blob), then you __don't__ want to use the __real__ FileReader).

Here's a more detailed example:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

I got this error when trying to use your solution: "Property '_realReader' does not exist on type 'FileReader'". Any advice?

That looks like a Typescript error, not a JavaScript error. If so, you just need to cast the fileReader to the any type. Something like this should work.

const realFileReader = (fileReader as any)._realReader;

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

This also worked for me, but make sure you attach your onloadend event handler to realFileReader and not @Ionic-native FileReader.

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

Hi, I'm having this exact issue (only on crosswalk, not default webview) and though I have been able to follow your instruction and successfully called the readAsArrayBuffer function, there is another the problem at the very last stage which is the input for it. "Uncaught TypeError: Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'"

Below is my piece of code and I'm pretty sure that the variable I passed as the parameter is indeed a File object and as you can see, I tried to cast it to File again right before passing it but there's no use.

  private readnewImgFile(file:any, index) {

   let reader = new FileReader();
    const realFileReader = (reader as any)._realReader;
    if (realFileReader) {
      console.log('has real file reader')
      console.log(file.type)
      realFileReader.onloadend = () => {
      //doing some stuff here with the result
      };
      realFileReader.onerror = function (error) {
        console.log(error);
        console.log('reader fail')
      };
      realFileReader.readAsArrayBuffer(<File>file);
    }
    else{
      console.log('no real filereader')
    }
}

When I console.log the file, these are the properties that I got

end:498212
lastModified:1572514141000
lastModifiedDate:1572514141000
localURL:"cdvfile://localhost/sdcard/Pictures/_img201993116291.png"
name:"_img201993116291.png"
size:498212
start:0
type:"image/png"

@hnguyen48206, since I've posted my original fix, I've had to alter the code a little bit. Here's the latest:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

The main thing is you have to check if the file is a Blob or not. If the file is a Blob, then use the __real__ FileReader. Otherwise, don't.

@hnguyen48206, since I've posted my original fix, I've had to alter the code a little bit. Here's the latest:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

The main thing is you have to check if the file is a Blob or not. If the file is a Blob, then use the real FileReader. Otherwise, don't.

Thanks for your quick response, by checking the type of Blob, now I can assure that the 'file' returned from the FileEntry.file() is not an actual Blob/File indeed. However, I dont really know exactly what was returned cause there is no way to check what the 'file' is. If I console.log the FileEntry, it does have all the properties that listed here: https://cordova.apache.org/docs/en/1.9.0/cordova/file/fileentry/fileentry.html and should be legit. Then now the question is what is the thing that FileEntry.file() return? Any help would be much appriciated.

Below is the code I use to turn File URI to File entry before converting it to File/Blob
(*) variable pic is the uri of a picture (either taken by fresh capturing or picking from library)

  if (pic.startsWith("file")) {
        this.file
          .resolveLocalFilesystemUrl(pic)
          .then(entry => {
            console.log(entry);
            (entry as FileEntry).file(
              file=>{
                console.log(typeof file)
                console.log(file instanceof Blob)
                this.readnewImgFile(file, i)
              }, err=>{
                console.log(err)
              }
             );
          })
          .catch(err => console.log(err));
      } else {
        this.file
          .resolveLocalFilesystemUrl("file://" + pic)
          .then(entry => {
            console.log(entry);
            (entry as FileEntry).file(file =>
              {
                console.log(typeof file);
                console.log(file instanceof Blob);
                this.readnewImgFile(file, i);
              });
          })
          .catch(err => console.log(err));
      }

@hnguyen48206, that file object is defined here: https://github.com/apache/cordova-plugin-file/blob/master/www/File.js. In that same directory you can see the definitions for FileEntry and other applicable classes.

Im having trouble reading this file returned by "fileEntry.file(file =>...)" using the _realReader. It throws an error saying the "file" its not an actual Blob. How can I convert this "cordova file" into a native js Blob?
I tryied to use its file.slice() method, but it actually returns a new instance of file (the cordova one) instead of a blob.
Has anyone managed to convert this? Thanks in advance!

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

---------------EDIT-----------------
Since I've posted the above code, I've had to make one small fix. If the file you are reading is not a real file (i.e. not a Blob), then you don't want to use the real FileReader).

Here's a more detailed example:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

Thank you so much. This solution worked for me with Ionic 5!

I was able to fix my issue by getting the original native browser instance and using it.

let fileReader = new FileReader();

// Get the original real FileReader. The polyfill saves a reference to it.
const realFileReader = fileReader._realReader;

// Make sure we were able to get the original FileReader 
if (realFileReader) {
    // Swap out the polyfill instance for the original instance.
    fileReader = realFileReader;
}

fileReader.readAsArrayBuffer(someBlob);

This worked fine in Android and iOS.

---------------EDIT-----------------
Since I've posted the above code, I've had to make one small fix. If the file you are reading is not a real file (i.e. not a Blob), then you don't want to use the real FileReader).

Here's a more detailed example:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

was having problems when building for android, for ios devices, it worked normally.

both solutions, changing the order of cordova.js or instantiating the browser file reader worked.

Brazil thanks you! 馃帀

@hnguyen48206, since I've posted my original fix, I've had to alter the code a little bit. Here's the latest:

getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {

      let fileReader = new FileReader();

      // Is this a "real" file? In other words, is this an instance of the original `File` class (not the one overriden by cordova-plugin-file).
      // If so, then we need to use the "real" FileReader (not the one overriden by cordova-plugin-file).
      if (file instanceof Blob) {
        const realFileReader = (fileReader as any)._realReader;
        if (realFileReader) {
          fileReader = realFileReader;
        }
      }

      fileReader.onloadend = function() {
        resolve(new Blob([this.result], { type: file.type }));
      };

      fileReader.onerror = function(err) {
        console.error('Inside fileReader.onerror', err);
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

The main thing is you have to check if the file is a Blob or not. If the file is a Blob, then use the __real__ FileReader. Otherwise, don't.

Your code save my life

Was this page helpful?
0 / 5 - 0 ratings

Related issues

goleary picture goleary  路  3Comments

hobbydevs picture hobbydevs  路  3Comments

mateo666 picture mateo666  路  3Comments

sabariferin picture sabariferin  路  4Comments

icchio picture icchio  路  3Comments