angularjs break forEach in $http success - angularjs

I have following code in Ionic framework,
var stopScan = false;
$scope.StopScan = function() {
stopScan = true;
};
$scope.ScanContacts = function() {
Contacts.unchecked().then(function(contacts) {
var promise = $q.all(null);
angular.forEach(contacts, function(contact) {
promise = promise.then(function() {
return $http.post(apiEndpoint+'/check', {number: contact.number})
.success(function(res) {
Contacts.update(contact.id, res);
if(stopScan)
// do break loop;
})
.error(function(err) {
console.log(err);
});
});
});
});
};
It's do sending http request in loop synchronously, and break on $http error, exactly like I wanted. But how I do break the loop in the $http success? I've tried throw 'Scan stopped'; and $q.reject('Scan stopped'); but no success.

First of all, angular.forEach does not support breaking (see here and here)
Second, break statement must be directly nested within the loop, even if it was a for or while loop.
And lastly, .success is happening asynchronously, after the loop has executed, so breaking there via some other mean would have been meaningless anyway.
It seems like you expect stopScan to be set asynchronously elsewhere (for example, in response to a click from the user), but you have to decide exactly what it means to stop - does it mean "do not make any more $http.post requests", or does it mean "make all the requests, but don't not process the response?". (Your example seems to imply the latter, because you're attempting to handle it in .success, but you should know, though, that POST typically implies that changes were made on the server).
You have to understand that once you kick off an HTTP request, it's going out (or it's pending, subject to max number of connections, which is browser-dependent).
So, what you could do is fire all of the requests at once and in parallel, and then manually "timeout" ($http supports a promise-based timeout) the ones that haven't been completed:
var stopScanTimeout = $q(function(resolve){
$scope.stopScan = function(){
resolve();
}
})
var promises = [];
angular.forEach(contacts, function(contact) {
var httpPromise = $http({ method: "POST",
url: apiEndpoint+'/check',
data: {number: contact.number},
timeout: stopScanTimeout })
.then(function(response){ return response.data; },
function(error) { return {error: error};});
promises.push(httpPromise);
});
Then you could handle all the results together, and some would be "errors" (but "soft" errors) if they were not completed in time:
$q.all(promises).then(function(results){
for (var i = 0; i < results.length, i++){
var result = results[i];
if (result.error) continue;
// otherwise, process the result
Contacts.update(contact.id, result);
}
})

If you want to run with parallel HTTP requests, then go with #NewDev's answer.
However if you want to stick with serial requests, then "breaking out of the loop" couldn't be simpler.
All you need to do is throw, which won't break as such but will send the constructed promise chain down its error path. At the stop point, there will be no unreturned requests and no more requests will be sent.
I would write something like this, using contacts.reduce(...) to build the chain.
$scope.ScanContacts = function() {
return Contacts.unchecked().then(function(contacts) {
return contacts.reduce(function (p, contact) {
return p.then(function() {
return $http.post(apiEndpoint + '/check', { number: contact.number })
.then(function(res) {
if(stopScan) throw new Error('scan stopped');
Contacts.update(contact.id, res);//you can choose to service the last response or not but placing this line above or below the throw line.
}, function(err) {
// As the second .then param, this callback will catch any http errors but not the 'scan stopped' error.
// By catching http errors, the scan will be allows to continue.
// To stop on http error, either remove this callback or rethrow the error.
console.log(err);
});
});
}, $q.when());
});
};
Here's evidence that throwing will give the required "stop" effect.
If throwing doesn't work in the real code, then it would seem that something else is wrong.

Related

How to access $http response from the finally block

I have an $http request that is handled by a success and error function, and there is something I would like to do with the response in both cases:
$http(params)
.then(success, error);
var success = function(response) {
// do stuff
foo(response);
}
var error = function(response) {
// do other stuff
foo(response);
}
I'd prefer not to repeat code in both handlers, and I thought I might use finally to solve this problem but it seems the finally function doesn't receive any arguments.
Am I stuck calling foo(response) from both the success and the error function? Or is there a better way? (please say there is a better way...)
What you can do is convert the failure into success:
$http(params)
.then(success, error)
.then(foo);
var success = function(response) {
// do stuff
return response;
}
var error = function(response) {
// do other stuff
return response;
}
finally callback is called with no arguments. The manual reasonably explains this:
finally(callback, notifyCallback) – allows you to observe either the
fulfillment or rejection of a promise, but to do so without modifying
the final value.
This behaviour complies with other promise implementations (particularly Q, which was the inspiration for $q).
The pattern for result processing may be
$http(...)
.catch(function (err) {
// condition err response
return err;
})
.then(function (result) {
// ...
});
This isn't the same as finally, because this chain results in fulfilled promise (unless the rejection took place in then), while finally doesn't affect chain state (unless the rejection took place in finally).

Force rejecting Angular $http call

