16

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?

Kendrick Burson
  • 531
  • 5
  • 9

9 Answers9

27

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);
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Merott
  • 7,189
  • 6
  • 40
  • 52
4

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.

alexpods
  • 47,475
  • 10
  • 100
  • 94
  • 1
    Thank you for that insightful response. I never much liked digging into the angular source code, it is very dense. As for ES6, it is a superset of ES5, thus I can mix the styles where needed for now, which is what I did. I changed the interceptor class into a function that returns an object, everything else was the same and it worked. I am pushing for ES6 to get ahead of the curve, but sometimes when your in front you get bugs in your teeth :-) – Kendrick Burson Feb 21 '15 at 09:28
4

This is exactly the same problem I'm experiencing, however, I found a workaround by setting the 'this' in a self variable just like solving the scoping issue on es5, and it works fine:

let self;

class AuthInterceptor{

   constructor(session){
       self = this;
       this.session = session;
   }

   request(config){
       if(self.session) {
           config.headers = self.session.getSessionParams().headers; 
       }
       return config;
   }

   responseError(rejection){
       if(rejection.status == 401){

       }

       return rejection;
   }

}

export default AuthInterceptor;
Avie
  • 66
  • 2
  • 1
    My one problem with this solution is that your 'self' variable is in global scope. I suppose if you wrap in an amd define it gets constrained in the closure, but it still feels like a global variable. – Kendrick Burson Feb 27 '15 at 17:44
  • I tried defining it inside the class, but it doesn't work, I dont know why it is forbidden to define local variables in es6 class , anyway the traceur compiler wraps it inside the System.registerModule definition – Avie Mar 02 '15 at 10:22
4

To add to the conversation, you could return an object from the constructor that contains explicitly bound class methods.

export default class HttpInterceptor {

   constructor($q, $injector) {
       this.$q = $q;
       this.$injector = $injector;

       return {
           request: this.request.bind(this),
           requestError: this.requestError.bind(this),
           response: this.response.bind(this),
           responseError: this.responseError.bind(this)
       }
   }

   request(req) {
       this.otherMethod();
       // ...
   }

   requestError(err) {
       // ...
   }

   response(res) {
       // ...
   }

   responseError(err) {
       // ...
   }

   otherMethod() {
       // ...
   }

}
Jbird
  • 2,839
  • 1
  • 21
  • 28
  • I found this to be the easiest/most eloquent solution. I threw the factory object creation into a create() method instead of the constructor. Your typo I blindly copied and pasted really threw me for a loop, though! `response: this.responseError.bind(this)` – hackel Dec 13 '16 at 21:13
1

Note that using arrow functions in class properties is an experimental feature for ES7. However most transpilers don't have a problem with it.

If you want to stick to the official ES6 implementation you can create instance methods instead of prototype methods by defining your methods in the constructor.

class AuthenticationInterceptor {
  /* ngInject */
  constructor($q, $window) {
    
    this.responseError = (rejection) => {
      const authToken = rejection.config.headers.Authorization;
      if (rejection.status === 401 && !authToken) {
        const authentication_url = rejection.data.errors[0].data.authenticationUrl;
        $window.location.replace(authentication_url);
        return $q.defer(rejection);
      }
      return $q.reject(rejection);
    };
    
  }
}

I like this solution because it decreases the amount of boilerplate code;

  • You no longer have to put all your dependencies in this. So instead of using this.$q you can just use $q.
  • No need to return explicitly bound class methods from the constructor

Having one extra level of indentation is a downside. Furthermore this method might not be suitable for classes that are instantiated a lot as it consumes more memory in that case. E.g.; Using direct class properties (transpiled to prototype methods) is more efficient for controllers of components that are likely to be used multiple times on one page. Don't worry about services, providers and factories as these are all singletons and they will only be instantiated once.

Erik Verheij
  • 193
  • 1
  • 7
0

Working solution with arrow functions:

var AuthInterceptor = ($q, $injector, $log) => {
    'ngInject';

    var requestErrorCallback = request => {
        if (request.status === 500) {
          $log.debug('Something went wrong.');
        }
        return $q.reject(request);
    };

    var requestCallback = config => {
        const token = localStorage.getItem('jwt');

        if (token) {
            config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
    };

    var responseErrorCallback = response => {
         // handle the case where the user is not authenticated
        if (response.status === 401 || response.status === 403) {
            // $rootScope.$broadcast('unauthenticated', response);
            $injector.get('$state').go('login');
       }
       return $q.reject(response);
    }

  return {
    'request':       requestCallback,
    'response':      config => config,
    'requestError':  requestErrorCallback,
    'responseError': responseErrorCallback,
  };
};

/***/
var config = function($httpProvider) {
    $httpProvider.interceptors.push('authInterceptor');
};

/***/    
export
default angular.module('services.auth', [])
    .service('authInterceptor', AuthInterceptor)
    .config(config)
    .name;
  • OP wanted to do it as a Class. your solution does it as a function or the classic way it's done in Angular 1, which isn't bad either. But it doesn't answer the question. – Andrei R Nov 17 '15 at 04:56
0

To compelement the other fine answers regarding arrow functions, I think it's a bit cleaner using a static factory method in the Interceptor:

export default class AuthenticationInterceptor {
 static $inject = ['$q', '$injector', '$rootRouter'];
 constructor ($q, $injector, $rootRouter) {
  this.$q = $q;
  this.$injector = $injector;
  this.$rootRouter = $rootRouter;
 }

 static create($q, $injector, $rootRouter) {
  return new AuthenticationInterceptor($q, $injector, $rootRouter);
 }

 responseError = (rejection) => {
  const HANDLE_CODES = [401, 403];

  if (HANDLE_CODES.includes(rejection.status)) {
   // lazy inject in order to avoid circular dependency for $http
   this.$injector.get('authenticationService').clearPrincipal();
   this.$rootRouter.navigate(['Login']);
  }
  return this.$q.reject(rejection);
 }
}

Usage:

.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {
$provide.factory('reauthenticationInterceptor', AuthenticationInterceptor.create);
$httpProvider.interceptors.push('reauthenticationInterceptor');
}]);
Stefan Norberg
  • 1,137
  • 13
  • 28
0

My working solution without using ngInject

myInterceptor.js

export default ($q) => {
let response = (res) => {
    return res || $q.when(res);
}

let responseError = (rejection) => {
    //do your stuff HERE!!
    return $q.reject(rejection);
}

return {
    response: response,
    responseError: responseError
}

}

myAngularApp.js

// angular services
import myInterceptor from 'myInterceptor';

// declare app
const application = angular.module('myApp', [])
        .factory('$myInterceptor', myInterceptor)
        .config(['$httpProvider', function($httpProvider) {  
           $httpProvider.interceptors.push('$myInterceptor');
        }]);
Rodrigo Asensio
  • 2,760
  • 4
  • 26
  • 26
-1
export default class AuthInterceptor{


    /*@ngInject;*/
    constructor(SomeService,$q){

        this.$q=$q;
        this.someSrv = SomeService;



        this.request = (config) =>{
            ...
            this.someSrv.doit();
            return config;

        }

        this.response = (response)=>{
            ...
            this.someSrv.doit();
            return response;
        }

        this.responseError = (response) => {
           ...
           return this.$q.reject(response);
        }



    }



}
Nikolay Mihaylov
  • 3,868
  • 8
  • 27
  • 32
grondon
  • 161
  • 1
  • 7