I've made an interceptor in my application that detects session loss (server sends an HTTP 419). In this case, I need to request a new session from the server, and then I would like to send the original request again automatically.
Maybe I could save the request in a request interceptor, and then send it again, but there might be a simpler solution.
Note that I have to use a specific webservice to create the session.
angular.module('myapp', [ 'ngResource' ]).factory(
'MyInterceptor',
function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
if(response.status == 419){
// session lost
// create new session server-side
// Session.query();
// then send current request again
// ???
}
return $q.reject(response);
});
};
}).config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('MyInterceptor');
});
Here is my solution using promises for those interested. Basically you need to request a new session, and wait for the response before sending a new request corresponding to the original request (using response.config). By returning the promise $http(response.config) you ensure that the response will be treated as if it was the original request.
(syntax may not be the best as I'm new to promises)
angular.module('myapp', [ 'ngResource' ]).factory(
'MyInterceptor',
function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
if(response.status == 419){
// session lost
var Session = $injector.get('Session');
var $http = $injector.get('$http');
// first create new session server-side
var defer = $q.defer();
var promiseSession = defer.promise;
Session.query({},function(){
defer.resolve();
}, function(){
// error
defer.reject();
});
// and chain request
var promiseUpdate = promiseSession.then(function(){
return $http(response.config);
});
return promiseUpdate;
}
return $q.reject(response);
});
};
}).config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('MyInterceptor');
});
The responseError method of httpInterceptor have to be like this:
responseError: function (response) {
// omit the retry if the request is made to a template or other url
if (response.config.apiCal === true) {
if (response.status === 419) {
var deferred = $q.defer();
// do something async: try to login.. rescue a token.. etc.
asyncFuncionToRecoverFrom419(funcion(){
// on success retry the http request
retryHttpRequest(response.config, deferred);
});
return deferred.promise;
} else {
// a template file...
return response;
}
}
}
And the magic happens here:
function retryHttpRequest(config, deferred){
function successCallback(response){
deferred.resolve(response);
}
function errorCallback(response){
deferred.reject(response);
}
var $http = $injector.get('$http');
$http(config).then(successCallback, errorCallback);
}
You're on the right path, you basically store the request in a queue and retry it after you've re-established the session.
Check out this popular module: angular http auth (https://github.com/witoldsz/angular-http-auth). In this module, they intercept 401 responses but you can model your solution off of this approach.
More or less the same solution, translated in typescript:
/// <reference path="../app.ts" />
/// <reference path="../../scripts/typings/angularjs/angular.d.ts" />
class AuthInterceptorService {
static serviceId: string = "authInterceptorService";
constructor(private $q: ng.IQService, private $location: ng.ILocationService, private $injector, private $log: ng.ILogService, private authStatusService) {}
// Attenzione. Per qualche strano motivo qui va usata la sintassi lambda perché se no ts sbrocca il this.
public request = (config: ng.IRequestConfig) => {
config.headers = config.headers || {};
var s: AuthStatus = this.authStatusService.status;
if (s.isAuth) {
config.headers.Authorization = 'Bearer ' + s.accessToken;
}
return config;
}
public responseError = (rejection: ng.IHttpPromiseCallbackArg<any>) => {
if (rejection.status === 401) {
var that = this;
this.$log.warn("[AuthInterceptorService.responseError()]: not authorized request [401]. Now I try now to refresh the token.");
var authService: AuthService = this.$injector.get("authService");
var $http: ng.IHttpService = this.$injector.get("$http");
var defer = this.$q.defer();
var promise: ng.IPromise<any> = defer.promise.then(() => $http(rejection.config));
authService
.refreshAccessToken()
.then((response) => {
that.$log.info("[AuthInterceptorService.responseError()]: token refreshed succesfully. Now I resend the original request.");
defer.resolve();
},
(err) => {
that.$log.warn("[AuthInterceptorService.responseError()]: token refresh failed. I need to logout, sorry...");
this.authStatusService.clear();
this.$location.path('/login');
});
return promise;
}
return this.$q.reject(rejection);
}
}
// Update the app variable name to be that of your module variable
app.factory(AuthInterceptorService.serviceId,
["$q", "$location", "$injector", "$log", "authStatusService", ($q, $location, $injector, $log, authStatusService) => {
return new AuthInterceptorService($q, $location, $injector, $log, authStatusService)
}]);
Hope this help.
Related
Is there a good way to reset the data in a factory/service in angular without creating a dependency to it?
I currently have an AuthService that takes a username and password, and gets an oauth token from a server. I also have an http interceptor that adds the token to all requests.
If I get a 401 (unauthorized) response then my token is no longer valid and I want to set _AuthData inside AuthService to null. But I have no good way of doing that right now.
If I add an AuthService dependancy into the interceptor (to be able to call LogOut()) then I get a circular reference since AuthService uses $http.
I keep re-reading the token from the localstorageservice inside AuthService for methods like IsAuthenticated() and Username(), but I'd like to avoid that if possible to avoid the performance hit.
Is there a way to "reset" AuthService from the AuthInterceptorService without creating a dependancy?
AuthService
appRoot.factory("AuthService", ["$http", "$q", "localStorageService", function ($http, $q, localStorageService) {
var _AuthData;
var AuthServiceFactory = {};
AuthServiceFactory.Username = function () {
return _AuthData.Username;
};
AuthServiceFactory.Roles = function () {
return _AuthData.Roles;
};
AuthServiceFactory.IsAuthenticated = function () {
return _AuthData != null;
};
AuthServiceFactory.LogOut = function () {
_AuthData = null;
localStorageService.remove("AuthData");
};
AuthServiceFactory.Login = function (Username, Password) {
var Deferred = $q.defer();
$http.post(ApiBaseUrl + "token", Username, { headers: { 'Content-Type': "application/x-www-form-urlencoded" } }).success(function (Response) {
_AuthData = {
Token: Response.access_token,
Username: Username,
Roles: Response.Roles
};
localStorageService.set("AuthData", _AuthData);
Deferred.resolve(Response);
}).error(function (err, status) {
Deferred.reject(err);
});
return Deferred.promise;
};
return AuthServiceFactory;
}]);
AuthInterceptorService
appRoot.factory("AuthInterceptorService", ["$q", "$location", "localStorageService", function ($q, $location, localStorageService) {
var AuthInterceptorServiceFactory = {};
AuthInterceptorServiceFactory.request = function (config) {
config.headers = config.headers || {};
var AuthData = localStorageService.get("AuthData");
if (AuthData) {
config.headers.Authorization = "Bearer " + AuthData.Token;
}
return config;
};
AuthInterceptorServiceFactory.responseError = function (Rejection) {
if (Rejection.status === 401) {
localStorageService.remove("AuthData");
//AuthService.LogOut(); //Need to reset token here
$location.url("/Login");
}
return $q.reject(Rejection);
};
return AuthInterceptorServiceFactory;
}]);
I can think of a few options, varyingly reasonable.
Just the first thing to consider - is the performance hit from local storage really an issue for you? Your current solution is straightforward and easy to understand, and that's a feature in itself.
Split AuthService into an Authorizer and AuthStorage. That way Authorizer can depend on $http, AuthStorage doesn't need to, and AuthInterceptorService can then depend on AuthStorage, where you can put the reset function.
This one feels like a big hammer, but AuthInterceptorService can broadcast an auth_failed event on appRoot, which AuthService can listen for to perform the reset. That's heading towards pretty global message passing, so I'd be concerned about its maintainability.
I'm writing an angularjs client for a token based restful API. The tokens in the API expire every hour so every time the token is expired in my client there should be a refresh token action.
The controller which handles the API call results looks like this:
angular.module('apitestApp')
.controller('MainCtrl', ['$rootScope', '$scope', 'httpService', function ($rootScope, $scope, httpService) {
$scope.messages = [];
var url = $rootScope.domainPath + $rootScope.apiPath + 'messages.json';
httpService.getRequest(url, {}).then(
function (data){
$scope.messages = data;
}
);
}]);
I have a service that makes the API calls using angularjs $resource
angular.module('apitestApp')
.service('httpService', ['$rootScope', '$resource', '$localStorage', function ($rootScope, $resource, $localStorage) {
this.getRequest = function (url, params){
var res = $resource(url, params, {
query: {
method: 'GET',
isArray: true,
headers: { 'Authorization': 'Bearer ' + $localStorage.token.access_token }
}
});
return res.query().$promise;
};
this.refreshToken = function (){
var url = $rootScope.domainPath + this.authPath;
var request = $resource(url);
return request.get({
client_id: this.clientId,
client_secret: this.secret,
grant_type: 'refresh_token',
refresh_token: $localStorage.token.refresh_token
},
function (data){
$localStorage.token = data;
}
).$promise;
};
}]);
And finally an interceptor that handles all unauthorized requests (401), refresh the access token and retries the failed request.
angular.module('apitestApp')
.factory('apiInterceptor', ['$q', '$injector', function ($q, $injector){
//Handling error codes
return {
response : function (response){
return response || $q.when(response);
},
responseError: function (rejection){
switch(rejection.status){
case 400:
console.log("Bad request");
break;
case 401:
var config = rejection.config;
var deferred = $q.defer();
var httpService = $injector.get('httpService');
httpService.refreshToken().then(deferred.resolve, deferred.reject);
return deferred.promise.then(function (){
return httpService.getRequest(config.url, config.params);
});
//break;
case 500:
console.log("Internal server error");
break;
default:
console.log("Another error");
break;
}
return $q.reject(rejection);
}
};
}]);
When the access token is valid, getRequest() method in my service successfully returns a promise, this is the same I want the interceptor to return but is not. In case the access token has expired the interceptor catches a 401 error, then updates the access token and finally makes the same request, the problem is that my controller doesn't get any response of it.
How can I perform a refresh token action and return the expected data on the behalf of the user? What am I doing wrong in the interceptor?
You're going to want to remove the $rootScope provider from the controller, that is not best practices for Angular as the controller has it's own scope inside of $rootScope. Services and Factories are okay to put on the $rootScope as it does not create it's own scope and that is where they will listen for their own events.
Also, it's best practice to put any asynchronous activity/HTTP calls into the services/factories. Just remember "skinny controllers, fat services".
Maybe try using an async handler that uses a sort of publish/subscribe design. Now, if it fails, it will call to update the stored value of messages once the getRequest function has completed async, triggering an update to the scope digest for any controller subscribed to the method:
Controller
angular.module('apitestApp')
.controller('MainCtrl', ['$scope', 'httpService', function ($scope, httpService) {
$scope.messages = [];
httpService.setPath();
httpService.onMessageReady($scope, function (messagesData) {
$scope.messages = messagesData;
});
}]);
Service
angular.module('apitestApp')
.service('httpService', ['$rootScope', '$resource', '$localStorage', function ($rootScope, $resource, $localStorage) {
var self = this;
this.messages = undefined;
this.setPath = function () {
self.getRequest($rootScope.domainPath + $rootScope.apiPath + 'messages.json', {});
};
this.getRequest = function (url, params) {
var res = $resource(url, params, {
query: {
method: 'GET',
isArray: true,
headers: { 'Authorization': 'Bearer ' + $localStorage.token.access_token }
}
});
return res.query().$promise.then(function (data) {
if (data) {
self.messages = data;
$rootScope.$broadcast('messagesReady');
}
});
};
this.refreshToken = function (){
var url = $rootScope.domainPath + this.authPath;
var request = $resource(url);
return request.get({
client_id: this.clientId,
client_secret: this.secret,
grant_type: 'refresh_token',
refresh_token: $localStorage.token.refresh_token
},
function (data){
$localStorage.token = data;
}
).$promise;
};
this.onMessageReady = function (scope, callback) {
callback(this.messages);
scope.$on('messagesReady', function () {
callback(this.messages);
});
};
}]);
In my application, I have a service called 'pendingRequests' that keeps track of my http requests. I configured my $httpProvider to use this service.
The purpose of this service is to give me the ability to cancel ALL pending http requests occurring in ANY controller.
Here is the code:
app.service('pendingRequests', function($rootScope, $q) {
var pending = [];
this.get = function() {
return pending;
};
this.add = function(request) {
pending.push(request);
//console.log("Pending Requests(before):" + pending);
};
this.remove = function(request) {
angular.forEach(pending, function(p , key) {
if(p.url == request.url) pending.splice(key, 1);
});
// console.log("Pending Requests(after):" + pending);
};
this.cancelAll = function() {
if(typeof pending !='undefined'){
angular.forEach(pending, function(p) {
p.canceller.resolve();
});
pending.length = 0;
}
};
});
app.config(function($httpProvider){
$httpProvider.interceptors.push(function($q, pendingRequests){
return {
'request': function (config){
var canceller = $q.defer();
pendingRequests.add({
url: config.url,
canceller: canceller
});
config.timeout = canceller.promise;
return config || $q.when(config);
},
'response': function (response){
pendingRequests.remove(response.config);
//pendingRequests remove request
return response;
}
}
});
});
The service is canceling the requests as intended. However, the next request submitted is delayed as if it is still waiting for another request to complete.
What is causing this delay?
ExpressJS is sending the following response...
res.send('ItemUploaded');
I'm trying to get AngularJS to see this response via an Interceptor and perform a redirect. Does anyone have sample code where Angular catches a server response (such as my "ItemUploaded") and performs a redirect to a partial (via $location)?
This works fine. I have used it in my application.
var interceptor = function ($q, $location) {
return {
request: function (config) {//req
console.log(config);
return config;
},
response: function (result) {//res
console.log('Repos:');
console.log(result.status);
return result;
},
responseError: function (rejection) {//error
console.log('Failed with', rejection.status, 'status');
if (rejection.status == 403) {
$location.url('/dashboard');
}
return $q.reject(rejection);
}
}
};
module.config(function ($httpProvider) {
$httpProvider.interceptors.push(interceptor);
});
Here is the factory for the interceptor:
.factory('InterceptorService',['$q', '$location', function( $q, $location, $http){
var InterceptorServiceFactory = {};
var _request = function(config){
//success logic here
return config;
}
var _responseError = function(rejection) {
//error here. for example server respond with 401
return $q.reject(rejection);
}
InterceptorServiceFactory.request = _request;
InterceptorServiceFactory.responseError = _responseError;
return InterceptorServiceFactory;
}]);
then register the interceptor:
.config(["$httpProvider", function ($httpProvider) {
$httpProvider.interceptors.push('InterceptorService');
}]);
Every request coming will be passed here.
You can implement a interceptor factory which will redirect if it gets a matching result.
angular
.module('app')
.factory("httpinterceptor", ["$location",
function(location) {
return {
'response': function(response) {
if (response.data === "ItemUploaded") {
location.path("/ItemUploaded")
}
}
}
}
]);
Given a Ajax request in AngularJS
$http.get("/backend/").success(callback);
what is the most effective way to cancel that request if another request is launched (same backend, different parameters for instance).
This feature was added to the 1.1.5 release via a timeout parameter:
var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve(); // Aborts the $http request if it isn't finished.
Cancelling Angular $http Ajax with the timeout property doesn't work in Angular 1.3.15.
For those that cannot wait for this to be fixed I'm sharing a jQuery Ajax solution wrapped in Angular.
The solution involves two services:
HttpService (a wrapper around the jQuery Ajax function);
PendingRequestsService (tracks the pending/open Ajax requests)
Here goes the PendingRequestsService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('PendingRequestsService', ["$log", function ($log) {
var $this = this;
var pending = [];
$this.add = function (request) {
pending.push(request);
};
$this.remove = function (request) {
pending = _.filter(pending, function (p) {
return p.url !== request;
});
};
$this.cancelAll = function () {
angular.forEach(pending, function (p) {
p.xhr.abort();
p.deferred.reject();
});
pending.length = 0;
};
}]);})(window.angular);
The HttpService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('HttpService', ['$http', '$q', "$log", 'PendingRequestsService', function ($http, $q, $log, pendingRequests) {
this.post = function (url, params) {
var deferred = $q.defer();
var xhr = $.ASI.callMethod({
url: url,
data: params,
error: function() {
$log.log("ajax error");
}
});
pendingRequests.add({
url: url,
xhr: xhr,
deferred: deferred
});
xhr.done(function (data, textStatus, jqXhr) {
deferred.resolve(data);
})
.fail(function (jqXhr, textStatus, errorThrown) {
deferred.reject(errorThrown);
}).always(function (dataOrjqXhr, textStatus, jqXhrErrorThrown) {
//Once a request has failed or succeeded, remove it from the pending list
pendingRequests.remove(url);
});
return deferred.promise;
}
}]);
})(window.angular);
Later in your service when you are loading data you would use the HttpService instead of $http:
(function (angular) {
angular.module('app').service('dataService', ["HttpService", function (httpService) {
this.getResources = function (params) {
return httpService.post('/serverMethod', { param: params });
};
}]);
})(window.angular);
Later in your code you would like to load the data:
(function (angular) {
var app = angular.module('app');
app.controller('YourController', ["DataService", "PendingRequestsService", function (httpService, pendingRequestsService) {
dataService
.getResources(params)
.then(function (data) {
// do stuff
});
...
// later that day cancel requests
pendingRequestsService.cancelAll();
}]);
})(window.angular);
Cancelation of requests issued with $http is not supported with the current version of AngularJS. There is a pull request opened to add this capability but this PR wasn't reviewed yet so it is not clear if its going to make it into AngularJS core.
If you want to cancel pending requests on stateChangeStart with ui-router, you can use something like this:
// in service
var deferred = $q.defer();
var scope = this;
$http.get(URL, {timeout : deferred.promise, cancel : deferred}).success(function(data){
//do something
deferred.resolve(dataUsage);
}).error(function(){
deferred.reject();
});
return deferred.promise;
// in UIrouter config
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//To cancel pending request when change state
angular.forEach($http.pendingRequests, function(request) {
if (request.cancel && request.timeout) {
request.cancel.resolve();
}
});
});
For some reason config.timeout doesn't work for me. I used this approach:
let cancelRequest = $q.defer();
let cancelPromise = cancelRequest.promise;
let httpPromise = $http.get(...);
$q.race({ cancelPromise, httpPromise })
.then(function (result) {
...
});
And cancelRequest.resolve() to cancel. Actually it doesn't not cancel a request but you don't get unnecessary response at least.
Hope this helps.
This enhances the accepted answer by decorating the $http service with an abort method as follows ...
'use strict';
angular.module('admin')
.config(["$provide", function ($provide) {
$provide.decorator('$http', ["$delegate", "$q", function ($delegate, $q) {
var getFn = $delegate.get;
var cancelerMap = {};
function getCancelerKey(method, url) {
var formattedMethod = method.toLowerCase();
var formattedUrl = encodeURI(url).toLowerCase().split("?")[0];
return formattedMethod + "~" + formattedUrl;
}
$delegate.get = function () {
var cancelerKey, canceler, method;
var args = [].slice.call(arguments);
var url = args[0];
var config = args[1] || {};
if (config.timeout == null) {
method = "GET";
cancelerKey = getCancelerKey(method, url);
canceler = $q.defer();
cancelerMap[cancelerKey] = canceler;
config.timeout = canceler.promise;
args[1] = config;
}
return getFn.apply(null, args);
};
$delegate.abort = function (request) {
console.log("aborting");
var cancelerKey, canceler;
cancelerKey = getCancelerKey(request.method, request.url);
canceler = cancelerMap[cancelerKey];
if (canceler != null) {
console.log("aborting", cancelerKey);
if (request.timeout != null && typeof request.timeout !== "number") {
canceler.resolve();
delete cancelerMap[cancelerKey];
}
}
};
return $delegate;
}]);
}]);
WHAT IS THIS CODE DOING?
To cancel a request a "promise" timeout must be set.
If no timeout is set on the HTTP request then the code adds a "promise" timeout.
(If a timeout is set already then nothing is changed).
However, to resolve the promise we need a handle on the "deferred".
We thus use a map so we can retrieve the "deferred" later.
When we call the abort method, the "deferred" is retrieved from the map and then we call the resolve method to cancel the http request.
Hope this helps someone.
LIMITATIONS
Currently this only works for $http.get but you can add code for $http.post and so on
HOW TO USE ...
You can then use it, for example, on state change, as follows ...
rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
angular.forEach($http.pendingRequests, function (request) {
$http.abort(request);
});
});
here is a version that handles multiple requests, also checks for cancelled status in callback to suppress errors in error block. (in Typescript)
controller level:
requests = new Map<string, ng.IDeferred<{}>>();
in my http get:
getSomething(): void {
let url = '/api/someaction';
this.cancel(url); // cancel if this url is in progress
var req = this.$q.defer();
this.requests.set(url, req);
let config: ng.IRequestShortcutConfig = {
params: { id: someId}
, timeout: req.promise // <--- promise to trigger cancellation
};
this.$http.post(url, this.getPayload(), config).then(
promiseValue => this.updateEditor(promiseValue.data as IEditor),
reason => {
// if legitimate exception, show error in UI
if (!this.isCancelled(req)) {
this.showError(url, reason)
}
},
).finally(() => { });
}
helper methods
cancel(url: string) {
this.requests.forEach((req,key) => {
if (key == url)
req.resolve('cancelled');
});
this.requests.delete(url);
}
isCancelled(req: ng.IDeferred<{}>) {
var p = req.promise as any; // as any because typings are missing $$state
return p.$$state && p.$$state.value == 'cancelled';
}
now looking at the network tab, i see that it works beatuifully. i called the method 4 times and only the last one went through.
You can add a custom function to the $http service using a "decorator" that would add the abort() function to your promises.
Here's some working code:
app.config(function($provide) {
$provide.decorator('$http', function $logDecorator($delegate, $q) {
$delegate.with_abort = function(options) {
let abort_defer = $q.defer();
let new_options = angular.copy(options);
new_options.timeout = abort_defer.promise;
let do_throw_error = false;
let http_promise = $delegate(new_options).then(
response => response,
error => {
if(do_throw_error) return $q.reject(error);
return $q(() => null); // prevent promise chain propagation
});
let real_then = http_promise.then;
let then_function = function () {
return mod_promise(real_then.apply(this, arguments));
};
function mod_promise(promise) {
promise.then = then_function;
promise.abort = (do_throw_error_param = false) => {
do_throw_error = do_throw_error_param;
abort_defer.resolve();
};
return promise;
}
return mod_promise(http_promise);
}
return $delegate;
});
});
This code uses angularjs's decorator functionality to add a with_abort() function to the $http service.
with_abort() uses $http timeout option that allows you to abort an http request.
The returned promise is modified to include an abort() function. It also has code to make sure that the abort() works even if you chain promises.
Here is an example of how you would use it:
// your original code
$http({ method: 'GET', url: '/names' }).then(names => {
do_something(names));
});
// new code with ability to abort
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
});
promise.abort(); // if you want to abort
By default when you call abort() the request gets canceled and none of the promise handlers run.
If you want your error handlers to be called pass true to abort(true).
In your error handler you can check if the "error" was due to an "abort" by checking the xhrStatus property. Here's an example:
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
},
function(error) {
if (er.xhrStatus === "abort") return;
});