I am using $http to make a call. Based on the successful result of the call I may decided to throw an error/reject and have it trickle down to the next call as an error. However if an error is thrown it just halt the process. How can I force the $http promise to reject without wrapping it in some $q code?
// A service
angular.module('app').factory('aService', function ($http, config) {
return {
subscribe: function (params) {
return $http({
url: '...'
method: 'JSONP'
}).then(function (res) {
// This is a successful http call but may be a failure as far as I am concerned so I want the calling code to treat it so.
if (res.data.result === 'error') throw new Error('Big Errror')
}, function (err) {
return err
})
}
}
})
// Controller
aService.subscribe({
'email': '...'
}).then(function (result) {
}, function (result) {
// I want this to be the Big Error message. How do I get here from the success call above?
})
In the above code I would like the Big Error message to end up as a rejected call. However in this case it just dies with the error. This is how I handle things in say Bluebird but it's a no go here.
Ti continue the Chain in a rejected state just return a rejected promise $q.reject('reason') from your $http result something like
$http.get(url).then(
function (response){
if(something){
return $q.reject('reason');
}
return response;
}
)
That way you'll get a a rejected promise and can react to it even when the api call is successful.

Angular test using $httpBackend fails with "400 thrown" error

For hours I've been trying to test my NewPostController with $httpBackend. The problem is whenever I set non-2xx status code in the response, the test fails.
NewPostController has the following method:
$scope.submit = function () {
var newPost = $scope.newPost;
PostService.insertPost(newPost).then(function (post) {
$location.path("/my-posts");
}, function (status) {
$scope.form.$setPristine();
$scope.error = status;
});
};
I have a problem testing the failure path:
it(...) {
...
$scope.post.text = "";
$httpBackend.expectPOST("/create-post", {"post":$scope.post}).respond(400);
$scope.submit();
$httpBackend.flush();
expect($scope.error).toBeDefined();
$scope.post.text = "This is a valid text.";
$httpBackend.expectPOST("/create-post", {"post": $scope.post}).respond(200);
$scope.submit();
$httpBackend.flush();
expect($location.path()).toBe("/my-posts");
});
The test fails with a message "400 thrown" (no callstack). I tried to change the order of subtests, use whenPOST instead of expectPOST and combine the methods as they do in Angular docs (https://docs.angularjs.org/api/ngMock/service/$httpBackend) but without success.
Please help.
EDIT:
Now when I look at PostService, it makes sense where the "400 thrown" comes from but I expected the error to be handled by angular. I threw it because of the section "Handling problems in nested service calls" of this article. It is supposed to be a shorter version of deferred.resolve/reject mechanism.
this.insertPost = function (newPost) {
return $http({
method: "post",
url: "/create-post",
data: {
post: newPost
}
}).then(function (res) {
return (res.data);
}, function (res) {
throw res.status;
});
};
This is indeed strange, and is perhaps something the angular team didn't consider.
When a promise is rejected by throwing (as you're doing), the angular $exceptionHandler service is called with the thrown exception. By default, this service just logs the exception in the browser console.
But when using ngMocks, this service is replaced by a mock implementation that can either log or rethrow the exception. The default mode is to rethrow, in order to make a test fail when an exception is thrown.
My advice would be to avoid using throw to simply reject a promise, and thus replace
function (res) {
throw res.status;
}
by
function (res) {
return $q.reject(res.status);
}
But if you really want to keep using throw, you can also configure the mock exceptionHandler to log instead of rethrowing:
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));

In AngularJS, using HTTP-Interceptor, $interval & $http to wait a while and then retry a timed-out http request

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.

How to handle a single 404 in AngularJS's $q

I have a couple chained $http combined with a single $http using $q.all([prmiseA, promiseB]). Everything is working fine, I get the data back and errors are handled no problem.
Except that on occasion data won't be found on a particular http call and it is not an error.
I am using a service to separate the logic from the UI. And my call looks like this
$scope.Loading = true;
var p = Service.Call(Param1, Param2);
p.then(function () {
$scope.Loading = false;
}, function (reason) {
$scope.Loading = false;
$scope.alerts.push({ msg: "Error loading information " + Param1, type: "danger" });
})
What I would like to be able to do is handling the 404 on that one URL inside the 'Service.Call' function. So that the UI code above remains untouched.
My problem is that if I add an error handler to the specific call that may return a 404. Then all errors are "handled" and so I loose errors for that one call.
Is there a way to "reraise" in $q?
Is there a way to "reraise" in $q?
Yes, you can rethrow by returning a rejected promise from the handler:
return $q.reject(new Error("Re Thrown")); // this is an actual `throw` in most
// promise implemenentations
In case an $http call 404 is not an error, you can recover from it. One of the cool features of promises is that we get to recover from errors:
var makeCallAndRecover(url){
return $http.get(...).catch(function(err){
// recover here if err is 404
if(err.status === 404) return null; //returning recovery
// otherwise return a $q.reject
return $q.reject(err);
});
}

Resources