Why is using Firebase Functions as an API endpoint so slow? - node.js

I built an app in Slack, that on interactions in Slack, will send an HTTP POST request to a URL. That URL is a Firebase Function that is triggered with an HTTP request.
The Firebase Function looks like this...
// process incoming shortcuts
exports.interactions = functions.https.onRequest(async (request, response) => {
response.send();
const payload = JSON.parse(request.body.payload);
functions.logger.log(payload);
if (payload.type === 'shortcut') {
functions.logger.log('Found a shortcut...');
const shortcuts = require('./shortcuts');
await shortcuts(payload);
} else if (payload.type === 'block_actions') {
functions.logger.log('Found a block action...');
const blockActions = require('./blockActions');
await blockActions(payload);
} else if (payload.type === 'view_submission') {
functions.logger.log('Found a view submission...');
const viewSubmissions = require('./viewSubmissions');
await viewSubmissions(payload);
}
functions.logger.log('Done with interactions.');
});
The problem is, is that Firebase is taking 5-10 seconds to respond, and Slack is expecting a response in 3 seconds.
So the app in Slack erroring out.

It turns out while I thought it would be useful to do a response.send() immediately when the function was called so that Slack had its instant response, I was then also inadvertently starting background activities in Firebase.
The line in the above Firebase docs that gave me the biggest clue was:
Background activity can often be detected in logs from individual invocations, by finding anything that is logged after the line saying that the invocation finished.
Which I found here... the function started, and completed, and then the code to open a modal began to be executed...
I then found in the Firebase docs
Terminate HTTP functions with res.redirect(), res.send(), or res.end().
So all I really had to do was move response.send() to the end of the function. Also I had to make sure that I had await statements before my async functions, so that async functions waited to be resolved before executing the final response.send()
// process incoming shortcuts
exports.interactions = functions.https.onRequest(async (request, response) => {
const payload = JSON.parse(request.body.payload);
functions.logger.log(payload);
if (payload.type === 'shortcut') {
functions.logger.log('Found a shortcut...');
const shortcuts = require('./shortcuts');
await shortcuts(payload);
} else if (payload.type === 'block_actions') {
functions.logger.log('Found a block action...');
const blockActions = require('./blockActions');
await blockActions(payload);
} else if (payload.type === 'view_submission') {
functions.logger.log('Found a view submission...');
const viewSubmissions = require('./viewSubmissions');
await viewSubmissions(payload);
}
functions.logger.log('Done with interactions.');
response.send();
});
The modal interaction response times in Slack are much quicker and usable now.

Related

Proper way to report error back in Firebase Functions HTTPS request in nodejs

I implemented a Firebase function to be called plainly on HTTPS via browser (I use postman for testing) in node.js :
exports.notifToAdmin = functions.https.onRequest((request, response) => {
const title = request.query.title
const body = request.query.body
const badge = request.query.badge
if (typeof title === 'undefined') { return response.status(500).send("title missing") }
if (typeof body === 'undefined') { return response.status(500).send("body missing") }
if (typeof badge === 'undefined') { return response.status(500).send("badge missing") }
notifications.sendNotifToAdmin(title, body, badge)
.then(message => {
const ackString = fingerPrint(msg);
return response.send(ackString);
})
.catch(error => {
console.error(error);
return response.status(500).send(error);
});
});
am I using a correct way to send errors back to the caller (via the response.status(500).send("...."))? In the Firebase errors documentation I see the usage of throw new Error(...). So I am unsure if what I do is the most optimal way? I did notice the doc saying //Will cause a cold start if not caught(linked to this throw error), I don't want to restart anything just report an error to the caller...
I know that the onRequest result should be a promise should I change my code and put a return in front of the notifications.SendNotifToAdmin(...) (this returns a promise) but how does this add up with the return response.send(...)? Is this also returning a promise then?
am I using a correct way to send errors back to the caller (via the response.status(500).send("...."))
Yes, that is standard for HTTP type functions that need to send an HTTP status code. But you should send a 4xx range HTTP codes for errors that are related to the client sending incorrect information.
I know that the onRequest result should be a promise
There is absolutely no obligation for an onRequest type function to return a promise. The function just needs to send a response after all promises are resolved so that the async work can complete before the function is terminated when the response is delivered.

Reading Firebase data prevents async method from resolving

I'm trying to write an Alexa skill which reads from Firebase
I'm in a position where I have a NodeJS method that gets called when I use the Alexa test console, but if I add in code to retrieve data from Firebase the method hangs until the lambda times out
const HelloWorldIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'plantsIntent';
},
async handle(handlerInput) {
const snapshot = (await db.collection('plants').get()).data();
const names = snapshot.docs.map(doc => doc.data().name);
const speakOutput = 'Get yourself some cool plants like' + names.join(' and ');
console.log(speakOutput);
var response = handlerInput.responseBuilder
.speak(speakOutput)
//.reprompt('add a reprompt if you want to keep the session open for the user to respond')
.getResponse();
console.log(response);
return response;
}
};
When I run this code I get both the speakOutput string and response object output into the logs, so I know the code is managing to get that far
I'm suspicious it's something to do with Firebase as if I remove the db.collection('plants').get() snippet (and the associated variables) then the code runs to completion
I'm suspicious that it's to do with the method not returning rather than an exception happening, because the output for response is the same in the working version (without Firebase .get()), and the non-working version
Any help would be appreciated!

