Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

angularjs http interceptor class (ES6) loses binding to 'this'

I am building and AngularJS app using ES6 classes with traceur transpiling to ES5 in AMD format.

in my module I import the interceptor class and register it as a service, and then register this service with the $httpProvider.interceptors in module.config:

var commonModule = angular.module(moduleName, [constants.name]);

import authenticationInterceptor from './authentication/authentication.interceptor';

commonModule.service('authenticationInterceptor', authenticationInterceptor);

commonModule.config( $httpProvider =>  {
    $httpProvider.interceptors.push('authenticationInterceptor');
});

My interceptor class injects both $q and the $window services, saves them in the constructor for later use. I followed this part with the debugger and the injection is happening properly:

'use strict';
/*jshint esnext: true */

var authenticationInterceptor = class AuthenticationInterceptor {

    /* ngInject */
    constructor($q, $window) {
        this.$q = $q;
        this.$window = $window;
    }

    responseError(rejection) {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}

authenticationInterceptor.$inject = ['$q', '$window'];

export default authenticationInterceptor;

When I make a request that responds with a 401 the interceptor triggers appropriately, but in the 'responseError' method the 'this' variable points to the window object and not to my interceptor, hence I do not have access to this.$q or this.$window.

I cannot figure out why? Any ideas?

like image 895
Kendrick Burson Avatar asked Feb 20 '15 21:02

Kendrick Burson


2 Answers

The context (this) is lost because the Angular framework only keeps references to the handler functions themselves, and invokes them directly without any context, as alexpods has pointed out.

I recently wrote a blog post about writing $http interceptors using TypeScript, which also applies to ES6 classes: AngularJS 1.x Interceptors Using TypeScript.

To summarise what I have discussed in this post, in order to not lose this in your handlers, you'll have to define the methods as arrow functions, effectively putting the functions directly inside of the class's constructor function in the compiled ES5 code.

class AuthenticationInterceptor {

    /* ngInject */
    constructor($q, $window) {
        this.$q = $q;
        this.$window = $window;
    }

    responseError = (rejection) => {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}

If you really insist on having your interceptor written as a fully prototype-based class, you could define a base class for your interceptor and extend it. The base class would replace the prototype interceptor functions with instance methods, so we can write our interceptors like this:

class HttpInterceptor {
  constructor() {
    ['request', 'requestError', 'response', 'responseError']
        .forEach((method) => {
          if(this[method]) {
            this[method] = this[method].bind(this);
          }
        });
  }
}

class AuthenticationInterceptor extends HttpInterceptor {

    /* ngInject */
    constructor($q, $window) {
        super();
        this.$q = $q;
        this.$window = $window;
    }

    responseError(rejection) {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}
like image 98
Merott Avatar answered Nov 19 '22 04:11

Merott


Look at these lines of source code:

// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
    if (interceptor.request || interceptor.requestError) {
        chain.unshift(interceptor.request, interceptor.requestError);
    }
    if (interceptor.response || interceptor.responseError) {
        chain.push(interceptor.response, interceptor.responseError);
    }
});

When interceptor.responseError method is pushed into chain it looses its context (just function is pushed, without any context);

Later here it will be added to promise as reject callback:

while (chain.length) {
    var thenFn = chain.shift();
    var rejectFn = chain.shift();

    promise = promise.then(thenFn, rejectFn);
}

So if promise will be rejected, rejectFn(your responseError function) will be executed as an ordinary function. In this case this references to window if script is being executed in non-strict mode, or equals null otherwise.

IMHO Angular 1 was written with ES5 consideration, so I think using it with ES6 is not a good idea.

like image 4
alexpods Avatar answered Nov 19 '22 05:11

alexpods