Firebase: Calling Cloud Function From Cloud Function - javascript

I am running in to an issue with Firebase cloud functions. I have an onWrite cloud function that triggers a sequence of events. I have a path for requests that the onWrite cloud function is tied to. When that cloud function executes, it deletes the new request for the requests path and pushes the request in to a render path/que that will be used client side for rendering UI elements/data. Once the data has been written to the render path, I call a vanilla javascript function that is not tied to any cloud events. The vanilla javascript function is supposed to reach out to an external API and fetch some data to later be updated on the render object that was pushed in to the render path.
The problem is that the vanilla javascript function never executes. I have been looking all over the web to figure out why this happening but can't seem to figure out why. I am on the Flame plan so outbound api requests should be allowed to my knowledge. Here an example of my code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const request = require('request');
admin.initializeApp();
exports.requestModule = functions.database.ref('/requests').onWrite((change, context) => {
// Create reference to database
let db = admin.database();
if (context && context.auth && context.auth.uid) {
const afterData = change.after.val();
let uid = context.auth.uid;
let cleanData = afterData[uid];
cleanData.status = "loading";
// Remove the requested module from the requests path
let cleansePath = db.ref('/requests/' + uid);
cleansePath.remove().then((snapshot) => {
return true;
}).catch((error) => {
console.log(error);
return false;
});
// Add requested module to the render path
let renderPath = db.ref('/render/' + uid);
renderPath.push(cleanData).then((snapshot) => {
let val = snapshot.val();
let key = snapshot.key;
// Trigger the get weather api call
getWeather(uid, key, val);
return true;
}).catch((error) => {
console.log(error);
return false;
});
}
});
// Fetches data from external api
function getWeather (uid, key, obj) {
console.log('Fetching weather!');
let db = admin.database();
request('https://api.someweathersite.net/forecast/', (error, response, body) => {
if (!error && Number(response.statusCode) === 200) {
console.log('error:', error);
console.log('statusCode:', response && response.statusCode);
console.log('body:', body);
obj.data = body;
obj.status = 'loaded';
// Set data from api response in render object to be shown client side
let render = db.ref('/render/' + uid + '/' + key );
render.set(obj).then(() => {
return true;
}).catch((error) => {
console.log(error)
return false;
});
}
});
}
The console.log message at the top of the "getWeather" function never executes. I don't think that the "getWeather" function is ever executing.
If I put the api call directly in the onWrite "requestModule" function, the api call will work. However, when it calls an external function it never gets called/works. I basically want to have the "requestModule" function handle all requests and plan to have a module dispatcher that handles which module function/api data should be fetched from. That's why I don't want to keep the api call in the "requestModule" function. Any idea of why this happening or how I can get this working?

getWeather is performing asynchronous work to fetch some data, but it's not returning a promise to indicate when that work is complete. In fact, none of the async work you're performing here is correctly using the promises returned by the various API calls. It's not sufficient to simply use then() on each promise.
You need to keep track of all of the async work, and return a single promise that resolves only after all the work is complete. Otherwise, Cloud Functions may terminate and clean up your function before the work is complete. (Note that it's not deterministic which work may or may not actually complete before forced termination, but the only way to ensure that all work completes is through that single promise you return.)
You may want to watch my tutorials on using promises in Cloud Functions to get a better handle on what you're required to do make your functions work correctly: https://firebase.google.com/docs/functions/video-series/

Related

Firebase http cant set headers after request has been sent

I am creating an HTTP-callable function that makes contact with an API. However, I am getting an error with sending back a 200. I think it has something to do with a mistake I made in using asynchronous functions. Here is my code.
exports.organisationDataToTemp = functions.region('europe-west3').https.onRequest((req, res) => {
res.set('Access-Control-Allow-Origin', '*');
const GETparam = req.query.kvk;
const KvK = GETparam.toString();
//Test if KvK number is already in temp collection
const snapshot = db.collection('temp').where('information.kvkNumber', '==', KvK).get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
//This is where it needs to send the header and it fails. I do get here only when I need this code to run, but it still fails
console.log('kvk number already in temp collection');
res.status(200).send(doc.id);
return;
});
});
//Irrelevant code
//Make API call using the provided KvK number
const keyName = 'VitozFMIS';
const API_key = 'sdfghjqwertyuiopdfghytrdcvbjftyujnbvc';
//Call the first JSON
const _EXTERNAL_URL = 'https://api.kvk.nl/api/v2/testprofile/companies?kvkNumber=' + KvK + '&' + keyName + '=' + API_key + '&startPage=1';
fetch(_EXTERNAL_URL)
.then(response => response.json())
.then(data => {
const total = data.data.totalItems;
for(n=1;n<=total;n++){
const API = 'https://api.kvk.nl/api/v2/testprofile/companies?kvkNumber=' + KvK + '&' + keyName + '=' + API_key + '&startPage=' + n;
//irrelevant code
//Make the API call
fetch(API)
.then(resp => resp.json())
.then(data => {
//irrelevant code
});
}
});
//Return 200 if no errors occured
res.status(200).send(cleanupID);
return;
});
Normally, the code runs exactly as needed, but when the kvk number is already in the collection, it needs to send the document ID back in a 200. I am not sure, but I think it is because it sends another code before this one, but I do not understand why it fails. Does someone know what fails?
This line of code is always going to immediately return a 200 response, before anything else happens:
res.status(200).send(cleanupID);
That's because the Firestore and fetch APIs you're using are asynchronous and return immediately. The callbacks you provide execute some time later, after the results are available. Calling then does not block your code from continuing - it just establishes a callback to be run when the promise resolves.
Cloud Functions requires that sending the response must be the very last thing your function does, so you should wait until all of the work fully succeeds or fails before sending the response.
You will need to structure your code to use the callbacks from the promises to decide what to do. All of the logic much be inside the callback, or deferred to another callback by returning another promise. I strongly suggest finding a tutorial to learn how this works. It's essential for writing effective JavaScript. It's also highly valuable to learn async/await syntax to make promises easier to work with.