Nodejs Telegram Bot: sendChatAction while waiting for network call

I am writing a Telegram bot using the Telegraf framework in Nodejs, and would like to display the 'bot is typing...' banner while the bot is performing a networking call.
The issue that I have is that my function isn't executed like how it is supposed to be, ie the 'typing' banner appears and disappears after 5 seconds (as per the docs), but the invoice never gets sent until at least 30 seconds later. Assessing the logs also shows that the console logs were executed after the function ends.
Here is my implementation:
module.exports = (bot) => bot.command('buy', (ctx) => {
let catalogueId = //some regex
return Promise.all([
ctx.replyWithChatAction('typing'),
sendInvoice(catalogueId, ctx)
])
})
function sendInvoice(catalogueId, ctx) {
console.log('1')
return helper.getItem(catalogueId)
.then((item) => {
console.log('2')
return createInvoice(item, ctx)
.then((invoice) => {
console.log('3')
return ctx.replyWithInvoice(invoice)
})
})
}
The log statements as seen in my Firebase Cloud Functions are like so:
As you can see, the console logs are printed after the function has ended, and are at almost 15 seconds apart. Other attempts:
//Attempt 2, same result as above
module.exports = (bot) => bot.command('buy', (ctx) => {
let catalogueId = //some regex
return ctx.replyWithChatAction('typing')
.then(() => sendInvoice(catalogueId, ctx))
})
//Attempt 3, log statements are printed before function ends, but the 'typing' only shows at the end. Need it to show when the networking calls starts
module.exports = (bot) => bot.command('buy', (ctx) => {
let catalogueId = //some regex
return sendInvoice(catalogueId, ctx))
})
function sendInvoice(catalogueId, ctx) {
console.log('1')
return helper.getItem(catalogueId)
.then((item) => {
console.log('2')
return createInvoice(item, ctx)
.then((invoice) => {
console.log('3')
return ctx.replyWithChatAction('typing')
.then(() => ctx.replyWithInvoice(invoice))
})
})
}
My bot is currently deployed on Firebase Cloud Function.
I had a similar problem with a bot running on AWS Lambda. My bot would send the typing chat action correctly to the chat, then all of a sudden the lambda function would quit and none of my other code would be processed.
I'm not an expert but I think that as soon as the handler function you provide to Firebase Cloud Function (or Lambda) returns something to the client that initiated your request, Firebase considers the request complete and shuts down your function invocation. The default webhook callback that Telegraf provides sends a response to the client when your bot replies with a chat action or a message. Consequently, if you export that default webhook reply as your handler (as I did) then your handler will return a response as soon as your bot replies with anything, including a chat action, and the rest of your code may not be processed.
I solved my problem by replacing this:
export const handler = makeHandler(bot.webhookCallback("/"))
with this:
export const handler = async (event, context, callback) => {
const tmp = JSON.parse(event.body) // get data passed to us
await bot.handleUpdate(tmp) // make Telegraf process that data
return callback(null, {
// return something for webhook, so it doesn't try to send same stuff again
statusCode: 200,
body: "",
})
}
(copied from this excellent github comment)
As you can see, instead of returning whenever the bot's webhook callback returns something, I'm now waiting until the update is completely handled and only returning afterward. Thus I can reply with a chat action, wait for my code to do some processing, reply with a message, wrap things up, and only then will my function invocation stop processing.

Why is Express res.json() holding up Google Sheets API for +30 seconds

