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);
}
};
});
Related
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;
});
Before making an HTTP request, I need to check if the access credentials I have are valid. If they are not valid, I need to make a first HTTP request to revalidate them and, after completion, then a second HTTP request (the original one). The function call needs to return Angular's $http promise from the second HTTP request. Here's my function:
var makeRequest = function (scope, address, request, basic, form) {
startLoad(scope);
// Check if user is logged in and needs a new session token...
if (ready() && (now() > getSessionExpires()-20) ) {
// Setup auth.session.refresh request
var refreshRequest = {
"refreshToken": getRefreshToken(),
"clientID": getClientID(),
};
// Make auth.session.refresh request
$http.post(API + 'auth.session.refresh', format(refreshRequest))
.error(function (data) {
// If refresh request fails, logout and redirect to expired message
stopLoad(scope); logoff();
$window.location.href = '/error/expired';
return;
})
.success(function (data) {
// If refresh request succeeds, makeRequest is called recursively and the else condition runs
process(data, true);
return makeRequest(scope, address, request, basic, form);
});
} else { // if not logged in, or if session token is valid, run request function as usual
// Add Authorization header with valid sessionToken
if (ready()) $http.defaults.headers.post['Authorization'] = 'Bearer ' + getSessionToken();
// Basic Request: returns promise (if next context not set, can chain other action)
if (basic) {
return $http.post(API + address, request)
.error(function(data) {
if (data && scope) stopLoad(scope, data.message);
else if (scope) stopLoad(scope, Defaults.serverError);
else stopLoad();
if (form) resetForm(form);
})
.success(function(data) {
process(data);
if (scope) {
stopLoad(scope);
if (scope.context.next) $location.path(scope.context.next);
} else stopLoad();
if (form) resetForm(form);
});
}
// Custom Request: returns promise (can chain .error, .success)
else return $http.post(API + address, request);
}
};
When the token is found to be invalid, however, the function returns undefined, and I get an error that I cannot run .success() or .error(). The else functionality runs, but I'm wondering how I can ensure that I don't get this error. Thank you!
Just return the upper $http.post(/*...*/) and let promise chaining do it's magic:
return $http.post(API + 'auth.session.refresh', format(refreshRequest))
.catch(function (response) {
// If refresh request fails, logout and redirect to expired message
stopLoad(scope); logoff();
$window.location.href = '/error/expired';
})
.then(function (response) {
// If refresh request succeeds, makeRequest is called recursively and the else condition runs
process(response.data, true);
return makeRequest(scope, address, request, basic, form);
});
UPDATE: since .success/.error functions are not chainable (and have been flagged deprecated), you should use .then and .catch instead.
$http.post(/*...*/)
.success(function(data) {
/* do something with data */
})
.error(function(err) {
/*...*/
});
becomes
$http.post(/*...*/)
.then(function(response) {
/*do something with response.data */
})
.catch(function(response) {
/*...*/
});
I want two different controllers to run different functions after some promises are resolved in a service (i dont want this service to make an http request each time a controller needs the data, I only want one http request).
I have a service that makes a request and gets a promise. I want controller1 to see this resolution and then run some code. I then want controller2 to also see that this promise resolves and run some code (basically multiple then() methods that run on the same promise but from different files). How can I go about doing this?
All the examples I have seen have one controller running code after a certain promise resolves, but not multiple controllers listening for the same promise to resolve.
here is some code im borrowing from this article (ill add a 'mother controller' to illustrate my example, I dont want the son service to ever make his http call twice): http://andyshora.com/promises-angularjs-explained-as-cartoon.html
son service
app.factory('SonService', function ($http, $q) {
return {
getWeather: function() {
// the $http API is based on the deferred/promise APIs exposed by the $q service
// so it returns a promise for us by default
return $http.get('http://fishing-weather-api.com/sunday/afternoon')
.then(function(response) {
if (typeof response.data === 'object') {
return response.data;
} else {
// invalid response
return $q.reject(response.data);
}
}, function(response) {
// something went wrong
return $q.reject(response.data);
});
}
};
});
father Controller:
// function somewhere in father-controller.js
var makePromiseWithSon = function() {
// This service's function returns a promise, but we'll deal with that shortly
SonService.getWeather()
// then() called when son gets back
.then(function(data) {
// promise fulfilled
if (data.forecast==='good') {
prepareFishingTrip();
} else {
prepareSundayRoastDinner();
}
}, function(error) {
// promise rejected, could log the error with: console.log('error', error);
prepareSundayRoastDinner();
});
};
Mother Controller:
var makePromiseWithSon = function() {
SonService.getWeather()
// then() called when son gets back
.then(function(data) {
// promise fulfilled
if (data.forecast==='good') {
workInTheGarden();
} else {
sweepTheHouse();
}
}, function(error) {
// promise rejected, could log the error with: console.log('error', error);
sweepTheHouse();
});
};
To have your factory service only get the url once, store the httpPromise in your factory service.
app.factory('SonService', function ($http) {
var weatherPromise;
function getWeather() {
return $http.get('http://fishing-weather-api.com/sunday/afternoon')
.then(function(response) {
if (typeof response.data === 'object') {
return response.data;
} else {
// invalid response
throw response;
}
}, function(response) {
// something went wrong
throw response;
});
}
function sonService() {
if (!weatherPromise) {
//save the httpPromise
weatherPromise = getWeather();
}
return weatherPromise;
}
return sonService;
});
The simple answer, in a non-angular-specific (but really easy to apply to Angular) way, is to create a service which caches ON-OUTBOUND-REQUEST (rather than caching return values, like most systems would).
function SearchService (fetch) {
var cache = { };
return {
getSpecificThing: function (uri) {
var cachedSearch = cache[uri];
if (!cachedSearch) {
cachedSearch = fetch(uri).then(prepareData);
cache[uri] = cachedSearch;
}
return cachedSearch;
}
};
}
function A (searchService) {
var a = this;
Object.assign(a, {
load: function ( ) {
searchService.getSpecificThing("/abc").then(a.init.bind(a));
},
init: function (data) { /* ... */ }
});
}
function B (searchService) {
var b = this;
Object.assign(b, {
load: function ( ) {
searchService.getSpecificThing("/abc").then(b.init.bind(b));
},
init: function (data) { /* ... */ }
});
}
var searchService = SearchService(fetch);
var a = new A(searchService);
var b = new B(searchService);
a.load().then(/* is initialized */);
b.load().then(/* is initialized */);
They're sharing the same promise, because the service they were talking to cached and returned the same promise.
If you wanted to be safe, you could cache a promise and then return new instances of promises which resolve (or reject) based on the cached promise.
// instead of
return cachedSearch;
// replace it with
return Promise.resolve(cachedSearch);
Each user is now getting a new instance, every time you make a request, but each instance is also passing or failing based on the original cached call.
And of course you can take it further, and put a time-limit on the cache, or have hooks to invalidate the cache, or whatever...
Converting this to Angular is also a snap
SearchService is a service
A and B are controllers
use $http instead of fetch (though fetch is really pretty)
in fetch( ).then(prepareData) you'd be converting data from JSON on success;
in $http, you'd be returning response.data because your users don't want to have to do that
either way, you're performing that operation exactly once, per outbound call, so cache it, too
use $q (and q methods) instead of native Promise
use angular.extend, instead of Object.assign
You're done; you've now ported that whole concept into Angular AND VanillaJS
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
I have a function which is going to make a REST call, but it cannot do this until an auth token has been fetched.
So I wrapped the REST call in the 'then()' of the auth token call's promise, like so:
var RESTCall = function() {
return authTokenPromise.then(function() {
return $http.get('userService' {
userId: 1234,
someFlag: true
});
});
};
This has the result of waiting to fire off the call to the userService until the authToken promise (from the service that gets the auth token) has resolved.
The problem comes when I try to remove the hard-coded params that set userId and someFlag. What I want to do is this:
var RESTCall = function(params) {
return authTokenPromise.then(function() {
return $http.get('userService' {
userId: params.userId, // params is undefined
someFlag: params.flag // params is undefined
});
});
};
How do I pass params into the anonymous function scope created by then(function() {...})?