I'm sending push messages using FCM through Firebase Functions. The messages are being sent properly, but I'm getting the 408 time-out error after the message is sent. I'm suspecting it might have to do with the unregistered tokens not being cleaned up because:
if I were to send another message to the same device, the same timeout occurs and
the only error message I get from the Firebase log is Function execution took 60002 ms, finished with status: 'timeout'.
exports.sendMessage = functions.https.onRequest(async (request, response) => {
const {
sender,
recipient,
content,
docID
} = request.body
functions.logger.log(
"docID:",
docID,
);
// Get the list of device notification tokens.
let deviceTokens; let ref;
try {
ref = admin.firestore().collection("deviceToken").doc(recipient);
const doc = await ref.get();
if (!doc.exists) {
console.log("No such document!");
response.status(500).send(e)
} else {
console.log("doc.data():", doc.data());
deviceTokens = doc.data().token;
}
} catch (e) {
response.status(500).send(e)
}
let senderProfile;
try {
senderProfile = await admin.auth().getUser(sender);
console.log("senderProfile", senderProfile);
} catch (e) {
console.log(e);
response.status(500).send(e)
}
// Notification details.
let payload = {
notification: {
title: senderProfile.displayName,
body: content,
sound: "default",
},
data: {
uid: senderProfile.uid,
displayName: senderProfile.displayName,
docID,
messageType: "status"
}
};
functions.logger.log(
"deviceTokens", deviceTokens,
"payload", payload,
);
// Send notifications to all tokens.
const messageResponse = await admin.messaging().sendToDevice(deviceTokens, payload);
// For each message check if there was an error.
messageResponse.results.forEach((result, index) => {
const error = result.error;
if (error) {
functions.logger.error(
"Failure sending notification to",
deviceTokens[index],
error,
);
// Cleanup the tokens who are not registered anymore.
if (error.code === "messaging/invalid-registration-token" ||
error.code === "messaging/registration-token-not-registered") {
const updatedTokens = deviceTokens.filter((token) => token !== deviceTokens[index]);
console.log("updatedTokens", updatedTokens);
ref.update({
token: updatedTokens,
})
.catch(function(e) {
console.error("Error removing tokens", e);
response.status(500).send(e)
});
}
}
});
response.status(200)
});
I'm unsure why the following isn't cleaning up the unregistered tokens:
const updatedTokens = deviceTokens.filter((token) => token !== deviceTokens[index]);
ref.update({
token: updatedTokens,
})
You always have to end HTTP functions with response.status(200).send() or response.status(200).end(). In the above function, you have response.status(200) so you have to end it either with response.status(200).send() or response.status(200).end(). Please check the documentation if it helps.
Related
I need your help to mock a twilio service which sends a message, using jest to mock the service
I have the next code:
import { SQSEvent } from "aws-lambda";
import { GetSecretValueResponse } from "aws-sdk/clients/secretsmanager";
export async function sendSms(event: SQSEvent, data: GetSecretValueResponse) {
const secrets = JSON.parse(data.SecretString);
const accountSid = secrets.TWILIO_ACCOUNT_SID;
const authToken = secrets.TWILIO_AUTH_TOKEN;
const twilioNumber = secrets.TWILIO_PHONE_NUMBER;
if (accountSid && authToken && twilioNumber) {
//Create a Twilio Client
const client = new Twilio(accountSid, authToken);
//Loop into al records of the event, every record is every message sent from Sqs
for (const record of event.Records) {
const body = JSON.parse(record.body);
const userNumber = "+" + body.number;
//SendMessage function
try {
const message = client.messages.create({
from: twilioNumber,
to: userNumber,
body: body.message,
});
return message;
} catch (error) {
return `Failed to send sms message. Error Code: ${error.errorCode} / Error Message: ${error.errorMessage}`;
}
}
} else {
return "You are missing one of the variables you need to send a message";
}
}
The I call this function from my index:
import { SQSEvent } from "aws-lambda";
import { sendSms } from "./services/sendSms/sendSms";
import { getSecret } from "./services/obtainSecrets/getSecret";
import { SecretsManager } from "aws-sdk";
export const lambdaHandler = async (event: SQSEvent) => {
try {
const obtainedSecret = await getSecret()
.then((credentials: SecretsManager.GetSecretValueResponse) => {
return credentials;
})
.catch(error => {
return error;
});
const response = sendSms(event, obtainedSecret)
.then(response => {
return response;
})
.catch(error => {
return error;
});
return {
message: "OK " + obtainedSecret + response,
code: 200,
};
} catch (error) {
throw new Error(error);
}
};
I have already make some tests, but them always makes a connection with Twilio api(requiring the real token, sid,etc), and I need to mock the Twilio service, so the function I call in my test.ts doesn't connects to internet.
import { Twilio } from "twilio";
import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import { sendSms } from "../../services/sendSms/sendSms";
//mock Twilio library and sendSms service
jest.mock("twilio");
jest.mock("../../services/sendSms/sendSms");
const smsMessageResultMock: Partial<MessageInstance> = {
status: "sent",
sid: "AC-lorem-ipsum",
errorCode: undefined,
errorMessage: undefined,
};
describe("SMS Service", () => {
describe("Send Message", () => {
it("Should fail", async () => {
// update smsMessageResultMock to simulate a faled response
const smsMessageMock = {
...smsMessageResultMock,
status: "failed",
errorCode: 123,
errorMessage: "lorem-ipsum",
};
// simulated response of secret management
let data = {
ARN: "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3",
Name: "MyTestDatabaseSecret",
SecretString:
'{"TWILIO_ACCOUNT_SID": "ACTWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN": "TWILIO_AUTH_TOKEN","TWILIO_PHONE_NUMBER": "TWILIO_PHONE_NUMBER"}',
VersionId: "EXAMPLE1-90ab-cdef-fedc-ba987SECRET1",
VersionStages: ["AWSPREVIOUS"],
};
// simulated response of SqsEvent
let event = {
Records: [
{
messageId: "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
receiptHandle: "MessageReceiptHandle",
body: '{"message": "Hello world","number": "(506)88888888"}',
attributes: {
ApproximateReceiveCount: "1",
SentTimestamp: "1523232000000",
SenderId: "123456789012",
ApproximateFirstReceiveTimestamp: "1523232000001",
},
messageAttributes: {},
md5OfBody: "{{{md5_of_body}}}",
eventSource: "aws:sqs",
eventSourceARN: "arn:aws:sqs:us-east-1:123456789012:MyQueue",
awsRegion: "us-east-1",
},
],
};
// simulate tokens for Twilio
const accountSid = "ACfjhdskjfhdsiuy876hfijhfiudsh";
const authToken = "fjfuewfiuewfbodfiudfgifasdsad";
//create client with mocked Twilio
const client = new Twilio(accountSid, authToken);
//call messages.create of Twilio client, and give it the expected result created
client.messages.create = jest
.fn()
.mockResolvedValue({ ...smsMessageMock });
console.log(await sendSms(event, data));
//expectes the function sendSms(event, data) to throw an error
await expect(sendSms(event, data)).rejects.toThrowError(
`Failed to send sms message. Error Code: ${smsMessageMock.errorCode} / Error Message: ${smsMessageMock.errorMessage}`
);
});
});
});
(event and data are simulated responses of SqsEvent and GetSecretValueResponse)
The problem is that when I run the npm test it throws me an error of Twilio's authentication, an it is because I'm passing self created tokens.
Expected substring: "Failed to send sms message. Error Code: 123 / Error Message: lorem-ipsum"
Received message: "Authentication Error - invalid username"
at success (node_modules/twilio/lib/base/Version.js:135:15)
at Promise_then_fulfilled (node_modules/q/q.js:766:44)
at Promise_done_fulfilled (node_modules/q/q.js:835:31)
at Fulfilled_dispatch [as dispatch] (node_modules/q/q.js:1229:9)
at Pending_become_eachMessage_task (node_modules/q/q.js:1369:30)
at RawTask.Object.<anonymous>.RawTask.call (node_modules/asap/asap.js:40:19)
at flush (node_modules/asap/raw.js:50:29)
So what I suppose is that the test is connecting to internet and calling Twilio's api.
I appreciate if you could help me.
I think what you want to do is mock the class returned by the module, using jest.mock('twilio', mockImplementation) and in mockImplementation return a function to act as a constructor that will take your account SID and auth token arguments and then return a mockClient implementation, which in this case needs to return an object which has a messages property, which in turn is an object with a create property that is a mock function.
It's probably easier to just show the code.
const mockClient = {
messages: {
create: jest.fn().mockResolvedValue({ ...smsMessageMock });
}
};
jest.mock("twilio", () => {
return function(accountSid, authToken) {
return mockClient;
}
});
I am trying to run the below cod which initialises the Firebase Admin SDK, and send a notification message.
const admin = require('firebase-admin/app');
const errorCodes = require('source/error-codes');
const PropertiesReader = require('properties-reader');
const prop = PropertiesReader('properties.properties');
exports.sendSingleNotification = async (event, context) => {
const params = event.queryStringParameters;
var serviceAccount = require("xxx-xxx-firebase-adminsdk-xxx-xxx.json");
try {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
console.log("INITIALIZED");
// This registration token comes from the client FCM SDKs.
const registrationToken = params.fcmtoken;
console.log()
const message = {
notification: {
title: 'FooCorp up 1.43% on the day',
body: 'FooCorp gained 11.80 points to close at 835.67, up 1.43% on the day.'
},
token: registrationToken
};
// Send a message to the device corresponding to the provided
// registration token.
admin.getMessaging().send(message)
.then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
return {"response":response}
})
.catch((error) => {
console.log('Error sending message:', error);
return {"error 1":error}
});
} catch (error) {
console.log(error);
return {"error 2":error}
}
};
Here the serviceAccount means the path of the Firebase private key file which is in the root of this project.
However when I run this code I always end up with the following error.
START RequestId: e66ffdd9-ab9c-4a68-ade2-7cfa97f42c31 Version: $LATEST
at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)] (/var/task/source/fcm/send-single-notification.js:14:42)rt' of undefined
END RequestId: e66ffdd9-ab9c-4a68-ade2-7cfa97f42c31
Something is undefined and I can't figure out what it is or what the error is.
How can I fix this?
having a real problem with getting this code to work. I have everything set up working great with Appwrite. I'm getting a response back from the server, but in my promise.then it finishes the other code and returns undefined from the login function. so then the post async function is sending a blank array in the try block. I've tried setting this code up every way I can think of but it never works. Sorry, i'm still trying to wrap my head around the promises and async js.
import { Appwrite } from 'appwrite';
export async function post({ locals, request }) {
const { email, password } = await request.json();
function login() {
// add logic to authenticate user with external service here
const sdk = new Appwrite();
sdk
.setEndpoint('https://') // API Endpoint
.setProject('') // project ID
;
let promise = sdk.account.createSession(email, password);
let userLogin;
promise.then(function (response) {
console.log(response); // Success
userLogin = response.providerUid;
console.log(userLogin);
}, function (error) {
console.log(error); // Failure
});
console.log('login.json.js', { email, password: !!password });
console.log(userLogin);
return userLogin;
}
try {
const user = login();
locals.user = user;
return {
status: 200
};
} catch (error) {
const message = `Error in endpoint /api/login.json: ${error}`;
return {
status: 500,
body: message
};
}
}
You're returning userLogin in login before it's even populated in the asynchronous promise.then chain
Also, since you're currently handling the rejection in your promise.then(onFulfilled, onRejected) that would mean any rejection is handled inside login and your try/catch (once written correctly) would never have an error to catch since login handled it already
One more potential issue - if const { email, password } = await request.json(); rejects, then the error will be thrown to whatever called post - is that what you want? or did that also need to be handled inside post?
Anyway here's how to fix your code:
import { Appwrite } from 'appwrite';
export async function post({ locals, request }) {
// note: if this throws then the error will be handled by whatever calls `post`
const { email, password } = await request.json();
function login() {
// add logic to authenticate user with external service here
const sdk = new Appwrite();
sdk
.setEndpoint('https://') // API Endpoint
.setProject('') // project ID
;
const promise = sdk.account.createSession(email, password);
return promise.then(function(response) {
let userLogin = response.providerUid;
return userLogin;
// or without redundant `userLogin` variable
// return response.providerUid;
});
}
try {
const user = await login();
locals.user = user;
return { status: 200 };
} catch (error) {
const message = `Error in endpoint /api/login.json: ${error}`;
return { status: 500, body: message };
}
}
Or, making login async
import { Appwrite } from 'appwrite';
export async function post({ locals, request }) {
// note: if this throws then the error will be handled by whatever calls `post`
const { email, password } = await request.json();
async function login() {
// add logic to authenticate user with external service here
const sdk = new Appwrite();
sdk
.setEndpoint('https://') // API Endpoint
.setProject('') // project ID
;
let response = await sdk.account.createSession(email, password);
let userLogin = response.providerUid;
return userLogin;
}
try {
const user = await login();
locals.user = user;
return {
status: 200
};
} catch (error) {
const message = `Error in endpoint /api/login.json: ${error}`;
return {
status: 500,
body: message
};
}
}
Or, removing inner Login function completely
import { Appwrite } from 'appwrite';
export async function post({ locals, request }) {
// note: if this throws then the error will be handled by whatever calls `post`
const { email, password } = await request.json();
try {
const sdk = new Appwrite();
sdk.setEndpoint('https://') // API Endpoint
.setProject(''); // project ID
const response = await sdk.account.createSession(email, password);
console.log(response); // Success
locals.user = response.providerUid;
return { status: 200 };
} catch (error) {
const message = `Error in endpoint /api/login.json: ${error}`;
return { status: 500, body: message };
}
}
I successfully registered firebase service worker with
let messaging = null;
export const registerFirebaseServiceWorker = () => {
if ("serviceWorker" in navigator) {
console.log("serviceWorker in navigator");
navigator.serviceWorker.register(("./firebase-messaging-sw.js"))
.then((registration) => {
console.log("Registration successful, scope is: ", registration.scope);
})
}
}
and try to receive the message with messaging.onMessage callback
if (supportsWebPush) {
messaging.onMessage(payload => {
console.log("There's a message!", payload);
const notificationTitle = "notificationTitle"
const notificationOptions = {
body: payload
};
//self.registration.showNotification(notificationTitle, notificationOptions);
navigator.serviceWorker.getRegistration("./firebase-messaging-sw.js").then(registration => {
console.log("got the registration");
registration.showNotification('Hello world!');
}).catch(error => {
console.log("No service worker registered", error);
});
});
Even though console.log was working and registration.showNotification was executed, there were no push notification on my browser ? Why is this? I've been trying multiple different ways and search for many suspected issues but can't found a proper way to display default web push pop up
I'm scratching my head trying to figure out the best way to handle errors from specific user actions. I'm using Express as my web server and even though it works, for the most part, I am getting not-so-useful, generic error messages. For instance, in the code below, I get the Request failed with status code 400 error message on the client side for the first two conditions/exceptions in the try block.
How do I approach this in the following example?
Express Server-side Controller
async function voteInPoll (req, res) {
const { category, pollId } = req.params;
const { name, choiceId, voterId } = req.body;
try {
const poll = await Poll.findById(pollId);
// Check if user has already voted in poll
const hasVoted = poll.votedBy.some(voter => voter.equals(voterId));
if (!voterId) { // Check if user is authenticated
res
.sendStatus(400)
.json({ message: 'Sorry, you must be logged in to vote' });
} else if (voterId && hasVoted) {
res
.sendStatus(400)
.json({ message: 'Sorry, you can only vote once' });
} else {
const choice = await poll.choices.id(choiceId);
const votedChoice = { name, votes: choice.votes + 1 };
await choice.set(votedChoice);
await poll.votedBy.push(voterId);
poll.save();
res
.sendStatus(200)
.json({
message: 'Thank you for voting. Find other polls at: ',
poll,
});
}
} catch (error) {
throw new Error(error);
}
}
React/Redux Action
export const voteInPoll = (category, pollId, votedItem, voterId) => async dispatch => {
try {
const response = await axios.post(
`http://localhost:3050/polls/${category}/${pollId}/vote`,
{
...votedItem,
voterId,
}
);
dispatch({ type: store.polls.VOTE_SUCCESS, payload: response.data.poll });
} catch (error) {
console.log(error);
dispatch({ type: store.polls.VOTE_FAILURE, payload: error.message });
}
};
Edit
What I find rather bizarre is I get the expected error response sent, as seen below under the Network tab of Chrome's Developer tools.
You should not be using res.sendStatus(statusCode) because of the following as defined in the docs here:
Sets the response HTTP status code to statusCode and send its string representation as the response body.
The key thing about the above is:
and send its string representation as the response body.
So doing: res.sendStatus(400).json({ message: 'Oops 400!'}) will not give you a JSON response which is what you're expecting, but simply display:
Bad Request
Which is the string representation of the 400 HTTP status code: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_errors
What you need to do is replace all of your res.sendStatus(..).json(..) with res.status(...).json(...) like so:
if (!voterId) { // Check if user is authenticated
res
.status(400)
.json({ message: 'Sorry, you must be logged in to vote' });
} else if (voterId && hasVoted) {
res
.status(400)
.json({ message: 'Sorry, you can only vote once' });
} else {
// ...
}
and so on.