AWS Lambda - unable to make call to 3rd resource - node.js

I have Lambda Node function behind API Gateway, what I am trying to do is:
Make call to API
Do some logic
Send a message to Slack
Problem:
When I run default test request, the function finishes successfully but it doesn't send a message to Slack (logs only print Slack and not HAHA1/2),however, it does when I run this function from local machine which leads me to believe that AWS stops any none client-server traffic. My function is not in VPC. What can I do to allow outgoing traffic to Slack? Thanks.
const AWS = require('aws-sdk');
const Slack = require('slack-node');
const dynamo = new AWS.DynamoDB.DocumentClient();
/**
* Demonstrates a simple HTTP endpoint using API Gateway. You have full
* access to the request and response payload, including headers and
* status code.
*
* To scan a DynamoDB table, make a GET request with the TableName as a
* query string parameter. To put, update, or delete an item, make a POST,
* PUT, or DELETE request respectively, passing in the payload to the
* DynamoDB API as a JSON body.
*/
sendSlack()
function sendSlack() {
webhookUri = "https://hooks.slack.com/services/....";
slack = new Slack();
slack.setWebhook(webhookUri);
console.log('Slack');
slack.webhook({
channel: "...",
username: "...",
text: "..."
}, function(err, response) {
console.log("HAHA1");
console.log(response);
console.log("HAHA2");
});
}
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
sendSlack()
let body;
let statusCode = '200';
const headers = {
'Content-Type': 'application/json',
};
try {
switch (event.httpMethod) {
case 'DELETE':
body = await dynamo.delete(JSON.parse(event.body)).promise();
break;
case 'GET':
body = await dynamo.scan({ TableName: event.queryStringParameters.TableName }).promise();
break;
case 'POST':
body = await dynamo.put(JSON.parse(event.body)).promise();
break;
case 'PUT':
body = await dynamo.update(JSON.parse(event.body)).promise();
break;
default:
throw new Error(`Unsupported method "${event.httpMethod}"`);
}
} catch (err) {
statusCode = '400';
body = err.message;
} finally {
body = JSON.stringify(body);
}
return {
statusCode,
body,
headers,
};
};

You will need to wait for the slack call by either providing a callback or promisifying it:
function sendSlack() {
return new Promise((resolve, reject) => {
webhookUri = "https://hooks.slack.com/services/....";
slack = new Slack();
slack.setWebhook(webhookUri);
console.log('Slack');
slack.webhook({
channel: "...",
username: "...",
text: "..."
}, function(err, response) {
if(err) return reject(err)
resolve(response)
});
})
}
Then in your code
await sendSlack()

It could be that your lambda handler is completing before your call to Slack is successfully executed.
Since the call to Slack is async, I would suggest you try to await its response before your handler returns a response.
I'm not 100% certain, by my guess is that the async Slack call gets scheduled but not run until the main body of the function has completed, at which point the lambda is "spun down" and nothing further is run.

Related

Cloud Run Readablestream takes 5 minutes to cancel enqueue