Aborting nested fetch requests when component is unmounting

The following function, when consumed, fetches and returns an array of up to 50 comments of a single post. It first fetches a single post by ID, and this post object has an array of comments ID's, which it will fetch.
My goal is to early abort this task in my React class component using componentWillUnmount, by calling abort on the signal on the class instance. The signal is passed as abortSignal.
The question is, I have a case of nested fetch requests. How should I approach this so I can make sure any on-going fetch requests are aborted when the component is unmounting? As I seee it, passing the signal to the outer fetch won't suffice if this stage was already completed. Should I create another signal inside of this function, and pass to to individual fetch?
const fetchComments = async (type, abortSignal) => {
const res = await fetch(endpoints[`${type}Stories`]);
const post = await res.json();
return Promise.all(post.slice(0, 50).map(async id => {
const url = endpoints.singleStory.replace('[id]', id);
const comment = await fetch(url);
return comment.json();
}));
}
I don't see any reason you can't reuse the abortSignal across all the fetch calls. You probably also want to check its aborted flag after awaiting so you bail proactively:
const fetchComments = async (type, signal) => {
const res = await fetch(endpoints[`${type}Stories`], {signal});
if (signal.aborted) {
return; // Or whatever
}
if (!res.ok) {
throw new Error("HTTP error " + res.status);
}
const post = await res.json();
if (signal.aborted) {
return; // Or whatever
}
return Promise.all(post.slice(0, 50).map(async id => {
// Probably not much point to checking `signal.aborted` here, you *just* checked it above
const url = endpoints.singleStory.replace('[id]', id);
const comment = await fetch(url, {signal});
if (!comment.ok) {
throw new Error("HTTP error " + comment.status);
}
return comment.json();
}));
}
Two notes on that:
I changed abortSignal to signal so I could use shorthand property notation when passing it to fetch, because I'm lazy. :-)
Your code is falling prey to the fetch footgun — you need to check for HTTP success, it only rejects on network error, not HTTP error. I've inserted checks above.
Note: If you can modify the API, I'd strongly recommend making it possible to ask for the 50 comments as part of the initial fetch, or at least to be able to ask for a batch of comments, rather than loading each comment individually with its own HTTP request (although HTTP/2 helps a lot).

Running query from inside Cloud Function using request parameters

I am having troubles running queries from Cloud Functions using the request parameters to build the query form HTTP calls. In the past, I have ran queries from cloud functions fine with no error. My problem arises when I try to run the query using parameters gotten from the request.
When I hardcode the location of the document in the function, it works fine but when I try to build a query, it returns status code of 200. I have also logged the the built query and it is logging out the right thing but no data is being returned. It only returns data when the document path is hardcoded. See code below.
Query looks like this
https://us-central1-<project-id>.cloudfunctions.net/getData/CollectionName/DocumentName
export const getData = functions.https.onRequest((request, response) => {
const params = request.url.split("/");
console.log("the params 0 "+params[0]);
console.log("the params 1 "+params[1]);
console.log("the params 2 "+params[2]);
//Build up the document path
const theQuery = "\'"+params[1]+"\/"+params[2]+"\'";
console.log("the query "+theQuery); <-- logs out right result in the form 'Collection/Document'
//Fetch the document
const promise = admin.firestore().doc("\'"+params[1]+"\/"+params[2]+"\'").get() <---- This doesnt work, building the query
//const promise = admin.firestore().doc('collectionName/DocID').get() <---- This hard coded and it works
promise.then(snapshot => {
const data = snapshot.data()
response.send(data)
}).catch(error => {
console.log(error)
response.status(500).send(error);
})
});
I tried using a different approach and giving the datafields a names as seen below
Query looks like this
https://us-central1-<project-id>.cloudfunctions.net/getData?CollectionName=CName&DocumentID=Dname
export const getData = functions.https.onRequest((request, response) => {
const collectName = request.query.CollectionName;
const DocId = request.query.DocumentName;
//Build up the document path
const theQuery = "'"+collectName+"\/"+collectName+"'";
console.log("the query "+theQuery); <---Logs out correct result
//Fetch the document
const promise = admin.firestore().doc(theQuery).get() <-- Building the query does not work
//const promise = admin.firestore().doc('collectionName/DocID').get() <---- This hard coded and it works
promise.then(snapshot => {
const data = snapshot.data()
response.send(data)
}).catch(error => {
console.log(error)
response.status(500).send(error);
})
});
In both cases, when the request is build from the URL, it does not return any data and it does not return any errors. And I am sure the documents I am trying to fetch exsist in the database. Am I missing anything ?
Try request.path. Then you can obtain the path components, e.g. request.path.split("/")[1]
The syntax for request.query is valid when using Express. This is referenced in some of the docs, but not made explicit that Express is required. It's confusing.
To properly handle the dynamic inputs, you may have more luck working with Express and creating routes and handlers. This Firebase page has links to some projects using it.
Walkthough set-up using Express on Firebase.

