In the workflow of my application AppSync will delivery user inputs (as messages) to an SQS queue, that will trigger a lambda for async processing.
Lambda will validate messages against the GraphQLInputType. For that purpose, I'm using the function coerceValue of graphql-js. Got the tip from this question How can I use isValidJSValue to validate a query variable in my client code?, exactly what I needed.
The problem is that the validation code run flawlessly when running local tests within nodejs, but when deployed to AWS the very same code fails with the message:
["Expected type CreateBookInput to be an object."].
Apparently the line schema.getType(inputType) is returning only a 'string' when running on aws.
Already tried with both functions 'coerceValue' and 'isValidJSValue' but no success. Also tried to Promisify my function and use async/await style but didn't make any difference, same error.
function validate(object, inputType) {
let schema = loadSchema()
let validationResult = coerceValue(object, schema.getType(inputType))
if (validationResult.errors) {
throw new Error(`GraphQL: ${validationResult.errors}`)
}
return true
}
function loadSchema() {
let schemaFile = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf8'),
awsTypesSchema = fs.readFileSync(path.join(__dirname, 'aws.graphql'), 'utf8')
return buildSchema(schemaFile += awsTypesSchema)
}
When running locally I can see that schema.getType(inputType) return a GraphQLInputObjectType but the behavior differs on AWS Lambda, there in the logs it only return the name of input type, e.g. "CreateBookInput".
Did anyone had a similar problem? Any help is welcome.
Actually, there was nothing to do with GraphQL libs or lambda at all, the problem was because of a missing JSON.parse on SQS message body, that way I was trying to validate a 'string', not an object.
Related
I started implementing a slash-command which kept evolving and eventually might hit the 3-second slack response limit. I am using serverless-stack with Node and TypeScript. With sst (and the vscode launchfile) it hooks and attaches the debugger into the lambda function which is pretty neat for debugging.
When hitting the api endpoint I tried various methods to send back an acknowledgement to slack, do my thing and send a delayed message back without success. I didnt have much luck finding info on this but one good source was this SO Answer - unfortunetly it didn't work. I didn't use request-promise since it's deprecated and tried to implement it with vanilla methods (maybe that's where i failed?). But also invoking a second lambda function from within (like in the first example of the post) didn't seem to be within the 3s limitation.
I am wondering if I am doing something wrong or if attachinf the debugger is just taking to long etc.
However, before attempting to send a delayed message it was fine including accessing and scaning dynamodb records, manipulating the results and then responding back to slack while debugger attached without hitting the timeout.
Attempting to use a post
export const answer: APIGatewayProxyHandlerV2 = async (
event: APIGatewayProxyEventV2, context, callback
) => {
const slack = decodeQueryStringAs<SlackRequest>(event.body);
axios.post(slack.response_url, {
text: "completed",
response_type: "ephemeral",
replace_original: "true"
});
return { statusCode: 200, body: '' };
}
The promise never resolved, i guess that once hitting return on the function the lambda function gets disposed and so the promise?
Invoking 2nd Lambda function
export const v2: APIGatewayProxyHandlerV2 = async (
event: APIGatewayProxyEventV2, context, callback
): Promise<any> => {
//tried with CB here and without
//callback(null, { statusCode: 200, body: 'processing' });
const slack = decodeQueryStringAs<SlackRequest>(event.body);
const originalMessage = slack.text;
const responseInfo = url.parse(slack.response_url)
const data = JSON.stringify({
...slack,
})
const lambda = new AWS.Lambda()
const params = {
FunctionName: 'dev-****-FC******SmE7',
InvocationType: 'Event', // Ensures asynchronous execution
Payload: data
}
return lambda.invoke(params).promise()// Returns 200 immediately after invoking the second lambda, not waiting for the result
.then(() => callback(null, { statusCode: 200, body: 'working on it' }))
};
Looking at the debugger logs it does send the 200 code and invokes the new lambda function though slack still times out.
Nothing special happens logic wise ... the current non-delayed-message implementation does much more logic wise (accessing DB and manipulating result data) and manages not to timeout.
Any suggestions or help is welcome.
Quick side note, I used request-promise in the linked SO question's answer since the JS native Promise object was not yet available on AWS Lambda's containers at the time.
There's a fundamental difference between the orchestration of the functions in the linked question and your own from what I understand but I think you have the same goal:
> Invoke an asynchronous operation from Slack which posts back to slack once it has a result
Here's the problem with your current approach: Slack sends a request to your (1st) lambda function, which returns a response to slack, and then invokes the second lambda function.
The slack event is no longer accepting responses once your first lambda returns the 200. Here lies the difference between your approach and the linked SO question.
The desired approach would sequentially look like this:
Slack sends a request to Lambda no. 1
Lambda no. 1 returns a 200 response to Slack
Lambda no. 1 invokes Lambda no. 2
Lambda no. 2 sends a POST request to a slack URL (google incoming webhooks for slack)
Slack receives the POST requests and displays it in the channel you chose for your webhook.
Code wise this would look like the following (without request-promise lol):
Lambda 1
module.exports = async (event, context) => {
// Invoking the second lambda function
const AWS = require('aws-sdk')
const lambda = new AWS.Lambda()
const params = {
FunctionName: 'YOUR_SECOND_FUNCTION_NAME',
InvocationType: 'Event', // Ensures asynchronous execution
Payload: JSON.stringify({
... your payload for lambda 2 ...
})
}
await lambda.invoke(params).promise() // Starts Lambda 2
return {
text: "working...",
response_type: "ephemeral",
replace_original: "true"
}
}
Lambda 2
module.exports = async (event, context) => {
// Use event (payload sent from Lambda 1) and do what you need to do
return axios.post('YOUR_INCOMING_WEBHOOK_URL', {
text: 'this will be sent to slack'
});
}
I've been trying to listen to Stripe webhooks with firebase functions.
here is my code:
exports.stripeEvents = functions.https.onRequest((request, response) =>
{
try
{
const stripesignature = request.headers['stripe-signature'] as string;
let stripeevent:Stripe.Event;
try
{
stripeevent = stripe.webhooks.constructEvent(request.rawBody, stripesignature, config.stripewebhooksecretkey);
}
catch (error)
{
sentry.captureException(error);
response.status(400).end();
return;
}
response.sendStatus(200);
}
catch(error)
{
sentry.captureException(error);
throw error;
}
});
and I keep getting this error:
No signatures found matching the expected signature for payload.
Are you passing the raw request body you received from Stripe? https://github.com/stripe/stripe-node#webhook-signing
I have tried changing request.rawBody to request.rawBody.toString() but of no avail. The same firebase function works perfectly when I do a test webhook run from the stripe website.
What could I be missing?
Stripe official documentation on how to do it: https://stripe.com/docs/webhooks/signatures
This may not be an immediate solution, but here are a few suggestions to try:
Check whether changing const stripesignature to a variable produces different behaviour
Log the values of all 3 inputs immediately before that line, then inspect to determine if they are as anticipated (or a substring of each instead, for the more security-minded)
See if you can narrow it down to which input is causing the issue (e.g. request.rawBody, or stripesignature).
On the other hand, although an incorrect webhook secret would be expected to produce a different error, there may be little harm checking it is propagating correctly from config and matching the one in the configured endpoint setting in the dashboard.
Dart function (passing token to sendToDevice):
Future<void> _sendNotification() async {
CloudFunctions functions = CloudFunctions.instance;
HttpsCallable callable = functions.getHttpsCallable(functionName: "sendToDevice");
callable.call({
'token': await FirebaseMessaging().getToken(),
});
}
index.ts file where I have defined sendToDevice method.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
const fcm = admin.messaging();
export const sendToDevice = functions.firestore
.document('users/uid')
.onCreate(async snapshot => {
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'Dummy title',
body: `Dummy body`,
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToDevice(tokens, payload); // how to get tokens here passed from above function?
}
);
Questions:
How can I receive tokens passed from my Dart function _sendNotification to Typescript's sendToDevice function.
When I was directly passing tokens inside index.ts file, I was getting this exception:
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: PlatformException(functionsError, Cloud function failed with exception., {code: UNAUTHENTICATED, details: null, message: UNAUTHENTICATED})
Can anyone please explain if I am supposed to authenticate something here? The command firebase login shows I am already signed in. I am very new to Typescript so please bear with these stupid questions.
Your Flutter side of code seems right, what's wrong is on the Cloud Function.
The sendToDevice function is not a callable function. It is a Cloud Firestore Triggers, it is only meant to be automatically called whenever a document matches users/{uid} is created.
Instead, you'll want to create a Callable Function, see below
export const sendToDevice = functions.https
.onCall(async (data) => {
const { token } = data; // Data is what you'd send from callable.call
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'Dummy title',
body: `Dummy body`,
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToDevice(token, payload);
}
);
You have created a database trigger, what you should do is create a callable function as shown below
exports.sendToDevice = functions.https.onCall(async (data, context) => {
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'Dummy title',
body: `Dummy body`,
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return await fcm.sendToDevice(data.token, payload);
});
There are few things to mention here:
1st The function used in 'getHttpsCallable' must be triggered by https trigger (reference here). Here we have a function triggered by firestore document create, so it won't work.
2nd You do not have parameter of your function, but you call it with parameters. If you need example of calling cloud function with parameter you can find it on pud.dev
3rd I do not have at the moment possibility to play with it, but I think that if you implement https triggered function with token parameter you should be able to pass this parameter.
I hope it will help!
UPDATE:
According to doc https triggered function has to be created with functions.https. There is a nice example in the doc. To function triggered this way you can add request body when you can pass needed data.
This answer might not solve your problem but will give you a few things to try, and you'll learn along the way. Unfortunately I wasn't able to get the callable https working with the emulator. I'll probably submit a github issue about it soon. The flutter app keeps just getting different types of undecipherable errors depending on the local URL I try.
It's good that you've fixed one of the problems: you were using document trigger (onCreate) instead of a https callable. But now, you're running a https callable and the Flutter apps needs to communicate with your functions directly. In the future, you could run the functions emulator locally, and do a lot of console.log'ing to understand if it actually gets triggered.
I have a few questions/ things you can try:
Is your user logged in the flutter app? FirebaseAuth.instance.currentUser() will tell you.
Does this problem happen on both iOS and android?
Add some logs to your typescript function, and redeploy. Read the latest logs through StackDriver or in terminal, firebase functions:log --only sendToDevice. (sendToDevice is your callable function name)
Are you deploying to the cloud and testing with the latest deployment of your functions? You can actually test with a local emulator. On Android, the url is 10.0.2.2:5001 as shown above. You also need to run adb reverse tcp:5001 tcp:5001 in the terminal. If you're on the cloud, then firebase login doesn't matter, I think your functions should already have the credentials.
To call the emulator https callable:
HttpsCallable callable = CloudFunctions.instance
.useFunctionsEmulator(origin: "http://10.0.2.2:5001")
.getHttpsCallable(functionName: "sendToDevice");
And iOS you need to follow the solution here.
One mistake I spotted. You should at least do return await fcm.sendToDevice() where you wait for the promise to resolve, because otherwise the cloud function runtime will terminate your function before it resolves. Alternatively, for debugging, instead of returning sendToDevice in your cloud function, you could have saved it into a variable, and console.log'd it. You would see its actually a promise (or a Future in dart's terminology) that hadn't actually resolved.
const messagingDevicesResponse: admin.messaging.MessagingDevicesResponse = await fcm.sendToDevice(
token,
payload
);
console.log({ messagingDevicesResponse });
return;
Make the function public
The problem is asociated with credentials. You can change the security policy of the CF and sheck if the problem is fixed. Se how to manage permisions on CF here
I have this an URL, let's assume, "www.sample.com/hello". Now I have triggered a lambda function on viewer request where I just need to change the url to "www.sample.com/hello2". I did it using lambda edge functions but it is throwing me an error.
This is the code I wrote in lamda
const path = require('path');
exports.handler = (event, context, callback) => {
const cf = event.Records[0].cf;
const request = cf.request;
const response = cf.response;
const statusCode = response.status;
const path = request.uri;
const afterpath = path.substring(path.indexOf("/")+1);
if (afterpath == 'sample') {
request.uri = request.uri
.replace(afterpath,'samplepathitis')
}
return callback(null, request);
};
I am getting this error
503 ERROR
The request could not be satisfied.
The Lambda function associated with the CloudFront distribution is invalid
or doesn't have the required permissions.
If you received this error while trying to use an app or access a website,
please contact the provider or website owner for assistance.
If you provide content to customers through CloudFront, you can find steps
to troubleshoot and help prevent this error by following steps in the
CloudFront documentation
(http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/http-
503-service-unavailable.html).
Generated by cloudfront (CloudFront)
Request ID: MsN6aG8qvI9ttt3_VLhQAqpY8kF2pHk3V095lAFVU_sWmDvF3IfqAA==
As the error message states it clearly : either the function is invalid or there is no permission to call the function.
To check if the function is valid : try to invoke it from the Lamba console. Use the Test button. You will need to pass a request as input. The console will propose you sample request that you can adjust to simulate your use case.
Also very in the doc the return value of the function. Is request the correct return value expected by Cloudfront ?
Once you are sure about the two above, verify the permission to invoke that function. What is the trigger ? Is Cloudfront authorized to invoke your function ?
Im using Firebase cloud functions. On an iOS device, im deploying a trigger to run a cloud function in Node.Js. In Xcode - here is my client function to trigger the Cloud function - I'm passing over the dictionary data
func updateTermsOfServiceCloudCall(){
let data = [
"accountId": "acct_1Ew#######"
]
Functions.functions().httpsCallable("updateAccountWithTOA").call(data) { (result, error) in
}
Now in Node, i'm running this code to deploy to the Firebase cloud
exports.updateAccountWithTOA = functions.https.onRequest((request, response) => {
const data = request.body;
const accoundId = data.accoundId;
stripe.accounts.update(
accoundId,
{
tos_acceptance: {
date: Math.floor(Date.now() / 1000),
ip: request.ip
}
},
)
});
I'm expecting to get the dictionary data that i passed over from my iOS client. However, im having an issue getting that data in Node. I thought request.body would give me the data, but i guess im wrong because i an getting this error. Any help or suggestions would be appreciated. Thanks
Error: Stripe: Argument "id" must be a string, but got: undefined (on
API request to POST /accounts/{id})
Your client code is trying to invoke a callable function, but your function is defined as an HTTP type function. They are different things. You can't invoke a regular HTTP function using the Functions SDK.
If you want to use the Functions SDK to invoke a function, it needs to be defined with onCall rather than onRequest, as shown in the documentation for callable functions. Or, if you don't want a callable on the backend, you will need to invoke the regular HTTP function with an HTTP client library.