Search code examples
angularjsangularjs-scopeangular-ui-routerangular-servicesangular-http-interceptors

Angular View doesnt update after http interceptor


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:

  1. The http interceptor picks up on the 401 errors from api, adds the rejected query to buffer and shows the Login Modal.

  2. After the login is successful, the queued api queries that had 401 before is tried again and the api returns the requested data successfully.

  3. 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);
        }
      };
    }]);

Solution

  • 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:

    1. You could use a traditional javascript for loop
    2. You could use an if statement conditional
    3. You could use an "external" variable you keep track of

    Your 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.