Need clarification on calling Meteor methods asynchronously

So i've been doing some reading and I think I have a general grasp on this subject but could use some insight from someone more experienced. I've been trying to write a simple RSS reader in Meteor and have been facing some issues with calling the Meteor method asynchronously. I currently define the method on the server(synchronously) and call it on the client(asynchronously). What I don't understand is that when I try to make the HTTP.call on the server, I return an undefined value passed to my client if I pass a callback into the request. But when I make the API request synchronously everything seems to work fine. Is this the normal behavior I should expect/the way I should be making the API call?
Meteor.methods({
getSubReddit(subreddit) {
this.unblock();
const url = 'http://www.reddit.com/r/' + subreddit + '/.rss';
const response = HTTP.get(url, {}, (err, res) => {
if(!err) {
//console.log(res.content);
return res;
} else {
return err;
}
});
}
});
Here's the method defined on the server side. Note that logging res.content shows that I'm actually getting the right content back from the call. I've tried reading some other answers on the topic and seen some things about using Future/wrapAsync, but I'm not sure I get it. Any help would be greatly appreciated!
The HTTP.get is doing async work, so callback passed to it will be called out of this meteor method call context.
To get desired result you should do it like this:
Meteor.methods({
getSubReddit(subreddit) {
// IMPORTANT: unblock methods call queue
this.unblock();
const url = 'http://www.reddit.com/r/' + subreddit + '/.rss';
const httpGetSync = Meteor.wrapAsync(HTTP.get);
try {
const response = httpGetSync(url, {});
//console.log(response.content);
return response.content;
} catch (err) {
// pass error to client
throw new Meteor.Error(...);
}
}
});

Firebase cloud function always timeout

I'm exploring the firebase cloud functions and I'm trying to send a notifications with an http request.
The problem is that even if I manage to send the notification, the request always goes timeout.
Here's my script
/functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.friendRequestNotification = functions.https.onRequest((req, res) => {
const senderId = req.query.senderId;
const recipientId = req.query.recipientId;
const getRecipientPromise = admin.database().ref(`/players/${recipientId}`).once('value');
const getSenderPromise = admin.database().ref(`/players/${senderId}`).once('value');
return Promise.all([getRecipientPromise, getSenderPromise]).then(results => {
const recipient = results[0];
const sender = results[1];
const recipientToken = recipient.child("notificationsInfo/fcmToken").val();
const notificationAuthorization = recipient.child("notificationsInfo/wantsToReceiveNotifications").val();
const recipientBadge = recipient.child("notificationsInfo/badgeNumber").val();
const senderUsername = sender.child("username").val();
const payload = {
notification: {
title: `FriendRequest`,
body: `You have a new friend request from ${senderUsername}!`,
badge: (recipientBadge+1).toString()
}
};
if (notificationAuthorization) {
return admin.messaging().sendToDevice(recipientToken, payload).then(response => {
});
}
return admin.database().ref(`/players/${recipientId}/notificationsInfo/badgeNumber`).setValue(recipientBadge+1);
});
});
Plus It seems that the badgeNumber in never updated, is that related to the timeout issue?
HTTP-triggered Cloud Functions work just like Express apps -- you have a response object (res) that you need to use to send something when the request is done. In this case, it looks like you could do something like:
return Promise.all([
/* ... */
]).then(() => {
res.status(200).send('ok');
}).catch(err => {
console.log(err.stack);
res.status(500).send('error');
});
#Michael Bleigh answer is perfectly fine for this question, let me add more in this for the future users.
As per firebase documentation:-
Use these recommended approaches to manage the lifecycle of your
functions:
Resolve functions that perform asynchronous processing (also known as
"background functions") by returning a JavaScript promise.
Terminate HTTP functions with res.redirect(), res.send(), or res.end(). (The case in this question.)
Terminate a synchronous function with a return; statement.
Note
It's important to manage the lifecycle of a function to ensure that it resolves properly. By terminating functions correctly, you can avoid excessive charges from functions that run for too long or loop infinitely. Also, you can make sure that the Cloud Functions instance running your function does not shut down before your function successfully reaches its terminating condition or state.
You need a paid plan (Blaze, pay as you go) to access external APIs.
You might see below warning in firebase functions log if the billing account is not configured.
Billing account not configured. External network is not accessible and
quotas are severely limited. Configure billing account to remove these
restrictions
Check this link for more information.

Categories

Resources