I'm using Meteor for first time and i'm trying to have a simple http call within a method so i can call this method from the client.
The problem is that this async call it's keep running even if i put it within a wrapper.
Client side:
Meteor.call('getToken', function(error, results) {
console.log('entered');
if(error) {
console.log(error);
} else {
console.log(results);
}
});
Server Side
Meteor.methods({
getToken: function(){
// App url
var appUrl = 'myAppUrl';
// Key credentials
var apiKey = 'mykey';
var apiSecret = 'mySecret';
function asyncCall(){
Meteor.http.call(
'POST',
appUrl,
{
data: {
key: apiKey,
secret: apiSecret
}
}, function (err, res) {
if(err){
return err;
} else {
return res;
}
}
);
}
var syncCall = Meteor.wrapAsync(asyncCall);
// now you can return the result to client.
return syncCall;
}
});
I'm always getting an undefined return.
If i log the response within the http.post call i'm geting the correct response.
If i try to log the syncCall i get nothing.
I would very appreciate any help on this.
You should use the synchronous version of HTTP.post in this case. Give something like this a try:
Meteor.methods({
getToken: function() {
var appUrl = 'myAppUrl';
var data = {apiKey: 'mykey', apiSecret: 'mySecret'};
try {
var result = HTTP.post(appUrl, {data: data});
return result;
} catch (err) {
return err;
}
}
});
Instead of returning the err I'd recommend determining what kind of error was thrown and then just throw new Meteor.Error(...) so the client can see the error as its first callback argument.
Related
I have a Cloud Function triggered by a pub/sub event. I use sendgrid nodejs api. The main idea is sending my clients a weekly stats email. sendEmail() function run for each client (80 times). But when I check function logs I see that 25-30 of client emails are sent with success but the remaining it gives that error: "socket hang up"
I shortened the whole code to show the main part related sending email. Here is the last part.
// I shortened the whole function as it is a very long function.
// The main and the last part is as below
// I have nearly 80 clients and sendEmail function run for each client.
function calcData(i, data) {
return admin.database().ref('clientUrlClicks/' + data.key)
.orderByChild('date')
.startAt(dateStartEpox)
.endAt(dateEndEpox)
.once('value', urlClickSnap => {
clients[i].clickTotalWeek = urlClickSnap.numChildren();
clients[i].listTotalWeek = 0;
admin.database().ref('clientImpressions/' + data.key)
.orderByKey()
.startAt(dateStart)
.endAt(dateEnd)
.once('value', snap => {
snap.forEach(function(impressionSnap) {
clients[i].listTotalWeek += impressionSnap.val();
})
}).then(resp => {
return sendEmail(i, clients[i]);
}).catch(err => {
console.log(err);
});
}).catch(err => {
clients[i].clickTotalWeek = 0;
console.log(err);
});
}
function sendEmail(i, data) {
var options = {
method: 'POST',
url: 'https://api.sendgrid.com/v3/mail/send',
headers:
{
'content-type': 'application/json',
authorization: 'Bearer ' + sgApiKey
},
body:
{
personalizations:
[{
to: [{ email: data.email, name: data.name }],
dynamic_template_data:
{
dateStart: xxx,
dateEnd: xxx,
}
}],
from: { email: 'info#xxx.com', name: 'xxx' },
reply_to: { email: 'info#xxx.com', name: 'xxx' },
template_id: 'd-f44eeexxxxxxxxxxxxx'
},
json: true
};
request(options, function (error, response, body) {
if (error) {
console.log("err: " + error);
return;
}
return;
});
}
Edit:
In addition to answers below related to "chaining the promises correctly", I also added all emails and personalizations to "personalizations" array as an object on "sendEmail" function. So, instead making a request for each email I make one request. No problem now.
You are not chaining the promises correctly and therefore not returning a final promise at the end of the chaining, which is mandatory for a Cloud Function.
The following set of modifications is a first attempt to solve this problem.
Also, it is not crystal clear how do you call Sendgrid and return the Promise returned by the Sendgrid call. I would suggest that you use the send() method, which returns a Promise, as explained in the doc of the Sendgrid v3 Web API for Node.js, see https://github.com/sendgrid/sendgrid-nodejs/tree/master/packages/mail.
function calcData(i, data) {
//Declare clients aray here
return admin.database().ref('clientUrlClicks/' + data.key)
.orderByChild('date')
.startAt(dateStartEpox)
.endAt(dateEndEpox)
.once('value')
.then(urlClickSnap => {
clients[i].clickTotalWeek = urlClickSnap.numChildren();
clients[i].listTotalWeek = 0;
return admin.database().ref('clientImpressions/' + data.key) //Here you didn't return the promise
.orderByKey()
.startAt(dateStart)
.endAt(dateEnd)
.once('value');
.then(snap => {
snap.forEach(function(impressionSnap) {
clients[i].listTotalWeek += impressionSnap.val();
})
return sendEmail(i, clients[i]);
}).catch(err => {
clients[i].clickTotalWeek = 0;
console.log(err);
return null;
});
}
I see two issues with your code related to promise chaining, which may be causing this problem.
First is that you are using request with callback in your sendEmail function. This will simply not wait for your network call to finish and returns the function. Now this will build up the calls in parallel and before you hit your 80 clients counts the execution of your cloud function will finish. The solution would be to use request-promise-native (https://github.com/request/request-promise-native) library with your request library. So your sendEmail Function will now become
sendEmail (i, data) {
.
.
.
return rpn(options).then((d)=>{return d}).catch((e)=>{return console.log(e)})
}
Other solution is to use sendgrid client for nodejs which will simply return the promise and you don't need to use request. https://github.com/sendgrid/sendgrid-nodejs/tree/master/packages/mail
Second issue is in you call for data read from firebase where you are also using callbacks instead of promises. Correct solution will be:
function calcData(i, data) {
return admin.database().ref('clientUrlClicks/' + data.key)
.orderByChild('date')
.startAt(dateStartEpox)
.endAt(dateEndEpox)
.once('value').then( urlClickSnap => {
clients[i].clickTotalWeek = urlClickSnap.numChildren();
clients[i].listTotalWeek = 0;
return admin.database().ref('clientImpressions/' + data.key)
.orderByKey()
.startAt(dateStart)
.endAt(dateEnd)
.once('value').then( snap => {
snap.forEach(function(impressionSnap) {
clients[i].listTotalWeek += impressionSnap.val();
})
return sendEmail(i, clients[i]);
})
.catch(err => {
console.log(err);
});
}).catch(err => {
clients[i].clickTotalWeek = 0;
console.log(err);
});
}
This will make sure that you function calcData returns after finishing the execution of all the promises chained.
One more thing if you are calling calcData in a loop, then make sure that you store all the promises in an array and after loop call Promise.all(promisesArray), so that you function waits for all the executions to finish.
I've been trying to get my refresh token to work for a while now, and I hope I'm close. My token refreshes and triggers a subsequent 200 call to whatever call caused the 401, but my the data on my page doesn't refresh.
When an access token expires, the following happens:
After the 401, the GetListofCompanyNames returns 200 with a list of names using the correct updated access token. However, my dropdown does not refresh.
My interceptor:
app.factory('authInterceptorService',['$q', '$location', 'localStorageService', '$injector', function($q, $location, localStorageService, $injector) {
return {
request: function(config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function(rejection) {
//var promise = $q.reject(rejection);
var authService = $injector.get('authService');
if (rejection.status === 401) {
// refresh the token
authService.refreshToken().then(function() {
// retry the request
var $http = $injector.get('$http');
return $http(rejection.config);
});
}
if (rejection.status === 400) {
authService.logOut();
$location.path('/login');
}
return $q.reject(rejection);
}
};
}
]);
My return statement on the 401 rejection looks suspect here, but I'm not sure what to replace it with. Thereby my question is: How can I get my page to refresh it's data when I make the new call?
Update:
This gets me past when the 200 returns and I can get a dropdown to refresh, but I lose any state on the page (ex. selected dropdown) with the below.
authService.refreshToken().then(function() {
var $state = $injector.get('$state');
$state.reload();
});
Back to the drawing board!
Try putting up your retry call in $timeout, it should work.
Here's the updated code:
app.factory('authInterceptorService',['$q', '$location', 'localStorageService', '$injector', function($q, $location, localStorageService, $injector) {
return {
request: function(config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function(rejection) {
//var promise = $q.reject(rejection);
var authService = $injector.get('authService');
if (rejection.status === 401) {
// refresh the token
authService.refreshToken().then(function() {
// retry the request
return $timeout(function() {
var $http = $injector.get('$http');
return $http(rejection.config);
}});
}
if (rejection.status === 400) {
authService.logOut();
$location.path('/login');
}
return $q.reject(rejection);
}
};
}
]);
$timeout returns a promise that is completed with what is returned
from the function parameter, so we can conveniently just return the
$http call wrapped in $timeout.
Thanks.
I think you may want to change up how you go about this. One way to go about this would be to inject the $rootScope into your authInterceptorService and then once you successfully refresh the token, call something like $rootScope.broadcast('tokenRefreshed').
I don't quite know how you have set up the view and controller that handles your dropdown, but I would set up a listener for that 'tokenRefreshed' event. From here, you can do another call to GetListofCompanyNames. If you do it this way you can easily control and ensure that the model gets updated.
My final solution:
app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', '$injector', function($q, $location, localStorageService, $injector) {
var $http;
var retryHttpRequest = function(config, deferred) {
$http = $http || $injector.get('$http');
$http(config).then(function(response) {
deferred.resolve(response);
},
function(response) {
deferred.reject(response);
});
}
return {
request: function(config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function(rejection) {
var deferred = $q.defer();
if (rejection.status === 401) {
var authService = $injector.get('authService');
authService.refreshToken().then(function() {
retryHttpRequest(rejection.config, deferred);
},
function () {
authService.logOut();
$location.path('/login');
deferred.reject(rejection);
});
} else {
deferred.reject(rejection);
}
return deferred.promise;
}
};
}
]);
Copied almost 1 for 1 from https://github.com/tjoudeh/AngularJSAuthentication/blob/master/AngularJSAuthentication.Web/app/services/authInterceptorService.js .
This one transparently handles all requests and refreshes them when necessary. It logs out users when the refresh token is expired and passes errors along to the controllers by properly rejecting them. However, it doesn't seem to work with multiple in flight requests, I'll look into that when I get a use case for it in my system.
I want to return a value from server side to client side, If I get error response from the API Call.
if Meteor.isClient
Meteor.call 'foo',data,(err,results)->
console.log(results)
if Meteor.isServer
Meteor.methods foo:(data)->
results = Meteor HTTP.post "example.com",{
data: data
},(err,results)
if err
console.log('abc')
return 0
If I get 400 error from example.com, then It is printing abc but it is not returning anything to the client.
Thanks in advance,
As #user3374348 stated, you should use HTTP.post synchronously if you want your method to return its results. Otherwise, HTTP.post's callback will return the result (to nowhere), not your foo method. Here's how you would do it, in regular JavaScript:
if (Meteor.isServer) {
Meteor.methods({
foo: function (data) {
try {
var result= HTTP.post("example.com", {
data: data
});
return result;
}
catch (error) {
// todo: check if the error is 400
console.log("abc");
return 0;
}
}
});
}
I need get data from external service. It has API. This is example:
http://portal.example.com/portal.api?l=username&p=keyphrase&act=brand_by_nr&nr=kl2&alt
Parameters are:
"l" - login, "p" - password, "act" - function to execute, "nr" - part number
I try connect by Meteor http.This is my server code:
var sources = {
mskv: {
url: "http://portal.example.com/portal.api",
auth: { l: "mylogin", p: "cBKoTyalCgbOQb37NG6sbb0qv2I0Q4PmWRJIJMWpOhCPFombqeDv7fBhdkjsdhkjah" },
params: { act: "brand_by_nr", nr: null }
}
};
Meteor.methods({
doRequest: function(partNumber) {
for (var key in sources) {
var url = sources[key].url;
var authData = sources[key].auth;
var paramsData = sources[key].params;
paramsData.nr = partNumber;
HTTP.call("POST", url, { auth: authData, params: paramsData }, function(err, res) {
if (err) {
throw new Meteor.Error("not-response", "Remote server not responding");
}
return res;
});
}
}
});
This is my client code:
Template.search.events({
"click .search": function(event) {
var partNumber = document.getElementsByClassName("input")[0].value;
Meteor.call("doRequest", partNumber, function(err, res) {
if(err === "not-response") return;
console.log(res);
});
}
});
I have error:
> Exception while invoking method 'doRequest' TypeError: Object
> #<Object> has no method 'indexOf' I20150227-00:01:35.455(3)? at Object._call (packages/http/httpcall_server.js:42:1)
> I20150227-00:01:35.455(3)? at Object._.extend.wrapAsync [as call]
> (packages/meteor/helpers.js:118:1) I20150227-00:01:35.455(3)? at
> [object Object].Meteor.methods.doRequest (app/server/server.js:19:18)
Can you help me, where is my error?
Try
var paramsData = [sources[key].params];
I suspect it's looking for an array there.
In my case auth field is not correct. The true way is auth:"login: password", look as simply string. Second error - auth field is not need. For this service login and password send as parameters { params: {l:"login", p: "password", act: "brand_by_nr" ....} }
could anybody please tell me how to make clients wait until the called function on the server is executed?
My code:
Meteor.methods({
markLettersAsRead: function(userId) {
if(serverVar) {
Users.update({_id: userId}, {$set: {letters: []}}); // removing all references
}
}
});
Template.letter.events({
'click a': function() {
Meteor.call('markLettersAsRead', Meteor.userId(), this._id, function(err) {
if (err) {
console.log(err);
}
});
var usersExistsWithThisLetter = Users.find({letters: {_id: this._id}}).count();
console.log(usersExistsWithThisLetter);
}
});
In my example usersExistsWithThisLetter is always 1 because the Users.find() doesn't wait until the Meteor.call is done. I verified this by checking the database and no users exists with entries in the letters array.
Any help would be greatly appreciated.
You need to query the collection inside the callback, because then you can be certain that your server method has already been executed. I would do something like this (note the self variable declaration):
var self = this;
Meteor.call('markLettersAsRead', Meteor.userId(), this._id, function(err) {
if (!err) {
var usersExistsWithThisLetter = Users.find({letters: {_id: self._id}}).count();
console.log(usersExistsWithThisLetter);
} else {
console.log(err);
}
});
I hope it helps!