Injecting $state (ui-router) into $http interceptor causes circular dependency

AngularjsAngular Ui-Router

Angularjs Problem Overview


What I'm trying to achieve

I would like to to transition to a certain state (login) in case an $http request returns a 401 error. I have therefore created an $http interceptor.

The problem

When I am trying to insert '$state' into the interceptor I get a circular dependency. Why and how do i fix it?

Code

//Inside Config function

	var interceptor = ['$location', '$q', '$state', function($location, $q, $state) {
		function success(response) {
			return response;
		}

		function error(response) {

			if(response.status === 401) {
				$state.transitionTo('public.login');
				return $q.reject(response);
			}
			else {
				return $q.reject(response);
			}
		}

		return function(promise) {
			return promise.then(success, error);
		}
	}];

	$httpProvider.responseInterceptors.push(interceptor);

Angularjs Solutions


Solution 1 - Angularjs

##The Fix##

Use the $injector service to get a reference to the $state service.

var interceptor = ['$location', '$q', '$injector', function($location, $q, $injector) {
    function success(response) {
        return response;
    }

    function error(response) {

        if(response.status === 401) {
            $injector.get('$state').transitionTo('public.login');
            return $q.reject(response);
        }
        else {
            return $q.reject(response);
        }
    }

    return function(promise) {
        return promise.then(success, error);
    }
}];

$httpProvider.responseInterceptors.push(interceptor);

##The Cause##

angular-ui-router injects the $http service as a dependency into $TemplateFactory which then creates a circular reference to $http within the $httpProvider itself upon dispatching the interceptor.

The same circular dependency exception would be thrown if you attempt to inject the $http service directly into an interceptor like so.

var interceptor = ['$location', '$q', '$http', function($location, $q, $http) {

##Separation of Concerns##

Circular dependency exceptions can indicate that there is a mixing of concerns within your application which could cause stability issues. If you find yourself with this exception you should take the time to look at your architecture to ensure you avoid any dependencies that end up referencing themselves.

##@Stephen Friedrich's answer

I agree with the answer below that using the $injector to directly get a reference to the desired service is not ideal and could be considered an anti pattern.

Emitting an event is a much more elegant and also decoupled solution.

Solution 2 - Angularjs

The question is a duplicate of https://stackoverflow.com/questions/20647483/angularjs-injecting-service-into-a-http-interceptor-circular-dependency

I am re-posting my answer from that thread here:

A Better Fix

I think using the $injector directly is an antipattern.

A way to break the circular dependency is to use an event: Instead of injecting $state, inject $rootScope. Instead of redirecting directly, do

this.$rootScope.$emit("unauthorized");

plus

angular
	.module('foo')
	.run(function($rootScope, $state) {
		$rootScope.$on('unauthorized', () => {
			$state.transitionTo('login');
		});
	});

That way you have separated the concerns:

  1. Detect a 401 response
  2. Redirect to login

Solution 3 - Angularjs

Jonathan's solution was great until I tried to save the current state. In ui-router v0.2.10 the current state does not seem to be populated on initial page load in the interceptor.

Anyway, I solved it by using the $stateChangeError event instead. The $stateChangeError event gives you both to and from states, as well as the error. It's pretty nifty.

$rootScope.$on('$stateChangeError',
    function(event, toState, toParams, fromState, fromParams, error){
        console.log('stateChangeError');
        console.log(toState, toParams, fromState, fromParams, error);

        if(error.status == 401){
            console.log("401 detected. Redirecting...");

            authService.deniedState = toState.name;
            $state.go("login");
        }
    });

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNicolasMoiseView Question on Stackoverflow
Solution 1 - AngularjsJonathan PalumboView Answer on Stackoverflow
Solution 2 - AngularjsStephen FriedrichView Answer on Stackoverflow
Solution 3 - AngularjsJustin WrobelView Answer on Stackoverflow