120

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);
NicolasMoise
  • 7,261
  • 10
  • 44
  • 65

3 Answers3

213

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.

Jonathan Palumbo
  • 6,851
  • 1
  • 29
  • 40
  • 1
    How would this be adjusted for Angular 1.3, where there are 4 different functions passed into interceptors? – Maciej Gurban Dec 15 '14 at 08:46
  • 3
    The usage would be the same, you inject the `$injector` service into your interceptor and call `$injector.get()` where you need to get the `$state` service. – Jonathan Palumbo Dec 15 '14 at 19:58
  • I get $injectot.get("$state") as << not defined >>, could you please tell what may be the issue ? – sandeep kale Mar 23 '15 at 10:34
  • This definitely is a lifesaver when it's a simple situation, but when you run into this it's a sign that you have indeed created a circular dependency somewhere in your code, which is easy to do in AngularJS with its DI. Misko Hevery explains it very well [in his blog](http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/); highly recommend you read it to improve your code. – JD Smith Nov 05 '15 at 20:25
  • 2
    `$httpProvider.responseInterceptors.push` don't work anymore if you are using later versions of Angular. Use `$httpProvider.interceptors.push()` instead. You'd have to modify the `interceptor` as well. Anyway, thanks for the wonderful answer! :) – shyam Dec 30 '15 at 06:38
  • Thanks! We just ran into this, was like whaaaa.. lol users could by-pass our login and go to our dashboard. Luckly no user data would be loaded. – Leon Gaban Jan 18 '17 at 20:56
25

The question is a duplicate of 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
Community
  • 1
  • 1
eekboom
  • 5,551
  • 1
  • 30
  • 39
  • 2
    Actually, that question is a duplicate of this question, because this question was asked before that one was. Love your elegant fix though, so have an upvote. ;-) – Aaron Gray Oct 28 '16 at 00:06
  • 1
    I'm confusing about where to put this.$rootScope.$emit("unauthorized"); – alex Mar 01 '17 at 17:12
16

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");
        }
    });
Justin Wrobel
  • 1,981
  • 2
  • 23
  • 36
  • I've submitted an [issue](https://github.com/angular-ui/ui-router/issues/1060) about this. – Justin Wrobel May 02 '14 at 14:09
  • I think this is not possible with ngResource since it is always asynchron? I tried some stuff but I don't get the resource call to prevent a state change... – Bastian Jul 03 '14 at 15:57
  • 4
    What if you want to intercept a request that does not trigger a state change? – Shikloshi Oct 28 '15 at 10:52
  • You can use the same approach as @Stephen Friedrich did above, which actually resembles this one but uses a custom event. – saiyancoder May 28 '17 at 18:10