Firstly I have to say that I'm using Expo, so from one library I get a data url (I have already checked that it is correct) and then tries to save this image in Firebase Storage, but every time I send it to the base I see the error 'Error loading preview'. I've tried many ways, such as changing it to a Blob using a request, and here I get another error:
[Unhandled promise rejection: TypeError: Network request failed]
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function () {
reject(new TypeError('Network request failed'));
};
xhr.responseType = 'blob';
xhr.open('GET', dataUrl, true);
xhr.send(null);
});
uploadBytes(storageDatabaseReference, blob as Blob).then(() => {
console.log('Uploaded a blob or file!');
});
And there are my previous tries:
const base64result = dataUrl.split(',')[1];
await uploadString(storageDatabaseReference, base64result, 'base64', {
contentType: 'image/png',
}).then(async () => {
const url = await getDownloadURL(storageDatabaseReference);
return url;
});
await uploadString(storageDatabaseReference, dataUrl, 'data_url', {
contentType: 'image/png',
}).then(async () => {
const url = await getDownloadURL(storageDatabaseReference);
return url;
});
for example dataUrl can have value:
data:image/png;base64,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
And this happens always after upload (which works only with uploadString currently):
.
Related
Miragejs handles json out-of-the-box but seems to be unable to handle FormData. When I post FormData and log the received request in mirage endpoint, request.requestBody is empty.
Simplified code examples:
POSTing FormData:
const testFile = new File(['hello'], 'hello.png', { type: 'image/png' });
const formData = new FormData('file', testFile);
fetch('https://localhost:3000/api/endpoint', {method: 'POST', body: formData});
// ...
receiving POST in mirage mock server:
this.post('/endpoint', (schema, request) => {
console.log('request:', request);
// request.requestBody is an empty string!
});
Possibly a related issue: https://github.com/miragejs/ember-cli-mirage/issues/74
It's possible to cast the request.requestBody to FormData and then parse the file.
Shortening the excellent solution described in How to handle uploading and parsing files in your frontend app tests:
this.post('/endpoint', (schema, request) => {
const readFile = async (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error('There was an error reading the file!'));
reader.onload = () => resolve(reader.result as string);
reader.readAsText(file);
});
const formData: FormData = request.requestBody as unknown as FormData;
const uploadFile: File = formData.get('file') as File;
const fileContents: string = await readFile(uploadFile);
console.log('Uploaded file contents:', fileContents);
});
Error given: [Unhandled promise rejection: FirebaseError: Firebase Storage: String does not match format 'data_url': Must be formatted 'data:[][;base64], (storage/invalid-format)]
I am able to add image to the database, however it is not uploading to the firebase storage. I have changed it to different media types but no luck. I have also tired uploadBytes instead of uploadString and it works but no image displays. Any help would be appreciated!
const uploadImageTops = async () => {
const docRef = await addDoc(collection(db, "tops"), {
username: "user",
apparel: "tops",
color: "black",
size: size,
timeStamp: serverTimestamp(),
});
const imageRef = ref(storage, `tops/${docRef.id}`);
await uploadString(imageRef, cameraImage, "data_url", {contentType:'image/jpg'})
.then(async (snapshot) => {
const downloadURL = await getDownloadURL(imageRef);
await updateDoc(doc(db, "tops", docRef.id), {
imageUrl: downloadURL,
});
})
.then(navigation.navigate("Home"));
};
There's some issue with react-native and firebase when trying to upload the image. Blob is needed to upload image to firebase storage and firestore. Add that blob at the top and make sure you close the blob at the end.
This code worked for me.
const uploadImageTops = async () => {
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function (e) {
console.log(e);
reject(new TypeError("Network request failed"));
};
xhr.responseType = "blob";
xhr.open("GET", cameraImage, true);
xhr.send(null);
});
const docRef = await addDoc(collection(db, "tops"), {
username: user,
apparel: "tops",
color: color,
size: size,
timeStamp: serverTimestamp(),
});
const imageRef = ref(storage, `tops/${docRef.id}`);
const metadata = {
contentType: "image/jpeg",
};
await uploadBytes(imageRef, blob, metadata)
.then(async (snapshot) => {
const downloadURL = await getDownloadURL(imageRef);
await updateDoc(doc(db, "tops", docRef.id), {
imageUrl: downloadURL,
});
blob.close();
})
.then(navigation.navigate("Home"));
};
I have the following Function that:
Listens for document (text message) creation
Grab IDs of members of a group chat
Get the FCM Tokens for each member
With a for-loop, send messages to group members
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const data = snap.data();
const mealID = context.params.mealID;
const senderID = data.senderID;
const senderName = data.senderName;
const messageContent = data.content;
var docRef = db.collection("chats").doc(mealID);
docRef
.get()
.then((doc) => {
if (doc.exists) {
const docData = doc.data();
const mealName = docData.name;
const userStatus = docData.userStatus;
var users = docData.to;
var eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(docData.from);
// get fcmTokens from eligibleUsers and send the messagme
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {
var fcmTokens = [];
var thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.data().uid == senderID) {
thumbnailPicURL =
doc.data().thumbnailPicURL == null
? "https://i.imgur.com/8wSudUk.png"
: doc.data().thumbnailPicURL;
} else {
fcmTokens.push(doc.data().fcmToken);
}
});
// send the message fcmTokens
fcmTokens.forEach((token) => {
if (token != "") {
const fcmMessage = {
message: {
token: token,
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL,
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
},
},
};
tokenManger.sendFcmMessage(fcmMessage);
}
});
return true;
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
return false;
}
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
return true;
});
My send function comes from a helper file that uses the HTTP V1 protocol to build the send-request:
const { google } = require("googleapis");
const https = require("https");
const MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
const SCOPES = [MESSAGING_SCOPE];
const PROJECT_ID = MY_PROJECT_ID;
const HOST = "fcm.googleapis.com";
const PATH = "/v1/projects/" + PROJECT_ID + "/messages:send";
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(function (err, tokens) {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
this.getAccessToken().then(function (accessToken) {
var options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
var request = https.request(options, function (resp) {
resp.setEncoding("utf8");
resp.on("data", function (data) {
console.log("Message sent to Firebase for delivery, response:");
console.log(data);
});
});
request.on("error", function (err) {
console.log("Unable to send message to Firebase");
console.log(err);
});
request.write(JSON.stringify(fcmMessage));
request.end();
});
};
It worked all fine in the emulator but once deployed, there're significant delays (~3 mins):
I also noticed that the console says the cloud function finishes execution BEFORE sendFcmMessage logs success messages.
I did some research online, it appears that it might have something to do with the usage of Promise but I wasn't sure if that's the sole reason or it has something to do with my for-loop.
The Problem
To summarize the issue, you are creating "floating promises" or starting other asynchronous tasks (like in sendFcmMessage) where you aren't returning a promise because they use callbacks instead.
In a deployed function, as soon as the function returns its result or the Promise chain resolves, all further actions should be treated as if they will never be executed as documented here. An "inactive" function might be terminated at any time, is severely throttled and any network calls you make (like setting data in database or calling out to FCM) may never be executed.
An indicator that you haven't properly chained the promises is when you see the function completion log message ("Function execution took...") before other messages you are logging. When you see this, you need to look at the code you are running and confirm whether you have any "floating promises" or are using callback-based APIs. Once you have changed the callback-based APIs to use promises and then made sure they are all chained together properly, you should see a significant boost in performance.
The fixes
Sending the message data to FCM
In your tokenManger file, getAccessToken() could be reworked slightly and sendFcmMessage should be converted to return a Promise:
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(
(err, tokens) => err ? reject(err) : resolve(tokens.access_token)
);
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
// CHANGED: return the Promise
return this.getAccessToken().then(function (accessToken) {
const options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
// CHANGED: convert to Promise:
return new Promise((resolve, reject) => {
const request = https.request(options, (resp) => {
resp.setEncoding("utf8");
resp.on("data", resolve);
resp.on("error", reject);
});
request.on("error", reject);
request.write(JSON.stringify(fcmMessage));
request.end();
});
});
};
However, the above code was built for googleapis ^52.1.0 and google-auth-library ^6.0.3. The modern versions of these modules are v92.0.0 and v7.11.0 respectively. This means you should really update the code to use these later versions like so:
// Import JWT module directly
const { JWT } = require('google-auth-library');
// FIREBASE_CONFIG is a JSON string available in Cloud Functions
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
const FCM_ENDPOINT = `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`;
const FCM_SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"];
exports.sendFcmMessage = (fcmMessage) => {
const key = require("./service-account.json"); // consider moving outside of function (so it throws an error during deployment if its missing)
const client = new JWT({
email: key.client_email,
key: key.private_key,
scopes: FCM_SCOPES
});
return client.request({ // <-- this uses `gaxios`, Google's fork of `axios` built for Promise-based APIs
url: FCM_ENDPOINT,
method: "POST",
data: fcmMessage
});
}
Better yet, just use the messaging APIs provided by the Firebase Admin SDKs that handle the details for you. Just feed it the message and tokens as needed.
import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
initializeApp(); // initializes using default credentials provided by Cloud Functions
const fcm = getMessaging();
fcm.send(message) // send to one (uses the given token)
fcm.sendAll(messagesArr) // send to many at once (each message uses the given token)
fcm.sendMulticast(message) // send to many at once (uses a `tokens` array instead of `token`)
The Cloud Function
Updating the main Cloud Function, you'd get:
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const mealID = context.params.mealID;
const { senderID, senderName, content: messageContent } = snap.data();
const docRef = db.collection("chats").doc(mealID);
/* --> */ return docRef
.get()
.then((doc) => {
if (!doc.exists) { // CHANGED: Fail fast and avoid else statements
console.log(`Could not find "chat:${mealID}"!`);
return false;
}
const { userStatus, to: users, name: mealName, from: fromUser } = doc.data();
const eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(fromUser);
// get fcmTokens from eligibleUsers and send the message
/* --> */ return db.collection("users")
.where("uid", "in", eligibleUsers) // WARNING: This will only work for up to 10 users! You'll need to break it up into chunks of 10 if there are more.
.get()
.then(async (snapshot) => {
const fcmTokens = [];
let thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.get("uid") == senderID) {
thumbnailPicURL = doc.get("thumbnailPicURL"); // update with given thumbnail pic
} else {
fcmTokens.push(doc.get("fcmToken"));
}
});
const baseMessage = {
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL || "https://i.imgur.com/8wSudUk.png", // CHANGED: specified fallback image here
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
}
}
// log error if fcmTokens empty?
// ----- OPTION 1 -----
// send the message to each fcmToken
const messagePromises = fcmTokens.map((token) => {
if (!token) // handle "" and undefined
return; // skip
/* --> */ return tokenManger
.sendFcmMessage({
message: { ...baseMessage, token }
})
.catch((err) => { // catch the error here, so as many notifications are sent out as possible
console.error(`Failed to send message to "fcm:${token}"`, err);
})
});
await Promise.all(messagePromises); // wait for all messages to be sent out
// --------------------
// ----- OPTION 2 -----
// send the message to each fcmToken
await getMessaging().sendAll(
fcmTokens.map((token) => ({ ...baseMessage, token }))
);
// --------------------
return true;
})
.catch((error) => {
console.log("Error sending messages:", error);
return false;
});
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
});
I found out that the culprit is my queries to db. Like #samthecodingman commented, I was creating floating Promises.
Originally, I have codes like:
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
All I needed to do is to return that call:
return db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
Although it's still not instant delivery, it's much faster now.
Error given: [Unhandled promise rejection: FirebaseError: Firebase Storage: String does not match format 'data_url': Must be formatted 'data:[][;base64], (storage/invalid-format)]
I am able to add image to the database, however it is not uploading to the firebase storage. I have changed it to different media types but no luck. I have also tired uploadBytes instead of uploadString and it works but no image displays. Any help would be appreciated!
const uploadImageTops = async () => {
const docRef = await addDoc(collection(db, "tops"), {
username: "user",
apparel: "tops",
color: "black",
size: size,
timeStamp: serverTimestamp(),
});
const imageRef = ref(storage, `tops/${docRef.id}`);
await uploadString(imageRef, cameraImage, "data_url", {contentType:'image/jpg'})
.then(async (snapshot) => {
const downloadURL = await getDownloadURL(imageRef);
await updateDoc(doc(db, "tops", docRef.id), {
imageUrl: downloadURL,
});
})
.then(navigation.navigate("Home"));
};
There's some issue with react-native and firebase when trying to upload the image. Blob is needed to upload image to firebase storage and firestore. Add that blob at the top and make sure you close the blob at the end.
This code worked for me.
const uploadImageTops = async () => {
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function (e) {
console.log(e);
reject(new TypeError("Network request failed"));
};
xhr.responseType = "blob";
xhr.open("GET", cameraImage, true);
xhr.send(null);
});
const docRef = await addDoc(collection(db, "tops"), {
username: user,
apparel: "tops",
color: color,
size: size,
timeStamp: serverTimestamp(),
});
const imageRef = ref(storage, `tops/${docRef.id}`);
const metadata = {
contentType: "image/jpeg",
};
await uploadBytes(imageRef, blob, metadata)
.then(async (snapshot) => {
const downloadURL = await getDownloadURL(imageRef);
await updateDoc(doc(db, "tops", docRef.id), {
imageUrl: downloadURL,
});
blob.close();
})
.then(navigation.navigate("Home"));
};
I've been having trouble viewing the image files I've uploaded to firebase and just noticed the issue is with the file type in firebase.
Two files in my firebase storage console. One uploaded from my IOS simulator (octet-stream) and the other uploaded directly into the console from the browser which uploads properly and is viewable.
Here are my select and upload functions:
_selectPhoto = async () => {
const status = await getPermission(Permissions.CAMERA_ROLL);
if (status) {
let imageName = "pic"
const result = await ImagePicker.launchImageLibraryAsync(options);
if (!result.cancelled) {
Animated.timing(this.animatedWidth, {
toValue: 600,
duration: 15000
}).start()
this.uploadImage(result.uri, imageName)
.then(() => {
this.props.navigation.navigate('Profile')
})
.catch((error) => {
Alert.alert('Must Sign In');
this.props.navigation.navigate('Login')
console.log(error);
})
}
}
};
uploadImage = async (uri, imageName) => {
const user = firebase.auth().currentUser;
const response = await fetch(uri);
const blob = await response.blob();
let storageRef = firebase.storage().ref().child(''images/'+user.displayName+'/'+imageName+'.jpg'');
const snapshot = await storageRef.put(blob);
blob.close();
snapshot.ref.getDownloadURL().then(function(downloadURL) {
console.log("File available at", downloadURL);
user.updateProfile({
photoURL: downloadURL.toString(),
}).then(function() {
console.log('saved photo')
}).catch(function(error) {
console.log('failed photo')
});
});
}
When I get the link in my console, it also has the media&token:
... .appspot.com/o/profile-pic.jpg?alt=media&token=56eb9c36-b5cd-4dbb-bec1-3ea5c3a74bdd
If I CMD+Click in VS Code I receive an error:
{
error: {
code: 400,
message: "Invalid HTTP method/URL pair."
}
}
So naturally, when I put that link in the browser it downloads a file with that name but says:
The file “pic.jpg” could not be opened.
It may be damaged or use a
file format that Preview doesn’t recognize.
Maybe it could be something with mediaTypes, but I'm not exactly sure how to use it.
mediaTypes : String -- Choose what type of media to pick. Usage:
ImagePicker.MediaTypeOptions., where is one of: Images,
Videos, All.
Thanks!
I've been fighting with this same issue for the past few days. I was finally able get images to upload and render as expected by following the Firebase Upload example in the Expo repo. I don't fully understand why it works, but it seems like Firebase doesn't like the blob that's generated by
const blob = await response.blob();
Try replacing the above with:
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onerror = function(e) {
console.log(e);
reject(new TypeError('Network request failed'));
};
xhr.responseType = 'blob';
xhr.open('GET', uri, true);
xhr.send(null);
});