I created an adapter-node Sveltekit API endpoint, which streams quotes using a readable stream. When I quit the client route The streaming has to stop. This works fine in development using Sveltekit "npm run dev" (vite dev) or using a windows desktop container (node build).
onDestroy(async () => {
await reader.cancel(); // stop streaming
controller.abort(); // signal fetch abort
});
But when I build and deploy the node container on Google Cloud Run the streaming works fine. Except when I quit the client route: the API endpoint keeps on streaming. The log shows: enqueus for 5 more minutes followed by a delayed Readablestream cancel() on the API server.
Why this 5 minutes between the client cancel / abort and the cancel on the server?
The API +server.js
import { YahooFinanceTicker } from "yahoo-finance-ticker";
/** #type {import('./$types').RequestHandler} */
export async function POST({ request }) {
const { logging, symbols } = await request.json();
const controller = new AbortController();
const ticker = new YahooFinanceTicker();
ticker.setLogging(logging);
if (logging) console.log("api ticker", symbols);
const stream = new ReadableStream({
start(controller) {
(async () => {
const tickerListener = await ticker.subscribe(symbols);
tickerListener.on("ticker", (quote) => {
if (logging) console.log("api", JSON.stringify(quote, ["id", "price", "changePercent"]));
controller.enqueue(JSON.stringify(quote, ["id", "price", "changePercent"]));
});
})().catch(err => console.error(`api listen exeption: ${err}`));
},
cancel() { // arrives after 5 minutes !!!
console.log("api", "cancel: unsubscribe ticker and abort");
ticker.unsubscribe();
controller.abort();
},
});
return new Response(stream, {
headers: {
'content-type': 'text/event-stream',
}
});
}
Route +page.svelte
const controller = new AbortController();
let reader = null;
const signal = controller.signal;
async function streaming(params) {
try {
const response = await fetch("/api/yahoo-finance-ticker", {
method: "POST",
body: JSON.stringify(params),
headers: {
"content-type": "application/json",
},
signal: signal,
});
const stream = response.body.pipeThrough(new TextDecoderStream("utf-8"));
reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (logging) console.log("resp", done, value);
if (done) break;
... and more to get the quotes
}
} catch (err) {
if (!["AbortError"].includes(err.name)) throw err;
}
}
...
The behavior you are observing is expected, Cloud Run does not support client-side disconnects yet.
It is mentioned in this article, that
Cloud Run (fully managed) currently only supports server-side
streaming. Having only "server-side streaming" basically means when
the "client" disconnects, "server" will not know about it and will
carry on with the request. This happens because "server" is not
connected directly to the "client" and the request from the "client"
is buffered (in its entirety) and then sent to the "server".
You can also check this similar thread
It is a known issue, there is already a public issue exists for the same. You can follow that issue for future updates and also add your concerns there.

Google Cloud Function failed with timeout

I have a Pub/Sub triggered cloud function that calls an API end-point and logs the message. But I am not seeing all log messages being logged in console except everything right before calling API.
Once the API is called I am logging the response, and exception messages in case of any error.
It is logging: Function execution took 120015 ms. Finished with status: timeout Earlier the default timeout was set to 60 sec, later I increased it to 120 sec. Still the problem persist.
I am not understanding the issue here since it is working locally without any issues.
Here I have custom module to log messages to Winston and GCP console (it doesn't have any issue and working fine).
Code calling the API module:
const console = require('./logging-utils');
const portal_api = require('./api-utils');
exports.triggerPortalNotifier = async (event, context) => {
try {
/*
.....
*/
console.metadata.cloudFunction = cf_name;
console.metadata.requestId = requestId;
console.metadata.organizationId = organizationId;
console.metadata.instanceId = instanceId;
console.logMessage(`Event received with payload: some message`);
var payload = {
//payload to API
}
var response = await portal_api.notifyPortal(payload);
console.logMessage(`Response received from portal API is: ${JSON.stringify(response.data)}`);
}
else {
throw new Error(`Invalid message received: ${_message}`);
}
}
catch (error) {
console.logMessage(`Portal API failed with exception: ${error}`);
throw new Error(`${error.message}`);
}
}
Code that make API request (using axios module)
require('dotenv').config();
const axios = require('./axios-instance');
const console = require('./logging-utils');
const nextgen_api = {
notifyPortal: async (payload) => {
try {
const config = {
headers: {
'Authorization': process.env.PORTAL_AUTHORIZATION_TOKEN,
'Content-Type': "application/json",
'Accept': "application/plain"
}
}
console.logMessage(`Input paylod for API end-point: ${process.env.PORTAL_API} => ${JSON.stringify(payload)}`)
const response = await axios.post(process.env.PORTAL_API, JSON.parse(JSON.stringify(payload)), config);
console.logMessage(`Response from API: ${JSON.stringify(response.data)}`);
return response;
}
catch (err) {
if (err.response && err.response.status !== 200) {
console.logMessage(`API call failed with status code: ${err.response.status} `);
throw new Error(`API call failed with status code: ${err.response.status} `);
}
else {
console.logMessage(`API call failed with ${err.stack}`);
throw new Error(`API call failed with status code: ${err.stack} `);
}
}
}
}
module.exports = my_api;
Message Response from API: ${JSON.stringify(response.data)} is not being logged.
Any help here is appreciated.

