Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Storage: string does not match format base64: invalid character found. Only when debug is off

I'm trying to upload an image file to firebase storage, save the download URL, and load it after the upload is completed. When I run the app with debug js remotely on it works fine. When I turn off debug mode it stops working with the invalid format exception. The same happens when I run in a real device (both iOS and Android)

The base64 response data from React Native Image Picker seems to be correct

Here's my code

...
import * as ImagePicker from 'react-native-image-picker'; //0.26.10
import firebase from 'firebase'; //4.9.1
...

handleImagePicker = () => {
    const { me } = this.props;
    const options = {
      title: 'Select pic',
      storageOptions: {
        skipBackup: true,
        path: 'images'
      },
      mediaType: 'photo',
      quality: 0.5,
    };
    ImagePicker.showImagePicker(options, async (response) => {
        const storageRef = firebase.storage().ref(`/profile-images/user_${me.id}.jpg`);

        const metadata = {
          contentType: 'image/jpeg',
        };

        const task = storageRef.putString(response.data, 'base64', metadata);
        return new Promise((resolve, reject) => {
          task.on(
            'state_changed',
            (snapshot) => {
              var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
              console.log('Upload is ' + progress + '% done');
            },
            (error) =>
              console.log(error),
            () => {
              this.onChangeProfileImage();
            }
          );
        });
     }
}

onChangeProfileImage = async () => {
    const { me } = this.props;

    const storageRef = firebase.storage().ref(`/profile-images/user_${me.id}.jpg`);

    const profileImageUrl = await new Promise((resolve, reject) => {
      storageRef.getDownloadURL()
      .then((url) => {
        resolve(url);
      })
      .catch((error) => {
        console.log(error);
      });
    });


  // some more logic to store profileImageUrl in the database

  }

Any idea how to solve this?

Thanks in advance.

like image 977
soutot Avatar asked Mar 05 '23 12:03

soutot


2 Answers

After some research and debug I found the cause of the issue and a solution for it.

Why does it happen?

Firebase uses atob method to decode the base64 string sent by putstring method. However, since JavaScriptCore doesn't have a default support to atob and btoa, the base64 string can't be converted, so this exception is triggered.

When we run the app in debug javascript remotely mode, all javascript code is run under chrome environment, where atob and btoa are supported. That's why the code works when debug is on and doesn't when its off.

How to solve?

To handle atob and btoa in React Native, we should either write our own encode/decode method, or install a lib to handle it for us.

In my case I preferred to install base-64 lib

But here's an example of a encode/decode script:

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const Base64 = {
  btoa: (input:string = '')  => {
    let str = input;
    let output = '';

    for (let block = 0, charCode, i = 0, map = chars;
    str.charAt(i | 0) || (map = '=', i % 1);
    output += map.charAt(63 & block >> 8 - i % 1 * 8)) {

      charCode = str.charCodeAt(i += 3/4);

      if (charCode > 0xFF) {
        throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
      }

      block = block << 8 | charCode;
    }

    return output;
  },

  atob: (input:string = '') => {
    let str = input.replace(/=+$/, '');
    let output = '';

    if (str.length % 4 == 1) {
      throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
    }
    for (let bc = 0, bs = 0, buffer, i = 0;
      buffer = str.charAt(i++);

      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
        bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
    ) {
      buffer = chars.indexOf(buffer);
    }

    return output;
  }
};

export default Base64;

Usage:

import Base64 from '[path to your script]';

const stringToEncode = 'xxxx';
Base64.btoa(scriptToEncode);

const stringToDecode = 'xxxx';
Base64.atob(stringToDecode);

After choosing either to use the custom script or the lib, now we must add the following code to the index.js file:

import { decode, encode } from 'base-64';

if (!global.btoa) {
    global.btoa = encode;
}

if (!global.atob) {
    global.atob = decode;
}

AppRegistry.registerComponent(appName, () => App);

This will declare atob and btoa globally. So whenever in the app those functions are called, React Native will use the global scope to handle it, and then trigger the encode and decode methods from base-64 lib.

So this is the solution for Base64 issue.

However, after this is solved, I found another issue Firebase Storage: Max retry time for operation exceed. Please try again when trying to upload larger images. It seems that firebase has some limitation on support to React Native uploads, as this issue suggests.

I believe that react-native-firebase may not struggle on this since it's already prepared to run natively, instead of using the web environment as firebase does. I didn't test it yet to confirm, but it looks like this will be the best approach to handle it.

Hope this can be helpful for someone else.

like image 175
soutot Avatar answered Apr 30 '23 06:04

soutot


The problem is now solved using fetch() API. The promise returned can be converted to blob which you can upload to firebase/storage

Here is an example

 let storageRef = storage().ref();
 let imageName = data.name + "image";
 let imagesRef = storageRef.child(`images/${imageName}`);

 const response = await fetch(image); 
 const blob = await response.blob(); // Here is the trick

 imagesRef
      .put(blob)
      .then((snapshot) => {
        console.log("uploaded an image.");
      })
      .catch((err) => console.log(err));
like image 41
Ayush Kumar Avatar answered Apr 30 '23 06:04

Ayush Kumar