I have a service that contains a generic call to $http that all ajax calls uses. Here I have "centralized" error handling where the status codes results in redirects.
"FieldsSync" service:
return $http({
method: 'POST',
url: url,
data: $.extend(defaultPostData, postData)
}).error(function(data, status) {
switch (status) {
case 401: // "Unauthorized". Not logged in.
redirect.toLoginPage();
break;
case 403: // "Forbidden". Role has changed.
redirect.toLogoutPage();
break;
}
});
When calling one of the service functions from the controller I always return the deferred object to be able to hook up more error callbacks to be able to handle errors that should result in some kind of feedback for the user.
Controller:
fieldsSync.createField(newField).success(function(data) {
...
}).error(function(data, status) {
switch (status) { // <--- DO NOT WANT
case 401:
case 403:
return; // These errors are handled (=redirects) in the generic error callback and we don't want to show do anything while it redirects.
}
... // "Individual" error handling. =Displaying messages and stuff
});
But because I don't want error messages popping up before the redirect occurs I have to exit the error callback if the status codes already have been handled.
The question is:
How do i get rid of the switch cases in my controllers?
Is it possible to exit the chain of error callbacks when a specific error code has been handled? Or is it possible to work around this in a less ugly way? :)
This is a reoccurring problem for me, and my mind seems stuck.
I have checked the docs and cant find any pretty solution for this in either $http or $q.
I propose you to use a responseInterceptor to handle those cases:
http://docs.angularjs.org/api/ng.$http
I wrote the following, when I had the same "problem" as you. Its only a first draft, but you can improve it to your needs
InternalServerErrorResponseInterceptor = function($q, InternalServerErrorService) {
return function(promise) {
return promise.then(function(response) {
return response;
}, function(response) {
if (response.status > 499) {
InternalServerErrorService.handleError();
return $q.reject(response);
}
return $q.reject(response);
});
};
};
module.factory('InternalServerErrorResponseInterceptor', ['$q', 'InternalServerErrorService', InternalServerErrorResponseInterceptor]);
module.config(['$httpProvider', function($httpProvider) {
$httpProvider.responseInterceptors.push('InternalServerErrorResponseInterceptor');
}]);
Now your specific InternalServerErrorService can handle it the way you want it to be handled :)
regards
One solution I come up with is to redo the sync functions (puh!) so that they DONT'T return the deferred object. The functions inside the service could take the callbacks as arguments. I could then check if the error already has been handled by the generic error handling before it is called.
Pseudocode:
Service:
fieldsSync.createField(newField, successCallback, failCallback) {
...
genericErrorHandlerResultingInRedirects()
if (not 401 || 403) {
failCallback()
}
};
Controller:
fieldsSync.createField({}, function successCallback(){}, function failCallback(){
/* Handle errors that should give the user feedback */
});
Other solutions are most welcome.
Related
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.
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);
});
}
Every time I hit the servers using any $resource I want to show the same alert to my users whenever it fails.
Today, it looks like:
function tryAgain() { alert("try again") }
myResource.query().$promise.catch(tryAgain);
myResource.update(...).$promise.catch(tryAgain);
myResource.delete(...).$promise.catch(tryAgain);
otherResource.query().$promise.catch(tryAgain);
Is there a way to configure the default error handling function for ngResource? I'm looking for something like:
$resource.somethingMagicHere.defaults.catch(tryAgain);
You can use an interceptor in your app.config() section. This will catch all response errors originating from $http which $resource uses.
$httpProvider.interceptors.push(function($q) {
return {
'responseError': function(response) {
if (response.status == 401) {
// Handle 401 error code
}
if (response.status == 500) {
// Handle 500 error code
}
// Always reject (or resolve) the deferred you're given
return $q.reject(response);
}
};
});
The #kba answer helped me find the path. The following article made me understand it:
http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/
Just declare it once and reuse:
var defaultErrorHandler = function() {
alert("try again")
}
myResource.query(...).$promise.catch(defaultErrorHandler());
myResource.update(...).$promise.catch(defaultErrorHandler());
...
Hello I am using ngResource $save method and I get two different behaviours, I don't understand why
First I'm using it in this way:
$scope.user = new User($scope.user);
$scope.user.$save(function () {
$window.location.href = //redirection here;
}, function (response) {
$scope.form.addErrors(response.data.errors);
});
Then I have another controller when I'm doing a similar operation, but even getting 404 or 422 errors from the server the first callback is executed and the errors callback is ignored.
Does anyone have any idea of this? I've been searching in Google for hours trying to find more documentation about $save but I'm still stuck with this problem.
Thank you.
Well, the problem was on an interceptor I am using to detect 401 (unauthorized errors)
here is the interceptor, notice that you must return $q.reject(response) otherwise the other callbacks are not called (in my case the error callback in ngResource.$save)
MyApp.config(function ($httpProvider) {
$httpProvider.interceptors.push(function($window, $q) {
return {
'responseError': function(response) {
if (response.status == 401) { // Unathorized
$window.location.href = 'index.html';
}
// return response; <-- I was doing this before cancelling all errors
return $q.reject(response);
}
};
});
});
I have a message system in place that has two methods show and clear. I want to use $http interceptors in order to handle authentication because my server is smart enough to know whether a user is authenticated or not per request and will tell me so by sending me me a status response of 401 or 403. So, I have something like this for my interceptor:
myModule.factory('statusInterceptor', function($location, MessageService, urlManager){
return {
responseError: function(response){
switch(response.status){
...
case 401:
case 403:
$location.url(urlManager.reverse('Login'));
MessageService.show('Dude, you need to log in first.');
break;
case 500:
break;
...
}
return response;
}
};
});
That works just fine on either a 401 or a 403 response as it shows the message as expected. The problem I'm having is clearing the message whenever the user logs in or goes to another route. I have a MainCtrl that is in charge of almost everything and one of the things that it is looking after is $routeChangeStart. My thought being that the view is changing to a different view, so I want to clear the messages at the beginning of the route switch. So, in my MainCtrl I have:
myControllers.controller('MainCtrl', function($scope, MessageService){
$scope.$on('$routeChangeStart', function(){
MessageService.clear();
});
});
Here's the way I expect the app to react:
A user has tried to do something without being authenticated by sending a request to the server
The server responds with either a 401 or 403 because the user is not authenticated.
The responseError function is fired calling my $location.url() method which, in my mind, should fire the $routeChangeStart, or even $locationChangeStart, events (I've tried both).
This should in turn fire my MessageService.clear() method and clear out any previous message.
The user is finally redirected to the login page with the correct "Dude, you need to log in first." message.
What really happens:
The user is redirected to the login page as expected, however, the error message does not display. When setting certain debug points in Chrome, I see that $location.path() is called immediately followed by MessageService.show('Dude...'), finally followed by the MessageService.clear() call.
Maybe you like this approach to that problem
http://jsfiddle.net/jYCPQ/1/
module.factory('authenticationInterceptor', function ($q, navigator, MessageService) {
return {
responseError: function (response) {
switch (response.status) {
case 403:
navigator.navigateTo("/login").then(function () {
MessageService.show('Dude, you need to log in first.');
});
return $q.reject(response);
}
return response;
}
};
});
module.factory("navigator", function ($location, $rootScope, $q) {
var navigateTo = function (url) {
var defered = $q.defer();
$location.url(url);
var unbind = $rootScope.$on('$routeChangeSuccess', function () {
defered.resolve();
unbind();
})
return defered.promise;
}
return {
navigateTo: navigateTo
}
});
regards
I finally found a solution and ended up with something like this:
In my interceptor function:
myModule.factory('statusInterceptor', function($rootScope){
return {
responseError: function(response){
switch(response.status){
...
case 401:
case 403:
$rootScope.$broadcast('event:notAuthenticated');
break;
case 500:
break;
...
}
return response;
}
};
});
And in my controller:
myControllers.controller('MainCtrl', function($scope, $location, MessageService){
$scope.$on('event:notAuthenticated', function(){
$location.path(urlManager.reverse('Login'));
var remove_this_binding = $scope.$on('$routeChangeSuccess', function(){
messageService.show('Dude, you need to log in first.');
remove_this_binding();
});
});
$scope.$on('$routeChangeStart', function(){
messageService.clear();
});
});
I was trying to find an angular way of having a callback associated with $location's path change and this is the best I could come up with. If anyone else has a better solution, I'm all ears.