AngularJS: how to stay in 'run' method until it finishes - javascript

I have a simple scenario - I wish to init my http calls with interceptor that will add a value in headers (a token of some kind).
The problem is that the token is received via http as well (it should be the first call) but I don't know how to make all other calls to wait for it to finish before issuing their own calls...
.factory('sessionData', function () {
var currentToken = '[uninitialized-token]';
return {
getToken: function () {
return currentToken;
},
setAuthData: function (token) {
currentToken = token;
}
}
})
.factory('sessionInjector', ['sessionData', function (sessionData) {
var sessionInjector = {
request: function (config) {
console.log("sending with token: " + sessionData.getToken());
config.headers['x-header-sessionID'] = sessionData.getToken();
}
};
return sessionInjector;
}])
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('sessionInjector');
}])
.run(['$http', 'configs', 'sessionData', function ($http, configs, sessionData) {
$http.get(configs.authApiUrl + 'login').then(function (ret) {
sessionData.setAuthData(ret);
console.log("successfully authenticated with token " + sessionData.getToken());
});
}])
.controller('TestCtrl', function($http){
$scope.p1 = 'Uninitialized';
$http.get('http://localhost/api/getData').then(function(ret){
$scope.p1 = ret;
});
});
The problem is that the TestCtrl issues an http call before the run method finished getting the token (resulting in header value having the [uninitialized-token] in it's value).
How to make the controllers wait for the 'run' async methods to finish?

$http interceptors can be used to return promises in their callbacks. You can use this to intercept each call and delay it until the promise is resolved.
You should understand how promises work for this.
Example:
myModule.factory('tokenPromise', function($http) {
return $http.get({url: 'myurl/token', bypassToken: true}).then(function(data) {
// This is when your token webservice return, deal with the response here
return data.token;
});
});
myModule.factory('myHttpInterceptor', function($q, tokenPromise) {
return {
'request': function(config) {
if (config.bypassToken) return config;
// This ensures the token promise is resolved before proceeding with the request.
return tokenPromise.then(function(token) {
config.headers['x-header-sessionID'] = token;
return config;
});
},
};
});
myModule.config(function($httpProvider) {
//wire the interceptor here
$httpProvider.interceptors.push('myHttpInterceptor');
})
reference: http service on angular official docs

Related

How to cancel request [duplicate]

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

Authenticate before calling REST endpoint

I'm trying to get an authentication token from my REST API before calling any other endpoint (preferably once).
For this I created a token factory that calls the login and receives a token back. I then expect to inject that token factory into my other controllers. I was hoping that the dependencies where being respected but my controller calls the service before obtaining the token from the token factory. what did I do wrong ?
factory:
app.factory('tokenFactory', function($http, appConfig) {
console.log('calling endpoint: ' + appConfig.REST_ENDPOINT + 'authentication/login');
var apiToken;
$http.post(appConfig.REST_ENDPOINT + 'authentication/login', {
"username": "john",
"password": "open$esame"
}).
success(function(data) {
apiToken = data.token;
}).
error(function(data) {
//
});
return {
apiToken: apiToken
};
});
controller:
app.controller('clientListCtrl', function($scope, $http, appConfig, tokenFactory) {
console.log('calling endpoint: ' + appConfig.REST_ENDPOINT+'/client/list');
$http.get(appConfig.REST_ENDPOINT+'/client/list', {
header: { 'Authorization': tokenFactory.apiToken }
})
.success(function(data) {
$scope.clients = data;
}).
error(function(data, status) {
//
});
});
Yes, you have to take into account the asynchronous aspect of Ajax and leverage promise chaining (the $http.post actually returns a promise that you need to return). The factory will use the method getToken can define a success method to be notified when the result is received.
app.factory('tokenFactory', function($http, appConfig) {
console.log('calling endpoint: ' + appConfig.REST_ENDPOINT + 'authentication/login');
return {
getToken: function() {
return $http.post(appConfig.REST_ENDPOINT + 'authentication/login', {
"username": "john",
"password": "open$esame"
}).
success(function(data) {
return data.token;
}).
error(function(data) {
//
});
}
};
});
That said, I think that you should leverage the HTTP interceptor feature of Angular. This allows to transparently set the security token within your request. The first time the token is gotten using AJAX and then you can reuse this one.
app.factory('securityTokenInterceptor', function($q, tokenFactory) {
var currentToken = null;
return {
request: function(config) {
if (currentToken != null) {
config.headers['Authorization'] = currentToken;
return config;
}
var deferred = $q.defer();
tokenFactory.getToken().then(function(token) {
config.headers['Authorization'] = token.token;
currentToken = token.token;
deferred.resolve(config);
}, function(err) {
// Handle error (reject promise, ...)
});
return deferred.promise;
}
};
})
Here is the way to register your interceptor on $httpProvider:
app.config(function($httpProvider) {
$httpProvider.interceptors.push('securityTokenInterceptor');
})
Here is the fake factory I use to get token:
app.factory('tokenFactory', function($q, $timeout) {
return {
getToken: function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve({token:'mytoken'});
}, 500);
return deferred.promise;
}
};
})
Hope it helps you,
Thierry
In your factory:
$http.post() is asynchronous, so the return after it will not contain the data coming from the post request. I would suggest returning the promise object you get from calling $http.post().
In your controller: you can use the returned promise and define the success method, in which you can do the get request.
tokenFactory.success(function (tokenData) {
token = tokenData.token;
$http.get(endpoint, { header: { 'auth': token } })
.success(...)
.error(...);
});
Not sure if it is the best way how to do it, but I think it could work this way.