I am building an app that looks up a word definition in a Google sheet from a Slack slash command. The app is hosted in Google Cloud Functions and written in Node.js.
To make the 3000ms time limit on slack commands, the app
posts an immediate 200 OK response, then
do the lookup in Sheets, and finally,
returns the full reply via Slack's request_url as defined in the Slack documentation.
So far, so good. But here's the kicker:
When I call res.json() in my main function glossary, Slack get's an initial reply, but the sendMessageToSlackResponseURL() isn't called for another 10-40 seconds. I eventually get the reply in Slack as expected, albeit painfully slow.
I have narrowed it down (by an embarrassing amount of console.log() calls) to the line:
const reply = (await sheets.spreadsheets.values.get(request)).data.values;
This command takes 2-3 seconds to run if res.json() is not called prior - barely making the Slack time limit. But if res.json() is called prior. This command takes up to 40 seconds.
How is the Google Sheets API call affected by a prior res.json() call? What am I missing?
// Simplified code pasted below:
exports.glossary = async (req, res) => {
// Give immediate response to prevent 3000ms Slack timeout.
res.json(initSlackResponse(req.body.text)); //Commenting out this line speeds up the app
// Get glossary result from Google Sheet
let response = await getGlossaryResults(query);
// Return late response
await sendMessageToSlackResponseURL(req.body.response_url, response);
return Promise.resolve();
};
const getGlossaryResults = async (query) => {
const content = await readFile(CREDENTIALS_PATH);
let oAuth2Client = await authorize(JSON.parse(content));
const request = {
spreadsheetId: spreadsheetId,
range: range,
auth: oAuth2Client
};
//The following command takes 10-40 seconds to run if res.json(initSlackResponse(query)); has been called.
//If res.json() is *not* called, the command takes 2-3 seconds.
const reply = (await sheets.spreadsheets.values.get(request)).data.values;
// *Generate the results here
return results;
};
function sendMessageToSlackResponseURL(responseURL, JSONmessage) {
let postOptions = {
uri: responseURL,
method: 'POST',
headers: {
'Content-type': 'application/json'
},
json: JSONmessage
};
request(postOptions, (error) => {
if (error){
console.error(error);
}
});
return Promise.resolve();
}
const initSlackResponse = (query) => {
return {
// *Build simple json object here
};
};

How to get data from a RESTful API with dialogflow

i am struggling a bit with my google assistant action. Right now i am using Dialogflow and Firebase for my webhook. In my code i would like to get data from an API, for example this one: API. I am coding with Node.js by the way. Since Node is asynchronous i do not know how to get the Data. When i try to make an Callback it doesnt work e.g.:
app.intent(GetData, (conv) => {
var test= "error";
apicaller.callApi(answer =>
{
test = answer.people[0].name
go()
})
function go ()
{
conv.ask(`No matter what people tell you, words and ideas change the world ${test}`)
}
For some reason this works when i test it in an other application. With Dialogflow it does not work
I have also tried to use asynch for the function app.intent and tried it with await but this did not work too.
Do you have any idea how i could fix this?
Thank you in advance and best regards
Luca
You need to return Promise like
function dialogflowHanlderWithRequest(agent) {
return new Promise((resolve, reject) => {
request.get(options, (error, response, body) => {
JSON.parse(body)
// processing code
agent.add(...)
resolve();
});
});
};
See following for details:
Dialogflow NodeJs Fulfillment V2 - webhook method call ends before completing callback
If this works in another application then I believe you're getting an error because you're attempting to access an external resource while using Firebases free Spark plan, which limits you to Google services only. You will need to upgrade to the pay as you go Blaze plan to perform Outbound networking tasks.
Due to asynchronicity, the function go() is gonna be called after the callback of callapi been executed.
Although you said that you have tried to use async, I suggest the following changes that are more likely to work in your scenario:
app.intent(GetData, async (conv) => {
var test= "error";
apicaller.callApi(async answer =>
{
test = answer.people[0].name
await go()
})
async function go ()
{
conv.ask(`No matter what people tell you, words and ideas change the world ${test}`)
}
First follow the procedure given in their Github repository
https://github.com/googleapis/nodejs-dialogflow
Here you can find a working module already given in the README file.
You have to make keyFilename object to store in SessionsClient object creation (go at the end of post to know how to genearate credentials file that is keyFileName). Below the working module.
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//////////////////////////////////////////////////////////////////
const dialogflow = require("dialogflow");
const uuid = require("uuid");
/**
* Send a query to the dialogflow agent, and return the query result.
* #param {string} projectId The project to be used
*/
async function runSample(projectId = "<your project Id>") {
// A unique identifier for the given session
const sessionId = uuid.v4();
// Create a new session
const sessionClient = new dialogflow.SessionsClient({
keyFilename: "<The_path_for_your_credentials_file>"
});
const sessionPath = sessionClient.sessionPath(projectId, sessionId);
// The text query request.
const request = {
session: sessionPath,
queryInput: {
text: {
// The query to send to the dialogflow agent
text: "new page",
// The language used by the client (en-US)
languageCode: "en-US"
}
}
};
// Send request and log result
const responses = await sessionClient.detectIntent(request);
console.log("Detected intent");
console.log(responses);
const result = responses[0].queryResult;
console.log(` Query: ${result.queryText}`);
console.log(` Response: ${result.fulfillmentText}`);
if (result.intent) {
console.log(` Intent: ${result.intent.displayName}`);
} else {
console.log(` No intent matched.`);
}
}
runSample();
Here you can obtain the keyFileName file that is the credentials file using google cloud platform using
https://cloud.google.com/docs/authentication/getting-started
For complete tutorial (Hindi language) watch youtube video:
https://www.youtube.com/watch?v=aAtISTrb9n4&list=LL8ZkoggMm9re6KMO1IhXfkQ

Resources