Twilio: forward a call to a flow in Twilio

I want to fordward a call to a Studio Flow after the agent in flex hangs up so a CSAT survey can play for the user.
I created a plugin that calls a function inside Twilio but there is a "Error - 11200" after the forwarding is done.
I replaced the hang up action so it redirects the call to a function in twilio. The function is supossed to send the call to a flow that will play the survey. I suspect the problem has to do with authentication but I can't find much about it.
I'm fairly new to twilio, so any help will be greatly appreciated
This is the part of the plugin that calls the function:
flex.Actions.replaceAction("HangupCall", (payload) => {
console.log('task attributes: ' + JSON.stringify(payload.task.attributes));
if (payload.task.attributes.direction === "inbound") {
// Describe the body of your request
const body = {
callSid: payload.task.attributes.call_sid,
callerId: payload.task.attributes.from,
destination: '+18xxxxxxxx',
Token: manager.store.getState().flex.session.ssoTokenPayload.token
};
// Set up the HTTP options for your request
const options = {
method: 'POST',
body: new URLSearchParams(body),
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
};
// Make the network request using the Fetch API
fetch('https://TWILIO INSTANCE.twil.io/TransferUtil', options)
.then(resp => resp.json())
.then(data => console.log(data));
} else {
original(payload);
}
});
And this is the function in twilio:
const TokenValidator = require('twilio-flex-token-validator').functionValidator;
exports.handler = TokenValidator(async function(context, event, callback) {
const response = new Twilio.Response();
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
response.appendHeader('Content-Type', 'application/json');
const client = require('twilio')();
const callSid = event.callSid;
const callerId = event.callerId;
const destination = event.destination;
console.log('Call Sid:', callSid);
console.log('Transfer call from:', callerId, 'to:', destination);
try {
let url = 'https://studio.twilio.com/v2/Flows/FWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Executions';
let call = await client.calls(callSid).update({method: 'POST', url: encodeURI(url)});
console.log(JSON.stringify(call));
response.appendHeader('Content-Type', 'application/json');
response.setBody(call);
callback(null, response);
}
catch (err) {
response.appendHeader('Content-Type', 'plain/text');
response.setBody(err.message);
console.log(err.message);
response.setStatusCode(500);
callback(null, response);
}
});
EDIT:
In the error Log I get this information:
Argh, I read the error wrong. There isn't anything wrong with the Function, the error is coming from the call trying to make a webhook request to the URL https://studio.twilio.com/v2/Flows/FWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Executions. That's the REST API trigger and needs to be requested in the same way as any other API request using your account credentials or API keys.
You should set that URL to the webhook trigger URL, which looks like https://webhooks.twilio.com/v1/Accounts/${ACCOUNT_SID}/Flows/${FLOW_SID}. Then the call will be able to request it as part of a normal webhook flow.

Twilio programmable SMS not sending in deployed Lambda function