Angularjs $http promise; Get the callback functions fn1 and fn2 from $http().then(fn1, fn2)

I am doing a custom Oauth2 authentication module. I have a requestExecutor factory:
$http({
url: requestObject.url,
method: requestObject.method,
cache: requestObject.cache,
headers: requestObject.headers,
data: requestObject.data})
.then(function (resData) {
if (requestObject.callback) {
requestObject.callback(resData.data);
}
}, function () {
if (requestObject && requestObject.customErrorCallback) {
requestObject.customErrorCallback();
}
});
and Http Interceptor:
'responseError': function (rejection) {
console.log(rejection)
switch (rejection.status) {
case 401 :
{
$rootScope.$emit(CONST.UNAUTHORIZED);
break;
}
case 403 :
{
$rootScope.$emit(CONST.FORBIDDEN, rejection.config);
break;
}
}
return $q.reject(rejection);
}
So when I execute a request and get 403 response error
from the server I want to send the full request to the listener of CONST.FORBIDDEN (the only thing that is missing is the callbacks from the $http().then() promise). The reason is that I want to execute the failed request again after I finish with the refreshing the access token.
My questions are:
Where are stored $http().then() promises?
Can to get $http().then() promises?
How can I implement 2.?
No sure about your intention, there would probably be better solutions, but you can register the callback handlers in the config object and access them in the interceptor. plnkr
But here is a better design. angular-app/securityInterceptor
You will do the refreshing in the interceptor and execute the request again. If the second request was successful you return the result, which will be processed by your success/error-handlers.
$scope.fetchAsync = function(forceError) {
function successHandler(result) {
$scope.content = result.data;
}
function errorHandler(result) {
$scope.content = result;
}
$scope.content = 'Loading...';
$http.get(forceError ? 'not-found' : 'test.txt', {
handler: {
success: successHandler,
error: errorHandler
}
}).then(successHandler, errorHandler);
}
$httpProvider.interceptors.push(function($q) {
return {
'responseError': function(rejection) {
console.log(rejection.config.handler);
return rejection;
}
};
});

How to intercept all http requests using AngularJS?

My Question is a bigger and broader version of this question. I want to intercept all http requests issued inside an AngularJS function. Later I need to alter the request URL and than pass it to the server..
How can I do that ? So far I have used $httpProvider and $q to create a interceptor but I am only able to intercept only $http requests not all the requests i.e. if someone clicks on any href link on my page etc. My interceptor code is :-
// register the interceptor as a service
myModule.factory('myHttpInterceptor', function ($q) {
return {
// optional method
'request': function (config) {
// do something on success
console.log("request success");
return config;
},
// optional method
'requestError': function (rejection) {
// do something on error
console.log("Request Error");
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
},
// optional method
'response': function (response) {
// do something on success
console.log("Response received");
return response;
},
// optional method
'responseError': function (rejection) {
// do something on error
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
}
};
});
myModule.factory('myInterceptor', ['$log', function ($log) {
$log.debug('$log is here to show you that this is a regular factory with injection');
var myInterceptor = {
};
return myInterceptor;
}]);
myModule.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('myHttpInterceptor');
}]);
Intercepting navigation to other pages is different from intercepting http requests. Maybe what you want is to intercept $location changes.
Have a read through this. You can do it but it depends on where the location changes are to.
http://uiadventures.wordpress.com/2013/09/06/routechange-angularjs/

How to abort a request in a http-interceptor?

In an app I have the following url structure for the api:
// public
public/url-xyz
// private
dashboard/url-xyz
Knowing that, and trying to save unnecessary requests: What would be the best way to cancel a request? What I've tried so far is:
angular.module('mymod').factory('httpInterceptors', function ($injector, $q, httpBuffer)
{
return {
request: function (config)
{
var url = config.url;
if (url.match('/dashboard/')) {
// immediately cancel request
var canceler = $q.defer();
config.timeout = canceler.promise;
canceler.reject(config);
// logout and go to login-page immediately
// ...
}
// request config or an empty promise
return config || $q.when(config);
}
};
});
But this can lead to problems with $ressource as it expects an array and gets an object as a response, if its request is canceled like that.
You should be able to return $q.reject([reason])
angular.module('mymod').factory('httpInterceptors', function ($injector, $q, httpBuffer)
{
return {
request: function (config)
{
var url = config.url;
if (url.match('/dashboard/')) {
// immediately cancel request
return $q.reject(config);
// logout and go to login-page immediately
// ...
}
// request config or an empty promise
return config || $q.when(config);
}
};
});

Categories