I have a unittest where a service returns a promise. Sometimes that service might make a $http request, sometimes it won't (It implements a cache of sorts). In either case, it resolves the promise, but only in $httpBackend.flush() does it actually get around to making the callbacks. How can I cause the promises that are resolved to actually call the functions in the .then() like flush() does.
This one works just fine
resolved = jasmine.createSpy();
rejected = jasmine.createSpy();
employeeEventService.loadSchedules()
.then(resolved, rejected);
$httpBackend.flush(); // This causes the promise to resolve/reject
expect(resolved).toHaveBeenCalled();
expect(rejected).not.toHaveBeenCalled();
This one doesn't work because I can't call flush() (since the service never called $http)
resolved = jasmine.createSpy();
rejected = jasmine.createSpy();
employeeEventService.loadSchedules()
.then(resolved, rejected);
//$httpBackend.flush(); // Can't call this because this call is "cached"
expect(resolved).toHaveBeenCalled();
expect(rejected).not.toHaveBeenCalled();
Service code:
if(loaded.startOn <= params.startOn && loaded.endOn >= params.endOn
&& new Date() - lastFetch < 60000) {
deferred.resolve(loaded.schedules);
} else {
service.loading +=1;
config = {params: params, timeout:30*1000};
$http.get('/api/employee-schedules/', config)
.then(function(response) {
...
process the json response
...
deferred.resolve(loaded.schedules);
}, function(reason, status) {
$log.error("Failed to get schedules", reason, status);
deferred.reject(reason, status);
})
.finally(function() {
service.loading -=1;
});
}
return deferred.promise;
All promises, regardless of what pattern they are in, are resolved when you call $scope.$digest();
You're using a promise antipattern in your service, which is why you're encountering these issues. Instead, try this pattern:
function loadSchedules () {
return (cachedSchedules) ? $q.when(cachedSchedules) : asyncHTTPStuffs();
}
(Thanks to Benjamin Gruenbaum)
This way, you can return a thenable object, regardless of cache status, while not needing to call $httpBackend.flush() if you know the data is cached.
Related
In my website, I have API's for twitter and facebook which enables the "mentions" feature (the one that pops up whenever we use the # symbol)
However, it is often that the access token for some feature is often expires resulting in the API not working. I store all my API's in an array and then I need to check if the token has failed or not resulting in resolved or rejected API promise.
This is an older code that needs changing due $q.all. As $q.all works whenever all the promises have been resolved, thus triggering the .then() call, this results in the .then() function NEVER working in my case (as the Facebook API is never working)
I need to find a condition where each API is checked and the .then() runs for only that API that is resolved (twitter in this case) and ignores the failed API (Facebook in this case)
if (selectedIds.allowed.TW) {
usersApi.push(TS.loginResource.getTwitterProfiles({
subUserId: selectedIds.allowed.TW,
name: searchTerm
}).$promise);
}
if (selectedIds.allowed.FB || selectedIds.allowed.FB_PAGE ||
selectedIds.allowed.FB_GROUP) {
$scope.post.showTags = true;
usersApi.push(TS.loginResource.getFbPages({
subUserId: selectedIds.allowed.FB_PAGE || selectedIds.allowed.FB
|| selectedIds.allowed.FB_GROUP,
name: searchTerm
}).$promise);
}
if (usersApi.length) {
$q.all(usersApi).then(function (responses) {
var tags1 = responses[0];
tags1.forEach(function (tag, i) {
tags1[i].name = tag.name.replace(/\"/g, "");
});
$scope.post.tags = tags1;
if (usersApi.length > 1) {
var tags2 = responses[1]
tags2.forEach(function (tag, i) {
tags2[i].name = tag.name.replace(/\"/g, "");
});
$scope.post.tags = $scope.post.tags.concat(tags2);
}
})
}
}, 500);
} else {
$scope.post.tags = [];
$scope.post.showTags = false;
}
I think you are looking to chain backup response that catch the api error and return a new success resolved promise on each specific api call before you wait on "all" of them.
apiCalls.push(doTwiterStuff().then(handleTwiterSuccess, handleApiFailure);
apiClass.push(doFBStuff().then(handleFbSuccess, handleApiFailure);
Promise.all(apiCalls).then(arr => {
arr.filter(x => !isNil(x)).forEach(x => doSomethingWithApiResult(x));
});
function handleApiFailure(x) {
...
return Promise.resolve(null);
}
Hopes this helps.
$q.all is not resilient1
If one of the promises is rejected, the $q.all is rejected with the first error.
To create a resilient composite promise, that is a promise that waits for all the promises to complete pass or fail, use .catch on each individual promise to convert the rejected promise to a successful promise.
var resilientPromises = [];
angular.forEach(promises, function(p) {
var resilientP = p.catch( function(result) {
//return to convert rejection to success
return result;
});
resilientPromises.push(resilientP);
});
$q.all(resilientPromises).then( function (results) {
//process results
});
The two things to take away from this answer:
A $q.all promise is not resilient. It is rejected with the first rejected promise.
A fulfilled promise can be created from a rejected promise by returning a value to the onRejected function of either the .then method or the .catch method.
In all my services, I'm just invoking REST services and returning the promises to the controllers. All the error's are handled at controllers using catch like below,
MyService.getData(url).then(getDataSuccess).catch(exception.catcher('Contact Admin : '));
My question here is, Since the real $http calls will be made at service, should I have to write catchers in service or catching in controller is fine?,
Scenario 1:
function getData(url){
return $http.get(url);
}
Scenario 2: (Nested calls to make combined results)
function getOtherData(url){
var defer = $q.defer();
$http.get(url).then(
function(response){
$http.get(nextService).then(
function(res){
defer.resolve('combined data');
}
)
}
);
return defer.promise;
}
Both the service method is not handling any errors. Instead it just returns the promise. Will there be any situation where this kind of exception handling will get failed?
Note: I have created decorators for handling javascript,angular errors and route errors separately. This question is particularly about $http service errors.
Yes what you have can fail triggering your catch because you have no reject().
You are using an anti-pattern creating your own promise and not chaining the nested request properly. Neither of those request rejections will be returned anywhere.
To be able to chain these get rid of the $q.defer() and do:
function getOtherData(url) {
// return beginning of promise chain
return $http.get(url).then(function (response) {
// return next promise
return $http.get(nextService).then(function (res) {
// combine and return the data
return {
d1 : response.data,
d2 : res.data
};
});
});
}
Now walk through the scenarios and each part of chain is intact.
Think of the chain as each then needs a return until the end of the chain
Scenario 2: (Nested calls to make combined results)
Failed Scenario
function getOtherData(url){
var defer = $q.defer();
$http.get(url).then(
function(response){
$http.get(nextService).then(
function(res){
defer.resolve('combined data');
}
)
}
);
return defer.promise;
}
This scenario will fail if the first $http.get has an error. The promise will hang and never get resolved. This is why we recommend avoiding using $q.defer to create promises from services that already return promises.
Instead return data and chain promises.
function getOtherData(url) {
var promise = $http.get(url);
var derivedPromise =
promise.then ( function (response) {
var data = response.data;
var nextPromise = $http.get(nextService);
var derivedNext = nextPromise.then(function(response) {
//return for chaining
return response.data;
});
//return composite for chaining
return $q.all([data, derivedNext]);
});
return derivedPromise;
};
The getOtherData(url) promise will be fulfilled with an array with the data from the two XHRs or it will be rejected with the first error response.
It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.1
Chaining error handlers
In an error handler, to convert a rejected resolution to a fulfilled resolution return data. To chain a rejection, throw the error.
For example:
promise = http.get(someUrl);
derivedPromise = promise.catch(function(errorResponse) {
if (fixable) {
fixedPromise = $http.get(fixedUrl);
//return to convert
return fixedPromise;
} else {
//throw to chain rejection
throw errorResponse;
}
};
By chaining error handlers, errors can be handled both by the service and the client of the service.
This makes it possible to implement powerful APIs like $http's response interceptors.1
Building on #georgeawg's answer, if you want to return multiple sets of data then you don't need nested calls.
function getOtherData(url) {
var promise1 = $http.get(url).then ( function (response) {
return response.data;
});
var promise2 = $http.get(nextService).then(function(response) {
return response.data;
});
return $q.all([promise1, promise2]);
};
Now the caller gets a promise that resolves to a list of the 2 data items (or is rejected if either request fails). The only real difference is that both requests are issues in parallel.
This generalises easily to a situation where you could have a list of urls, fetch them all in parallel and get an array of the response.data items.
Because you get back only a single promise that resolves to an array of data you can handle the result in the controller, but you only need one error handler.
MyService.getOtherData(url)
.then(getDataSuccess)
.catch(exception.catcher('Contact Admin : '));
Although the original question doesn't specify, it might be the case that the second url depends on the result from the first. You can handle that case here as well if you remember that you can call .then() multiple times on the same promise:
function getOtherData(url) {
var promise1 = $http.get(url).then ( function (response) {
return response.data;
});
var promise2 = promise1.then(function(response) {
// compute nextService from response.data here...
var nextService = foo(response.data);
return $http.get(nextService).then(function(response) {
return response.data;
});
});
return $q.all([promise1, promise2]);
};
I have a globalDataService in my app that reads a couple of entities from the server.
I only want to read the data once, and then serve it up via a method on the service. Here's a simplified version
angular.module("myApp").factory("globalData", ["siteResource", globalData]);
function globalData( siteResource) {
var sites = [];
siteResource.query().$promise.then(function(data){
sites = data;
},
function(response) {
//handle bad stuff
});
var getSites = function () { return sites; }
return { getSites: getSites };
}
and in my controller I just want to be able to do this
this.sites = globalData.getSites();
and know that the data is there, and if it isn't then something is wrong. What do I need to do in my service to make this happen, I've just wasted 2 hours trying to do something with $q but with no joy.
It's pot luck whether the globalData service has loaded the data or not when I need it, particularly when the app first loads....
Save the promise and return the promise. Create the promise once and reuse it.
angular.module("myApp").factory("globalData", ["siteResource", globalData]);
function globalData(siteResource) {
var promise;
function getSitesPromise () {
if (!promise) {
promise = siteResource.query().$promise;
promise = promise.catch( function (error) {
//handle bad stuff
});
};
return promise;
};
return { getSitesPromise: getSitesPromise };
}
What your are doing is pretty simple.
Prob making $http requests to your server to get data. But you don't want to make them every time just on init.
you can use $q like this ...
var promise1 = $http.get('/mypath1');
var promise2 = $http.get('/mypath2');
var promises = $q.all([promise1, promise2]); // resolves when all promises are resolved OR will reject when ONE promise rejects
promises.then(function(arrayContainingAllServerResponses) {
// do something
}).catch(function(error) {
// oops one of the requests failed
})
sorry but i dont have time for more detail - this might get you on the right track - cheers
this works because $http returns a promise :)
Remember if your have a promise then you can call THEN on it - the THEN code will be executed when the promise is resolved (not immediately). You can even chain THENs by returning a promise from a THEN. Hope this all helps.
If a method returns a promise then you can call THEN on its return value. REMEMBER $http returns a promise which is resolved or rejected when your server responds or the request times out!
I'm trying to use HTTP-Interceptor to handle when a $http request times out and to ensure that the requesting function actually gets the response it is after. Retry request with $http interceptor works for me BUT, it has an immediate retry. What I'm really more interested in doing is to wait 5 seconds, and then retry. Furthermore, I also want the user to be able to trigger an immediate retry.
What I'm attempting to do is something like the below:
joinApp.factory('httpResponseErrorInterceptor',function($q, $injector, $interval) {
return {
'responseError': function(response) {
if (response.status === 0) {
setTimeToRetry(5); //sets retry timeout to 5 seconds, and starts an interval to reduce the count every second
// should retry
$interval(function(response) {
if(getTimeToRetry() <= 0) { //when retry timeout gets to 0, then retry - this also allows for a method that sets the retry timeout to 0 earlier so that a user can make an immediate retry
var $http = $injector.get('$http');
return $http(response.config);
}
},1000)
}
// give up
return $q.reject(response);
}
};
});
joinApp.config(function($httpProvider) {
$httpProvider.interceptors.push('httpResponseErrorInterceptor');
});
(As you can see, I've very liberally copied the base code from that linked answer)
But what I'm finding, is that then when the $http request is retried, then it completes the HTTP request successfully, but the code that was looking for the original promise response does not actually get what its after.
I'm GUESSING that this is because the existence of the $interval actually moves the response outside of the thread that the request was made in, so the response doesn't go to the same requester, but that's a guess.
What I've also tried is to chain the $interval promise with the $http promise. Something like
var retryIntervalPromise = $interval(function(response) {
if(getTimeToRetry() <= 0) { //when retry timeout gets to 0, then retry
var $http = $injector.get('$http');
return $http(response.config);
}
},1000)
.then(function() {
var $http = $injector.get('$http');
return $http(response.config);
});
return retryIntervalPromise
But I've been informed that the only thing that can be done with the $interval promise is to cancel it, and that didn't seem to work anyway.
Does anybody have an idea for how this can be done?
Or whether the intent works and I've just done something horribly wrong?
Thanks!
You can return a "delay" promise from the responseError function, to which you chain the $http promise to try again. This is a simple example with a fixed timeout:
responseError: function(response) {
if (response.status === 0) {
var $http = $injector.get('$http');
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(true);
}, 5000);
return deferred.promise.then(function() {
return $http(response.config);
});
}
return $q.reject(response);
}
If you need a variable delay, you could store it in a variable within a factory or other service and have the rest of the user interface change it there. The next time the timeout fires, it will read the new value of the interval from the singleton and wait accordingly before firing the request again.
Could you explain in general what does this code do:
App.config(['$httpProvider', function ($httpProvider) {
$httpProvider.responseInterceptors.push('HttpSpinnerInterceptor');
$httpProvider.defaults.transformRequest.push(function (data, headersGetter) {
angular.element('.brand img').attr("src","<%= asset_path('brand/brand.gif') %>");
return data;
});
}]);
App.factory('HttpSpinnerInterceptor', function ($q, $window) {
return function (promise) {
return promise.then(function (response) {
angular.element('.brand img').attr("src","<%= asset_path('brand/brand.png') %>");
return response;
}, function (response) {
angular.element('.brand img').attr("src","<%= asset_path('brand/brand.png') %>");
return $q.reject(response);
});
};
});
I have completely no understanding except some guesses that it intercepts some response and injects a src attribute of image.
I do not understand how and when is HttpSpinnerInterceptor called and what the "promise" parameter is.
HttpSpinnerInterceptor is been called after each request issued by using $http service is completed (successfully or not), but before promise is been resolved to caller (so you can defer result). Actually transform request is not needed, because it does mostly same as HttpSpinnerInterceptor (or HttpSpinnerInterceptor is not needed...), because it does not transform anything.
promise parameter is a $q promise that could be used in case if you need to perform some async actions when with result of your request as you can resole it later, so caller would get result later. Actually in your code, you directly resolve this promise (or reject it), changing src attribute of the image.
Here are some links to documentation:
Using $http service: http://docs.angularjs.org/api/ng.$http - take careful look at "Response interceptors" and "Transforming Requests and Responses"
Promises in AngularJS: http://docs.angularjs.org/api/ng.$q