La inyección de $ state (ui-router) en el interceptor $ http provoca una dependencia circular

120

Lo que estoy tratando de lograr

Me gustaría hacer la transición a un determinado estado (inicio de sesión) en caso de que una solicitud $ http devuelva un error 401. Por lo tanto, he creado un interceptor $ http.

El problema

Cuando intento insertar '$ state' en el interceptor, obtengo una dependencia circular. ¿Por qué y cómo lo soluciono?

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
fuente

Respuestas:

213

La solución

Utilice el $injectorservicio para obtener una referencia al $stateservicio.

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

La causa

angular-ui-router inyecta el $httpservicio como una dependencia en la $TemplateFactoryque luego crea una referencia circular $httpdentro de $httpProvidersí mismo al enviar el interceptor.

Se lanzaría la misma excepción de dependencia circular si intenta inyectar el $httpservicio directamente en un interceptor como ese.

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

Separación de intereses

Las excepciones de dependencia circular pueden indicar que hay una combinación de preocupaciones dentro de su aplicación que podrían causar problemas de estabilidad. Si se encuentra con esta excepción, debe tomarse el tiempo para observar su arquitectura para asegurarse de evitar cualquier dependencia que termine haciendo referencia a sí misma.

La respuesta de @Stephen Friedrich

Estoy de acuerdo con la respuesta a continuación de que usar el $injectorpara obtener directamente una referencia al servicio deseado no es ideal y podría considerarse un patrón anti.

Emitir un evento es una solución mucho más elegante y además desacoplada.

Jonathan Palumbo
fuente
1
¿Cómo se ajustaría esto para Angular 1.3, donde se pasan 4 funciones diferentes a los interceptores?
Maciej Gurban
3
El uso sería el mismo, usted inyecta el $injectorservicio en su interceptor y llama a $injector.get()donde necesita obtener el $stateservicio.
Jonathan Palumbo
Recibo $ injectot.get ("$ state") como << no definido >>, ¿podría decirnos cuál puede ser el problema?
col rizada sandeep
Esto definitivamente es un salvavidas cuando se trata de una situación simple, pero cuando te encuentras con esto es una señal de que has creado una dependencia circular en algún lugar de tu código, lo cual es fácil de hacer en AngularJS con su DI. Misko Hevery lo explica muy bien en su blog ; Le recomiendo encarecidamente que lo lea para mejorar su código.
JD Smith
2
$httpProvider.responseInterceptors.pushya no funciona si está utilizando versiones posteriores de Angular. Úselo en su $httpProvider.interceptors.push()lugar. También tendrías que modificar el interceptor. De todos modos, ¡gracias por la maravillosa respuesta! :)
shyam
25

La pregunta es un duplicado de AngularJS: inyectar servicio en un interceptor HTTP (dependencia circular)

Estoy volviendo a publicar mi respuesta de ese hilo aquí:

Una mejor solución

Creo que usar el inyector $ directamente es un antipatrón.

Una forma de romper la dependencia circular es usar un evento: en lugar de inyectar $ state, inyecte $ rootScope. En lugar de redirigir directamente, haz

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

más

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

De esa forma ha separado las preocupaciones:

  1. Detecta una respuesta 401
  2. Redirigir para iniciar sesión
Stephen Friedrich
fuente
2
En realidad, esa pregunta es un duplicado de esta pregunta, porque esta pregunta se hizo antes que la otra. Sin embargo, me encanta su solución elegante, así que tenga un voto positivo. ;-)
Aaron Gray
1
Estoy confundido acerca de dónde poner esto. $ RootScope. $ Emit ("no autorizado");
alex
16

La solución de Jonathan fue excelente hasta que intenté guardar el estado actual. En ui-router v0.2.10, el estado actual no parece estar poblado en la carga de página inicial en el interceptor.

De todos modos, lo resolví usando el evento $ stateChangeError en su lugar. El evento $ stateChangeError le da tanto hacia como desde estados, así como el error. Es bastante ingenioso.

$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
fuente
Envié un problema sobre esto.
Justin Wrobel
Creo que esto no es posible con ngResource ya que siempre es asincrónico. Probé algunas cosas pero no recibo la llamada de recurso para evitar un cambio de estado ...
Bastian
4
¿Qué sucede si desea interceptar una solicitud que no desencadena un cambio de estado?
Shikloshi
Puede usar el mismo enfoque que @Stephen Friedrich hizo anteriormente, que en realidad se parece a este, pero usa un evento personalizado.
saiyancoder