I am working on a Serverless AWS service that uses Twilio's programmable SMS to deliver text messages.
My setup is consistently delivering messages successfully when I run the stack locally (e.g. sls offline start), but in the deployed environment I seem to be unable to even call the method on the Twilio client.
Here's how the message delivery is set up:
const twilio = require('twilio');
const twilioClient = twilio(
process.env.TWILIO_SID,
process.env.TWILIO_TOKEN,
{
lazyLoading: true,
}
);
export function sendMessage(user, message) {
twilioClient.messages.create({
from: process.env.TWILIO_NUMBER,
to: user.phone,
body: message,
}, function(err, message) {
console.log('error', err);
console.log('message', message);
});
}
// And then usage in a Serverless Function Handler
function example(event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
// user is also determined here
sendMessage(user, 'This is a message');
return {
body: JSON.stringify({}),
statusCode: 200
};
}
Locally, running this works and I am able to see the output of the message log, with nothing on the error log. However, when deployed, running this yields nothing -- the method appears to not even get called (and I can verify in the Twilio logs that no API call was made), therefor no error or message logs are made in the callback.
In debugging I've tried the following:
I've logged all the environment variables (Twilio SSID, auth token, phone number), as well as the function arguments, and they all appear to be in place. I also checked out the Lambda function itself to make sure the environment variables exist.
I've examined my CloudWatch logs; no errors or exceptions are logged. Other than the Twilio method not getting called the Lambda function is executed without issue.
I've tried logging things like twilio and twilioClient.messages.create to make sure the client and function definition didn't get wiped out somehow.
I thought maybe it had to do with context.callbackWaitsForEmptyEventLoop so I changing it from false to true.
I have come up empty, I can't figure out why this would be working locally, but not when deployed.
Edit: per the Twilio client example, if you omit the callback function the method will return a Promise. I went ahead and tried to await the response of the method:
export function sendMessage(user, message) {
return twilioClient.messages.create({
from: process.env.TWILIO_NUMBER!,
to: user.phone,
body: message,
});
}
// Usage...
async function example(event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
try {
const message = await sendMessage(user, 'This is a message');
console.log('message', message)
} catch (error) {
console.log('error', error);
}
return {
body: JSON.stringify({}),
statusCode: 200
};
}
In this example the Lambda function is successful, but neither the message nor the error are logged.
I tried this and it works. I tried to make my code as similar to use, with a few changes.
const twilio = require('twilio');
const twilioClient = twilio(
process.env.TWILIO_SID,
process.env.TWILIO_TOKEN
);
let user = '+14075551212';
function sendMessage(user, message) {
return twilioClient.messages.create({
from: process.env.TWILIO_NUMBER,
to: user,
body: message,
});
}
exports.handler = async function(event, context, callback) {
try {
const message = await sendMessage(user, 'This is a message');
console.log('message', message);
callback(null, {result: 'success'});
} catch (error) {
console.log('error', error);
callback("error");
}
};

Set context in Dialogflow through Promise Request

I am having a problem with the contexts of dialogflow, when trying to change them through fulfillment.
When I want to add (or clear, by setting lifespanCount to -1) a context I generally use this code:
const {WebhookClient} = require('dialogflow-fulfillment');
const agent = new WebhookClient({ request, response });
/*Irrelevant Code Here*/
var context=agent.session+"/contexts/contextName";
agent.setContext({
"name":context,
"lifespanCount": 1
});
If I use this snippet, the contexts can be successfully manipulated. However, I am now trying to use this snippet when sending a request-promise and, based on the response of that request, I want to have different outputContexts. For some reason, this action doesn't have any results, I cannot clear the contexts or create one, as these don't appear on the Firebase logs when I print JSON.stringify(request.body).
return new Promise( (resolve,reject) => {
const request = require('request-promise-native');
const options = {
uri: 'link',
method: 'POST',
headers: {
'api-token': 'token'
},
body: {
someData:"someData",
},
json: true
};
request(options)
.then(function (body) {
if (body.status===0)
agent.add("Option 1");
else {
var sessionIdArray=agent.session.split("/");
var sessionId=sessionIdArray[sessionIdArray.length-1];
var context=agent.session+"/contexts/contextName";
agent.setContext({
"name":context,
"lifespanCount": 1
});
agent.add("type something");
}
resolve();
})
.catch(function (err) {
// POST failed...
agent.add("An error happened during your last call.");
console.log(err.stack);
resolve();
});
});
It's like the promise-request prevents the agent to set the context. Does anyone know how to make this work? Thanks in advance!

Resources