I am using promise in controller to render the view after $http
request. Everything works fine except when the http interceptor gets involved during 401 errors. When that happens, this is the issue I am facing:
The http interceptor picks up on the 401 errors from api, adds the rejected query to buffer and shows the Login Modal.
After the login is successful, the queued api queries that had 401 before is tried again and the api returns the requested data successfully.
However what happens is, even when the data is returned from api, the angular view is not updated. The view only updates after the manually refresh that state.
These are my codes:
1) http interceptor:
I am using http-auth-interceptor
in this
$httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
return {
responseError: function(rejection) {
if (!rejection.config.ignoreAuthModule) {
var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
angular.forEach(rejectionReasons, function(value, key) {
if(rejection.data.error === value) {
var deferred = $q.defer();
httpBuffer.append(rejection.config, deferred);
$rootScope.$broadcast('event:auth-loginRequired', rejection);
return deferred.promise;
}
})
}
// otherwise, default behaviour
return $q.reject(rejection);
}
};
}]);
2) http interceptor watch:
scope.$on('event:auth-loginRequired', function() {
LoginModalService.openLoginModal();
console.log('login needed');
});
scope.$on('event:auth-loginCancelled', function() {
$state.go('index');
});
3) Controller:
.controller('FetchData', ['$scope', 'DataService',
function($scope, DataService) {
DataService.fetchNamesList()
.success(function(names) {
$scope.namesList = names;
}).error(function(error) {
// The login modal should show if auth error occurs
});
}])
Here, the DataService.fetchNamesList()
does a get request from api.
If an auth interceptor is triggered for the above, I can see the $http $_GET
is tried again after succesful login but the $scope.namesList
is not updating in view.
What I have thought of so far:
1) I was thinking of adding an additional $watch
for the above in order for it to work after http interceptor. I thought of that then I discarded that option since if I have mutliple get requests in one page (like the dashboard), then watching each and every request can go overboard.
2) Manually refresh the ui router state after the login like below. This works but it doesnt help when the view involves a form submit where the completed form will be reset after state reload:
$state.go($state.current, {}, {reload: true});
So I am sure how to resolve this. Can someone guide me here please....
EDIT:
After @ChrisFoster's suggestion in comments, I moved the return deferred.promise
outside angular.forEach
and slightly changed the foreach check. This seems to update the view correctly after login. However, now the modal shows for every errors and not just the ones listed in rejectionReasons:
$httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
return {
responseError: function(rejection) {
if (!rejection.config.ignoreAuthModule) {
var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
// Loop through each rejection reason and redirect
angular.forEach(rejectionReasons, function(value, key) {
if(!(rejection.data.error === value)) {
return $q.reject(rejection);
}
});
var deferred = $q.defer();
httpBuffer.append(rejection.config, deferred);
$rootScope.$broadcast('event:auth-loginRequired', rejection);
return deferred.promise;
}
// otherwise, default behaviour
return $q.reject(rejection);
}
};
}]);
Glad to see your answer is working. You can still use angular.forEach
if you'd like, here is an example of what that would look like:
$httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
return {
responseError: function(rejection) {
var loginRequired = false;
var rejectionReasons = [
'token_not_provided', 'token_expired',
'token_absent', 'token_invalid'
];
// Loop through each rejection reason
angular.forEach(rejectionReasons, function(value, key) {
if (!(rejection.data.error === value)) {
// Set loginRequired, and check this later
// We *can't* return here because we are inside a function :)
loginRequired = true;
}
});
// Create a promise
var deferred = $q.defer();
if (loginRequired && !rejection.config.ignoreAuthModule) {
// Display a login module and queue a retry request
httpBuffer.append(rejection.config, deferred);
$rootScope.$broadcast('event:auth-loginRequired', rejection);
} else {
// Not a auth error, or ignoreAuthModule is enabled
// Therefore immediately reject the promise with the fail value
deferred.reject(rejection);
}
// Return the promise
return deferred.promise;
}
};
}]);
The reason that you were running into issues with forEach
is because you pass a function as an argument to forEach
. When you return
inside of there, you are returning that value in the forEach
function, not in the HTTP interceptor function. Therefore, you have three options:
for
loopif
statement conditionalYour example is using the second approach, but if you still wanted to use angular.forEach
my code above is a demonstration of the third approach. The first approach is possible as well, but slightly less common and a little more messy.