Injetar $ state (ui-router) no interceptor $ http causa dependência circular

120

O que estou tentando alcançar

Gostaria de fazer a transição para um determinado estado (login), caso uma solicitação $ http retorne um erro 401. Portanto, criei um interceptor $ http.

O problema

Quando estou tentando inserir '$ state' no interceptador, recebo uma dependência circular. Por que e como corrigi-lo?

Código

//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
fonte

Respostas:

213

O conserto

Use o $injectorserviço para obter uma referência ao $stateserviço.

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);

A causa

O angular-ui-router injeta o $httpserviço como uma dependência na $TemplateFactoryqual, em seguida, cria uma referência circular $httpdentro de $httpProvidersi ao despachar o interceptador.

A mesma exceção de dependência circular seria lançada se você tentar injetar o $httpserviço diretamente em um interceptador como esse.

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

Separação de preocupações

As exceções de dependência circular podem indicar que há uma mistura de preocupações em seu aplicativo que pode causar problemas de estabilidade. Se você se deparar com essa exceção, reserve um tempo para examinar sua arquitetura para garantir que você evite quaisquer dependências que acabem se referenciando.

Resposta de @Stephen Friedrich

Eu concordo com a resposta abaixo que usando o $injector para obter diretamente uma referência ao serviço desejado não é o ideal e pode ser considerado um antipadrão.

A emissão de um evento é uma solução muito mais elegante e também dissociada.

Jonathan Palumbo
fonte
1
Como isso seria ajustado para o Angular 1.3, onde existem 4 funções diferentes transmitidas aos interceptadores?
Maciej Gurban
3
O uso seria o mesmo: você injeta o $injectorserviço no seu interceptador e liga para $injector.get()onde precisa obter o $stateserviço.
Jonathan Palumbo
Recebo $ injectot.get ("$ state") como << não definido >>, você poderia informar qual pode ser o problema?
sandeep kale
Definitivamente, é um salva-vidas quando se trata de uma situação simples, mas quando você se depara com isso, é um sinal de que você realmente criou uma dependência circular em algum lugar do código, o que é fácil de fazer no AngularJS com seu DI. Misko Hevery explica muito bem em seu blog ; recomendo que você o leia para melhorar seu código.
JD Smith #
2
$httpProvider.responseInterceptors.pushnão funcionará mais se você estiver usando versões posteriores do Angular. Use em $httpProvider.interceptors.push()vez disso. Você precisaria modificar o interceptortambém. Enfim, obrigado pela resposta maravilhosa! :)
shyam
25

A pergunta é uma duplicata do AngularJS: injetando serviço em um interceptor HTTP (dependência circular)

Estou repostando minha resposta a partir desse tópico aqui:

Uma correção melhor

Eu acho que usar o injetor $ diretamente é um antipadrão.

Uma maneira de quebrar a dependência circular é usar um evento: em vez de injetar $ state, injete $ rootScope. Em vez de redirecionar diretamente, faça

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

mais

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

Dessa forma, você separou as preocupações:

  1. Detectar uma resposta 401
  2. Redirecionar para fazer login
Stephen Friedrich
fonte
2
Na verdade, essa pergunta é uma duplicata dessa pergunta, porque essa pergunta foi feita antes dessa. Ame sua correção elegante, portanto, tenha um voto positivo. ;-)
Aaron Gray
1
Estou confuso sobre onde colocar isso. $ RootScope. $ Emit ("não autorizado");
alex
16

A solução de Jonathan foi ótima até eu tentar salvar o estado atual. No ui-router v0.2.10, o estado atual não parece ser preenchido no carregamento inicial da página no interceptador.

Enfim, resolvi usando o evento $ stateChangeError . O $ stateChangeError evento dá-lhe tanto para e de estados, bem como o erro. É bem bacana.

$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
fonte
Enviei um problema sobre isso.
Justin Wrobel
Eu acho que isso não é possível com ngResource, pois é sempre assíncrono? Eu tentei algumas coisas, mas eu não receber a chamada recurso para evitar uma mudança de estado ...
Bastian
4
E se você deseja interceptar uma solicitação que não aciona uma alteração de estado?
Shikloshi
Você pode usar a mesma abordagem que o @Stephen Friedrich fez acima, que realmente se assemelha a essa, mas usa um evento personalizado.
Saiyancoder 28/05