I have a global error handler for my angular app which is written as an $http interceptor, but I'd like to take it a step further. What I'd like is for each $http call that fails (is rejected), any "chained" consumers of the promise should first try to resolve the error, and if it is STILL unresolved (not caught), THEN I'd like the global error handler to take over.
Use case is, my global error handler shows a growl "alert box" at the top of the screen. But I have a couple of modals that pop up, and I handle the errors explicitly there, showing an error message in the modal itself. So, essentially, this modal controller should mark the rejected promise as "handled". But since the interceptor always seems to be the first to run on an $http error, I can't figure out a way to do it.
Here is my interceptor code:
angular.module("globalErrors", ['angular-growl', 'ngAnimate'])
.factory("myHttpInterceptor", ['$q', '$log', '$location', '$rootScope', 'growl', 'growlMessages',
function ($q, $log, $location, $rootScope, growl, growlMessages) {
var numLoading = 0;
return {
request: function (config) {
if (config.showLoader !== false) {
numLoading++;
$rootScope.loading = true;
}
return config || $q.when(config)
},
response: function (response) {
if (response.config.showLoader !== false) {
numLoading--;
$rootScope.loading = numLoading > 0;
}
if(growlMessages.getAllMessages().length) { // clear messages on next success XHR
growlMessages.destroyAllMessages();
}
return response || $q.when(response);
},
responseError: function (rejection) {
//$log.debug("error with status " + rejection.status + " and data: " + rejection.data['message']);
numLoading--;
$rootScope.loading = numLoading > 0;
switch (rejection.status) {
case 401:
document.location = "/auth/login";
growl.error("You are not logged in!");
break;
case 403:
growl.error("You don't have the right to do this: " + rejection.data);
break;
case 0:
growl.error("No connection, internet is down?");
break;
default:
if(!rejection.handled) {
if (rejection.data && rejection.data['message']) {
var mes = rejection.data['message'];
if (rejection.data.errors) {
for (var k in rejection.data.errors) {
mes += "<br/>" + rejection.data.errors[k];
}
}
growl.error("" + mes);
} else {
growl.error("There was an unknown error processing your request");
}
}
break;
}
return $q.reject(rejection);
}
};
}]).config(function ($provide, $httpProvider) {
return $httpProvider.interceptors.push('myHttpInterceptor');
})
This is rough code of how I'd expect the modal promise call to look like:
$http.get('/some/url').then(function(c) {
$uibModalInstance.close(c);
}, function(resp) {
if(resp.data.errors) {
$scope.errors = resp.data.errors;
resp.handled = true;
return resp;
}
});
1. Solution (hacky way)
You can do that by creating a service doing that for you. Because promises are chain-able and you basically mark a property handled at the controller level, you should pass this promise to your service and it'll take care of the unhandled errors.
myService.check(
$http.get('url/to/the/endpoint')
.then( succCallback, errorCallback)
);
2. Solution (preferred way)
Or the better solution would be to create a wrapper for $http and do something like this:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//optional solution, you can even say resp.handled = true
myhttp.setAsHandled(resp);
//do not forget to reject here, otherwise the chained promise will be recognised as a resolved promise.
$q.reject(resp);
}
Here the myhttp service call will apply the given success and failed callbacks and then it can chain his own faild callback and check if the handled property is true or false.
The myhttp service implementation (updated, added setAsHandled function which is just optional but it's a nicer solution since it keeps everything in one place (the attribute 'handled' easily changeable and in one place):
function myhttp($http){
var service = this;
service.setAsHandled = setAsHandled;
service.get = get;
function setAsHandled(resp){
resp.handled = true;
}
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(resp){
if(resp.handled !== true){
//your awesome popup message triggers here.
}
})
}
}
3. Solution
Same as #2 but less code needed to achieve the same:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//if you provide a failedCallback, and you still want to have your popup, then you need your reject.
$q.reject(resp);
}
Other example:
//since you didn't provide failed callback, it'll treat as a non-handled promise, and you'll have your popup.
myhttp.get('url/to/the/endpoint', successCallback);
function successCallback(){ ... }
The myhttp service implementation:
function myhttp($http){
var service = this;
service.get = get;
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(){
//your awesome popup message triggers here.
})
}
}
Related
I have a service below. I will call this service every time when I open a model and when I close the model and then open another one the previous values are getting reflected and in this case I want to cancel the promise every time I close the model.
I have tried the following code,
Model closing.js
$scope.closeButton = function() {
DetailDataSvc.storeDefer().resolve()
}
My Service, (DetailDataSvc)
self.storeDefer = function() {
return self.deferReturn;
};
self.getDetailReportData = function(postData, functionName) {
var promises = {};
var d = $q.defer(),
metricDataType;
self.deferReturn = $q.defer();
promises = {
detailReport: metricDataType,
recommendedMetrics: DataSvc.getData(_logPrefix + functionName, recommendedMetricUrl),
metricInfo: DataSvc.getData(_logPrefix + functionName, metricInfoUrl)
};
$q.all(promises).then(function(res) {
$log.debug(_logPrefix + 'getDetailReportData(). Called from %s. $q.all Response (raw): ', functionName, res);
else {
if (response && !_.isEmpty(_.get(response, 'largeCard.chartData.dataValues.rows')) && response.overlayEnabled) {
self.getMetricOverLay(pdata, functionName).then(function(overlayData) {
response.largeCard.chartData.overlay = overlayData;
d.resolve(response);
}, function(msg, code) {
d.reject(msg);
$log.error(_logPrefix + 'getDetailReportData(). Error code: %s. Error: ', code, msg);
});
} else {
d.resolve(response);
}
}
}, function(msg, code) {
d.reject(msg);
$log.error(_logPrefix + 'getDetailReportData(). Error code: %s. Error: ', code, msg);
});
return d.promise;
};
Can anyone please help me whether the process I followed is the right one.
What you have attempted could be made to work but it's best fixed by racing the promise returned by $q.all() against a rejectable Deferred (ie. a Deferred, of which a reference is kept to its reject method), thus avoiding the deferred anti-pattern.
self.getDetailReportData = function(postData, functionName) {
var metricDataType = ......; // ???
var d = $q.defer();
// cancel previous
if(self.cancelDetailReport) {
self.cancelDetailReport(new Error('previous getDetailReportData() cancelled'));
}
// keep a reference to the deferred's reject method for next time round.
self.cancelDetailReport = d.reject;
var promises = {
'detailReport': metricDataType,
'recommendedMetrics': DataSvc.getData(_logPrefix + functionName, recommendedMetricUrl),
'metricInfo': DataSvc.getData(_logPrefix + functionName, metricInfoUrl)
};
// Race aggregated `promises` against `d.promise`, thus providing the required cancellation effect.
return $q.race([$q.all(promises), d.promise])
.then(function(response) {
// arrive here only if all promises resolve and d.reject() has not been called.
$log.debug(_logPrefix + 'getDetailReportData(). Called from %s. $q.all Response (raw): ', functionName, response);
if (response && !_.isEmpty(_.get(response, 'largeCard.chartData.dataValues.rows')) && response.overlayEnabled) {
return self.getMetricOverLay(pdata, functionName)
.then(function(overlayData) {
response.largeCard.chartData.overlay = overlayData;
return response;
});
} else {
return response;
}
})
.catch(function(msg, code) { // signature?
// all error cases including cancellation end up here.
var message = _logPrefix + `getDetailReportData(). Error: (${code}): ${msg}`; // or similar
$log.error(message);
throw new Error(message); // see https://stackoverflow.com/a/42250798/3478010
});
};
Notes:
$q.race() is transparent to whichever promise wins the race, and opaque to the other. So, if the d is rejected before the promise returned by $q.all() settles, then d will win out; response handling will not happen and d's rejection will fall through to the .catch() clause. Alternatively, if the promise returned by $q.all(promises) wins out then flow will follow that promise's success path (ie response handling) or possibly its error path (which will drop through to the .catch() clause).
Not too sure about the signature of the .catch() callback. You would normally expect it to accept a single error argument.
Assign already created deferred.
Try and change this line:
self.deferReturn = $q.defer();
self.deferReturn = d;
i have written a polling service in AngularJS and want to start the service if my post request is done.But if I call the gui, the poll service is active.
i have try to implement a start function, end function and call the start() function if the post request is done.. but it doesnt work :/
My poll service :
.factory('NotificationPollService',
['$http', '$q', '$interval',
function ($http, $q, $interval) {
var deferred = $q.defer();
var notification = {};
notification.poller = $interval(function(id) {
$http.get('http://localhost:9999/v1/jmeter/id', {cache: false})
.success(function(data, status, headers, config) {
return data;
}, 10000);
});
notification.endPolling = function() {$interval.cancel(this.interval);};
}])
and the controller which i post the request
.controller('HomeController',
['$scope', '$rootScope', 'SendJmeterFile', 'NotificationPollService',
function ($scope, $rootScope, SendJmeterFile , NotificationPollService) {
$scope.upload = function() {
var customArtifacts = "";
var testDataBase = "";
if($scope.jmeterFile.customArtifact == undefined){
customArtifacts = null;
} else {customArtifacts = $scope.jmeterFile.customArtifact.base64}
if($scope.jmeterFile.testDataBase == undefined){
testDataBase = null;
} else {testDataBase = $scope.jmeterFile.testDataBase.base64}
SendJmeterFile.upload($scope.jmeterFile.jmeter.base64, customArtifacts, $scope.jmeterFile.customProperties, $scope.jmeterFile.instanceConfiguration, $scope.jmeterFile.instances, $scope.jmeterFile.providerID, testDataBase)
.then(function(data) {
alert("Daten erfolgreich verschickt!");
console.log(data);
NotificationPollService.poller(data.id)
//.then(function(data) {
/*if(data.status == "SETUP")
if(data.status == "TEST")
if(data.status == "DONE")
if(data.status == "ERROR")
}), function(data) {
})*/
}, function(data) {
alert("Fehler!");
console.log(data);
});
};
}])
One problem is that $interval() is called immediately upon injection into your controller. Your hunch to implement a 'Start' method or something similar was a good one - but you can probably simplify it even more by letting the factory return a function. Then you can just instantiate that function in your controller as many times as you need a Poller.
However, there are more problems. A promise can only be resolved once, and since you execute a HTTP request multiple times, my guess is that you want to be 'notified' of state changes until the state is marked as 'Done'. You're currently putting the responsibility for checking the state with the controller. If all you want is to be notified of "error" and "success" steps however, it is probably much better to let the Poller service be responsible for interpreting the state information that comes back from your service, and simply depend on standard promise behaviour in your controller. I opted to show an example of the latter case:
UPDATE: sample plnkr here: http://plnkr.co/edit/e7vqU82fqYGQuCwufPZN?p=preview
angular.module('MYMODULENAMEHERE')
.factory('NotificationPoller',
['$http', '$q', '$interval',
function ($http, $q, $interval) {
return poller;
function poller(id) {
var _this = this;
var deferred = $q.defer();
var interval = $interval(function() {
$http
// I assumed you actually want to use the value of 'id' in your
// url here, rather than just the word 'id'.
.get('http://localhost:9999/v1/jmeter/' + id, {cache: false})
.then(function(data, status, headers, config) {
// I commented out the following statement. It is meaningless because
// you can't do anything with the return value since it's an anonymous
// function you're returning it from. Instead, you probably meant to
// use the promise to return the data.
// return data;
if(data.status == "SETUP") {
deferred.notify(data);
}
else if(data.status == "TEST") {
deferred.notify(data);
}
else if(data.status == "DONE") {
_this.endPolling(); // Automatically stop polling when status is done
deferred.resolve(data);
}
else { // data.status == "ERROR" (or anything else that's not expected)
_this.endPolling(); // Automatically stop polling on error
deferred.reject(data);
}
}, function(data) {
_this.endPolling();
deferred.reject(data);
});
}, 10000);
this.endPolling = function() {
$interval.cancel(interval);
};
// Make the promise available to calling code
this.promise = deferred.promise;
};
}])
Now your controller can much more easily use your polling service. Here's an example of a stripped-down controller using your polling service, for clarity:
angular.module('MYMODULENAMEHERE')
.controller('HomeController', [
'NotificationPoller',
function(NotificationPoller) {
var some_id = 207810;
var poller = new NotificationPoller(some_id);
poller.promise.then(onSuccess, onError, onNotify);
function onSuccess(data) {
// data.status == "DONE"
};
function onError(data) {
// data.status == "ERROR"
};
function onNotify(data) {
// data.status == "TEST" || data.status == "SETUP"
};
}]);
As you see, the factory has received a little more responsibility this way, but your controller doesn't need to be aware of the details of all the statuses that the backend can send anymore. It just uses standard promises.
You try to call NotificationPollService.poller(data.id) which is Promise, actually, because previously in NotificationPollService you assigned notification.poller like so
notification.poller = $interval(function(id) {
$http.get('http://localhost:9999/v1/jmeter/id', {cache: false})
.success(function(data, status, headers, config) {
return data;
}, 10000);
});
Now your notification.poller is a return value of $interval function.
To make it work you should wrap the function so you could actually pass an id to it.
I'm trying to make promise inside a factory and then validate in locationChangeStart. The problem is that the locationChangeStart doesn't wait for my promise. What can I do to make my promise wait to complete?
Here is my code,
app.run(['$rootScope','$location','KeyFactory',
function($root, $location,KeyFactory) {
$root.$on('$locationChangeStart', function(event, curr, prev) {
KeyFactory.check();
console.log(KeyFactory.GetKeyPass()); ///PRINT undefined
if(KeyFactory.GetKeyPass()== true){
console.log('authorised');
}else{
$location.path('/login');
}
});
}]);
app.factory('KeyFactory', ['$http','$log', function($http,$log) {
var key = {};
key.setKeyPass = function(set) {
key.Status = set;
}
key.GetKeyPass = function() {
return key.Status;
}
key.check = function(){
$http.post('http://localhost/api/CheckPass').success(function(data) {
console.log(data);
key.setKeyPass(true);
}).error(function (data, status){
$log.error("error you cant acess here!");
console.log(status);
});
}
return key;
}]);
Asynchronous code doesn't work in synchronous way as you are thinking. After making an ajax it doesn't respond in the next line. In angular after making an ajax it return an promise object which is responsible to tell that response/error is going to happen.
There are couple of things missing in your code.
You should return a promise from the check method of service.
Then put .then function on check method promise & expect response in its success/error callback.
Code
key.check = function(){
return $http.post('http://localhost/api/CheckPass').then(function(response) {
var data = response.data;
key.setKeyPass(true);
}, function (response){
key.setKeyPass(false);
});
Run
KeyFactory.check().then(function(){
if(KeyFactory.GetKeyPass()== true){
console.log('authorised');
}else{
$location.path('/login');
}
});
communicating with a REST service in ionic, I'ld like to have functions similar to this
function ListCategories_Request(){
rqst=_BuildRequest("ListCategories");
rqst.Data={extraParam1:1,
extraParam2:2
}
return $http({...ValidParameters including the rqst...}).then(GetResult,Request_onError)
}
For each function of the REST-Service, I would build a similar java-script function.
Now, as the sent request is unique, the received result needs to be handled unique, too. The REST-Service-Based analysis of the result is added to the .then-chain, but updating the parent class and the UI needs to happen in the parent class.
So, I would like to do calls like
ListCategories_Request().then(function(res){ UpdateCategories()});
ListFrames_Request().then(function(res){ UpdateFrames()});
The current problem is, that UpdateCategories() is called, before the result of the http-Request is analysed.
So, how do I prevent return $http(...).then(GetResult,OnError) to return, before the specific function inside GetResult is called?
Code of GetResult:
function GetResult( res){
if(res.status==200)
{
if( (res.data!=={}) && (res.data.Data!=={}) )
{
return AnalyzeResult(res.data);
}
}
};
While AnalyzeResult is like:
function AnalyzeResult(Result)
{
Func=Result.Func;
switch(Func.toUpperCase())
{
case "LISTCATEGORIES":
erg = ListCategories_Result(Result);
break;
case "LISTFRAMES":
erg= ListFrames_Result(Result);
break;
default:
erg = {};
console.log("UnKnown Result!");
}
FinalizeRequest(Index);
}
return erg;
}
So, I do not really get, where my mistake is. How do I prevent ListCategories_Request or ListFrames_Request from returning too early?
Thank you and best regards
Frank
You are better off using $q (given you are using angular anyway via ionic). That way you can use promises to do what you want to do. Here's an example that I put together to demonstrate the idea:
var app = angular.module("TestApp",[]);
app.controller("TestController", function($scope, $http, $q){
$scope.message = "Deferred Example";
var deferred = $q.defer();
function getAllPosts(extractor) {
$http.get("https://jsonplaceholder.typicode.com/posts")
.then(function(data){
console.log("Data is: ", data);
$scope.postIds = extractor(data.data);
deferred.resolve($scope.postIds);
});
return deferred.promise;
}
function extractPostIds(data) {
console.log("Exracting the data: ", data);
return data.map(function(post){
return post.id;
})
}
function squarePostIds(postIds) {
console.log("Squaring post ids: ", postIds);
$scope.squaredPostIds = postIds.map(function(id){ return id*id;});
}
getAllPosts(extractPostIds)
.then(function(postIds){
squarePostIds(postIds);
});
});
And here's the JSBin for this: https://jsbin.com/fugeyad/3/edit?html,js,output
Update: Adding some links to read about promises.
http://andyshora.com/promises-angularjs-explained-as-cartoon.html
http://www.html5rocks.com/en/tutorials/es6/promises/
I am doing custom $http service that looks something like this:
angular.factory('myHttp', function($http){
var obj = {};
obj.get = function(path) {
return $http.get(path,{timeout: 5000}).error(function (result) {
console.log("retrying");
return obj.get(path);
});
}
});
The system works fine. It does return the data when success, and retrying when connection fail. However, I am facing problem that it will return to controller when the connection is timeout. How can I prevent it from returning and continue retrying?
You need to use $q.reject. This will indicate that the error handler failed again and the result should populated to the parent error handler - NOT the success handler.
obj.get = function(path) {
return $http.get(path, {
timeout: 5000
}).then(null, function(result) {
console.log("retrying");
if (i < retry) {
i += 1;
return obj.get(path);
} else {
return $q.reject(result); // <-- use $q.reject
}
});
}
See plunker
See the reject.status to determine the timeout
$http.get('/path', { timeout: 5000 })
.then(function(){
// Your request served
},function(reject){
if(reject.status === 0) {
// $http timeout
} else {
// response error
}
});
Please see the following question for a good overview about handling timeout errors:
Angular $http : setting a promise on the 'timeout' config