0

I uploaded a form data which contains an image file and some other fields. I uploaded the file image to my firebase cloud storage and added the other form data to my firebase firestore collection with a constructed URL of the file image I uploaded which was like (https://firebasestorage.googleapis.com/v0/b/${storageBucket key})/o/${filename}?alt=media). my upload was successful but I got an error when I tried viewing the file image on my browser using the URL

{
  "error": {
    "code": 404,
    "message": "Not Found.  Could not get object",
    "status": "GET_OBJECT"
  }
}

Below is my RestApi Code:

exports.addType = (req, res) => {

    const BusBoy = require('busboy');
    const path = require('path');
    const os = require('os');
    const fs = require('fs');

    const busboy = new BusBoy({headers: req.headers});
    let fileName;
    let fileToBeUploaded = {};
    let fields = {};
    busboy.on('field', (fieldname, data)=> {
        fields[fieldname] = data;
    })
    busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
        const imageExtension = filename.split('.')[filename.split('.').length - 1];
        fileName = `${Math.round(Math.random() * 10000000000000)}.${imageExtension}`;
        const filepath = path.join(os.tmpdir(), fileName);
        fileToBeUploaded = { filepath, mimetype };
        file.pipe(fs.createWriteStream(filepath));
    })
    busboy.on('finish', ()=> {
        let imageUrl;
        
        admin.storage().bucket().upload(fileToBeUploaded.filepath, {
            destination:`Type/${fileName}`,
            resumable: false,
            metadata: {
                metadata: {
                    contentType: fileToBeUploaded.mimetype
                }
            }
        })
        .then(() => {
            imageUrl =  `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${fileName}?alt=media`;
            return db.collection('Type').doc(fields.type);
        })
        .then(data => {
            if(data.exists){
                return res.status(400).json({error: `${fields.type} already exist`})
            }
            db.collection('Type').doc(fields.type).set({
                type: fields.type,
                category: fields.category,
                details: fields.details,
                imageUrl
            })
        })
        .then(()=> {
            return res.status(200).json({message: `${fields.type} has been added`});
        })
        .catch(err => {
            return res.status(500).json({error: err.code});
        })
    })
    busboy.end(req.rawBody);
}
Kelvin
  • 1
  • 1

1 Answers1

0

Yep. This is a bug in Firebase Storage. Access (under Security Rules) requires an "authorization" header - and HTML & Browsers do not support added headers for <img> tags. It is possible to access such files either by:

  • fetching a "long-lived" (i.e. permanent) URL using ref.getDownloadURL(), which BYPASSES the Security Rules (useful for images that need to become part of SEO results - fully public)
  • OR
  • using an asynchronous fetch (fetch or axios or XMLHttpRequest or whatever) while adding the appropriate authorization: header. What is the proper value for the header? It took a fair bit of diving into Google's Github, BUT it turns out it is as simple as `Firebase ${JWTToken}` (note the space).
  • This gitHub issue discusses it at length, but the solution-for-the-moment is:

A proposed Browser/Node-side solution

Not the most straight forward, due to browser limitations. It is NOT possible to add custom headers directly to <img> tags, but it IS possible to set an onError handler to do an independent fetch.

JSX

              <img
                src={image}
                //crossOrigin="use-credentials"
                style={whatever}
                alt={whatever}
                onError={errorHandler}
              />

where: {image} is the Firebase Storage URL, which can be entirely programatically generated. NOTE: the URL MUST include the alt=media tag (or whatever is appropriate to the file type). If this parameter is missing, Firebase Storage will return the metadata JSON object. {errorHandler} see below

Javascript

//built in React
 const [authorization, setAuthorization] = useState(null);

//Asynchronously fetches users JWT.  Note closure, as useEffect itself must be synchronous
  useEffect(() => {
    if (user) {
      (async () => {
        const authToken = await fetchJWT();
        setAuthorization(authToken);
      })();
    }
  }, [user]);

//Technically, this is in my wrapper library - uses the Firebase Auth service to fetch a user's JWT
 export const fetchJWT = async (user) => {
  const thisUser = user || FirebaseAuth.currentUser;
  //the "true" below forces a reset
  const JWT = await thisUser.getIdToken(true);
  return JWT;
 }

//More-or-less copied from firebase-js-sdk/storage/src/implementation/requests.ts
  const addAuthHeader_ = async (headers) => {
    if (authorization !== null && authorization.length > 0) {
      headers["Authorization"] = "Firebase " + authorization;
    }
  };

// handles the actual error event 
  const errorHandler = async (e) => {
    e.stopPropagation();
    const target = e.target;
    const src = $(target).attr("src");
    let headers = {};
    addAuthHeader_(headers);
    const options = {
      headers: headers
    };
    await fetch(src, options)
      .then((res) => {
        return res.status === 200 && res.blob();
      })
      .then((blob) => {
        if (!blob) {
          return;
        }
        $(target).prop("src", URL.createObjectURL(blob));
      });
  };

Explanation

when the <img> tag encounters an error, the errorHandler

  • fetches the src attribute from the event target.
  • It then creates an options object for the fetch() call, including headers. The headers get an authorization header added, which consists of a string with `Firebase ${authortization}`
  • The resulting fetch is saved as a Blob to local storage, and a URL created with URL.createObjectURL(blob), which is then set as the src for the target <img>.

Result

A file (in this case, an image) is fetched from Firebase Storage, following all of the Security Rules set therein. No permanent token needed. Only authenticated users have access. (I will note that I use Anonymous User accounts for new users, with appropriate restrictions set on their access)

Going Forward

I am likely to added a class to <img> elements that are intended to show images from Firebase Storage, or possibly examine the URLs. That way a single errorHandler can be designated to handle all of them.

LeadDreamer
  • 3,303
  • 2
  • 15
  • 18