I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.
There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.
Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.
Basically this is the behaviour I am trying to do with an interceptor:
factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.finally(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
}
}
}])
When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.
Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?
according to $http documentation, you can return your own config from request interceptor.
try something like this:
config(function($httpProvider) {
var cache = {};
$httpProvider.interceptors.push(function() {
return {
response : function(config) {
var key = createKey(config);
var cached = cache[key];
return cached ? cached : cached[key];
}
}
});
}
Very old question, but I'll give a shot to handle this situation.
If I understood correctly, you are trying to:
1 - Start a request and register something to refer back to it;
2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.
This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:
function DropoutInterceptor($injector) {
var $q = $q || $injector.get('$q');
var dropouts = {};
return {
'request': function(config) {
// I'm using the request's URL here to make
// this reference, but this can be bad for
// some situations.
if (dropouts.hasOwnProperty(config.url)) {
// Drop the request
dropouts[config.url].resolve();
}
dropouts[config.url] = $q.defer();
// If the request already have one timeout
// defined, keep it, othwerwise, set up ours.
config.timeout = config.timeout || dropouts[config.url];
return config;
},
'requestError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
},
'response': function(response) {
delete dropouts[response.config.url];
return response;
},
'responseError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
}
};
}
Related
I want to cache the response [i.e. parsed JSON response] of HTTP requests rather than the response itself. My data is big and gzipped so there is actually a fair performance hit decompressing this so would like to store the raw data itself.
Currently I am using a HTTP Interceptor for caching and TimeToLive mechanics described here alongside AngularJS' built in $cacheFactory.
So how can I, using an intercepter, stop the HTTP request and return my own response. Note I still plan on using $cacheFactory, I'd just manage my own data.
.factory('cacheInterceptor', ['$cacheFactory', function($cacheFactory) {
return {
request: function(config) {
if (config.cache) {
// if we have stored this request, return it, else let the request happen naturally and cache after
// Things I don't know:
// How to return existing cache data and prevent the reqeust from happening
// Cache the data I get back from a HTTP request
}
return config;
}
};
}])
I would preffer to inject this into your service and make your factory only handle the data recived/cached. This time I only created a service for you which holds the logic of HTTP / Cache switch. I think you will be able to create a factory to handle your data/states on your own.
.service('getService', ['$cacheFactory', '$http', '$q', function($cacheFactory, $http, $q) {
return {
request: function() {
function getData () {
var deferred = $q.defer();
if (angular.isUndefined($cacheFactory.get('getServiceData'))) {
$http({
'method': 'GET',
'url': 'someUrl'
}).then(function (result) {
$cacheFactory.put('getServiceData', result.data);
deferred.resolve(result.data);
});
} else {
deferred.resolve($cacheFactory.get('getServiceData'));
}
}
return getData();
},
flush: function () {
$cacheFactory.remove('getServiceData');
},
refresh: function () {
this.flush();
return this.refresh();
}
};
}]);
it's enough to add {cache: true} to the request options.
see the here
$http.get('some/url', {cache: true})
.then( ({data}) => data)
I have this app that uploads a file to a server using $cordovaFileTransfer and then sends data about the file to the same server. The file is transferred fine. The data is then sent to the server, and the server responds. But the response does not make it back to the promise callback. Why?
$scope.sendPost = function(data) {
//first upload a file then send more data about the file
$cordovaFileTransfer.upload('http://example.com', 'myfile.txt', options)
.then(function(result) {
var promise = MyFactory.sendFileData(data);
});
promise.then(function(response) {
//we never make it to here
});
}
and in MyFactory:
service.sendFileData = function(data) {
return $http({
//bunch of parameters. This function works, data is sent to the server and a response received
}).then(function(response) {
//this is fired when the response is received from the server. All is good so far.
return.response.data
});
}
return service;
$cordovaFileTransfer.upload returns a promise object, which you could use to build up promise chaining mechanism.
Code
$scope.sendPost = function(data) {
//get hold on `upload` function promise
var promise = $cordovaFileTransfer.upload('http://example.com', 'myfile.txt', options)
.then(function(result)) {
//return MyFactory.sendFileData promise here which will follow promise chaining
return MyFactory.sendFileData(data);
});
//promise.then will get call once `MyFactory.sendFileData` complete it
promise.then(function(response) {
//will get called once `sendFileData` complete its promise
});
}
its because you're relaying on another promise's callback to initiate a the promise and.. most probably before the promise gets initialized you are attaching a callback tot it.. so at the time of you attaching the callback, the promise is not yet initialized i.e. promise is null.. so in your console you'll see an error..
try doing some thing like
var x = function(response) {
//we'll make it to here now...
}
$cordovaFileTransfer.upload('http://example.com', 'myfile.txt', options)
.then(function(result)) {
var promise = MyFactory.sendFileData(data);
promise.then(x);
});
You should follow #PankajParkar solution though it's a better approach...
$scope.sendPost = function(data) {
//first upload a file then send more data about the file
$cordovaFileTransfer.upload('http://example.com', 'myfile.txt', options)
.then(function(result)) {
return MyFactory.sendFileData(result.data);
})
.then(function(response) {
});
UPDATE
I think I solved it myself. Check my interceptor that I posted as a solution below.
ORIGINAL
I was wondering if it would be possible to write an http interceptor that can let the caller know how the request is doing.
Right now, when I want to call my backend, I wrap an $http call in a wrapper that sets attributes on an object I pass it:
publ.wrap = function(f, ctrl){
ctrl.busy = true;
ctrl.error = false;
return f()
.then(function(res){
ctrl.busy = false;
ctrl.result = res;
return res;
}).catch(function(err){
ctrl.busy = false;
ctrl.error = err;
ctrl.result = undefined;
})
};
publ.login = function(args, ctrl){
publ.wrap(function(){
return $http.post('http://localhost:3001/authenticate', {
username : args.username,
password : args.password
}).then(function(jwt){
$cookies.put('token', jwt);
})
}, ctrl);
};
In this case, I call login(authArgs, $scope.loginCtrl) in my login page controller. Then I use loginCtrl.busy, loginCtrl.result & loginCtrl.error in my login template.
I pretty much want every call I make to the backend to set these attributes and make them available to the views that initiate the request.
Using a wrapper function like this gets the job done, but I'm wondering if it can be done using an interceptor? It feels to me like that would provide a much cleaner request flow that doesn't require me to explicitly wrap all of my backend calls in my services.
Now I read up on httpInterceptors, and can't seem to find a way to have them set attributes on a user-provided object. The closes thing I found was this article that has an example ( Timestamp Marker (request and response interceptors) ) where they add attributes to the config object in both the request and response interceptor stages.They don't show how to access the config object inside the responseError stage or in the caller controller.
Any help would be greatly appreciated :)
I use Angular events to handle stuff like this- for example:
.controller('parentCtrl', function($scope,$rootScope) {
$rootScope.$on('loading',function(e,_statusObj.loading) {
$scope.loading = _statusObj.loading;
if(!!_statusObj.msg) {
alert(_statusObj.msg);
}
});
})
.controller('childCtrl', function($scope,$http) {
$scope.myAjaxCall = function(_url,_data) {
$scope.$emit('loading',{ loading: true});
$http.post(_url,_data).success(function(_response) {
$scope.$emit('loading',{ loading: false });
})
.error(function(_error) {
$scope.$emit('loading',{
loading : false,
msg : _error.message
});
});
}
});
I managed to get the interceptor working. Apparently we CAN access the config file in all interceptor phases:
/******************************************
SETUP BUSY/ERROR/DATA HTTP INTERCEPTOR
*******************************************/
.config(function($httpProvider){
$httpProvider.interceptors.push(function($q) {
return {
request : function(config) {
if(config.ctrl){
config.ctrl.busy = true;
config.ctrl.error = false;
config.ctrl.data = undefined;
}
return config;
},
response : function(response) {
if(response.config && response.config.ctrl){
response.config.ctrl.busy = false;
response.config.ctrl.data = response.data;
}
return response;
},
responseError : function(response){
// note: maybe use a different error message for different kinds of responses?
var error = response.status + " "+response.statusText+" - "+response.data;
if(response.config && response.config.ctrl){
response.config.ctrl.busy = false;
response.config.ctrl.error = error;
}
return $q.reject(error);
}
};
});
})
I have a service with rest angular with following structure
function countrySvc(restangular) {
restangular.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
if (operation === 'getList') {
var newResponse = response.data;
return newResponse;
}
return response;
});
var baseCountry = restangular.all('country');
this.countries = function() {
baseCountry.getList();
};
}
also a controller
function countryCtrl(scope, countrySvc) {
scope.countries = countrySvc.countries();
}
but when i access the countries from controller, the result is empty with a successful request with data, my question is how a can extract the data from response with proper promise pattern, ie( i need array of countries when i access scope.countries)
You need to resolve promise...
There are two ways to do it...
1) Using $object
just add .$object to end of promise so once request is done it resolves promise...
scope.countries = countrySvc.countries().$object;
2) Using then
if you need to do some stuff after promise is resolved pick this option, once request is done callback function in then will be fired
scope.countries = countrySvc.countries().then(function (response){
// DO SOMETHING IF YOU NEED BEFORE SET OBJECT
scope.countries = response;
// DO SOMETHING IF YOU NEED AFTER SET OBJECT
});
I implemented Primus (Sockets) on my Server and would like to access it via the client, which uses AngularJS. I would like to be able to still use libraries like Restangular or the $resource from Angular. So IMHO the best way to achieve this is to extend the $http service, which is used by most libraries as the basis.
I would like this new service to be able to gracefully fall back to the normal $http, when there is no socket connection available.
In Pseudocode:
socketHttpService = function(config) {
if(socketEnabled) {
var message = buildMessageFromConfig();
primus.write(message);
return promise;
}
return $http(config);
}
Call it like you would $http
socketHttpService({method: 'GET', url: '/someUrl'}).then(function() {
// do whatever
});
My question is, how can i replace the standard $http service with this newly created one? Is there an elegant way, while still retaining the default $http behaviour?
In the meantime, I found a solution to the problem
.config(function($provide) {
$provide.decorator('$httpBackend', function($delegate, $q, $log, SocketService) {
// do not blast mock $httpBackend if it exists
if (angular.isDefined(angular.mock)) {
return $delegate;
}
var httpBackendSocket = function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
if(SocketService.isOpen) {
console.log('open');
method = method.toLowerCase();
// we only know get, post, put, delete
if(method === 'get' || method === 'post' || method === 'put' || method === 'delete') {
// we can not handle the authentication links via sockets, so exclude them
if( url.substring( 0, '/api/v1/currentuser'.length ) !== '/api/v1/currentuser' &&
!angular.equals(url, '/api/v1/login') &&
!angular.equals(url, '/api/v1/logout') &&
!angular.equals(url, '/api/v1/session') ) {
var promise = SocketService.writeRest(method, url, post || {});
return promise.then(function promiseSuccess(response) {
return callback(response.status, response.data, response.headers, '');
}, function promiseError(response) {
// is caught via http handlers
// LATER: If error, retry with $httpBackend ($delegate)
return callback(response.status, response.data, response.headers, '');
});
}
}
}
return $delegate(method, url, post, callback, headers, timeout, withCredentials, responseType);
}
return httpBackendSocket;
});
})
Why? Because it feels like 5 times faster than http, because there is a standing connection and I am not losing any of the realtime options. It's like a cherry on top.
Kind Regards