Custom cache for HTTP requests - angularjs

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)

Related

Cache API calls using AngularJS interceptor

Indent is to cache API calls by intercepting request and response using AngularJS interceptor. Below is my current code. I don't know what to do if the request exist in the cache. Is this feasible? Is this the right way to do it?
app.factory('apiCacheMiddleware', apiCacheMiddleware);
function apiCacheMiddleware($cacheFactory) {
var cache = $cacheFactory('apiCache');
var interceptor = {
request: function(config) {
console.log(config)
if(config.method === 'GET' && cache.get(config.url)){
// What to return from here???
}
return config;
},
response: function(response) {
console.log(response);
if(response.config.method === 'GET'){
cache.put(response.config.url, response.data);
}
return response;
}
};
return interceptor;
};
What you need to do (if you still happen to need it) is to assign the cache you created with $cacheFactory to the request (config.cache = cache; – it will probably be useful to check the previous value and take some decision upon it). On the response then, you will fill in that cache as you already do, and AngularJS will take care of the rest.

Angular return promise from httpBackend.when()

How do you return a promise from httpBackend.when()? I wanted to load some canned data stored in a .json file and return that from httpBackend.whenGET(). When I try to return the promise from http.get('mydata.json') the response is returned to the failure callback of the factory.
function getAvailablePackagesComplete(response) {
return response.data;
}
function getAvailablePackagesFailed(error) { // {error = Object {data: undefined, status: 0, config: Object, statusText: ""}
$log.error(error.data.description);
return false;
}
function getAvailablePackages() {
return $http.get('/1.0/get-available-packages')
.then(getAvailablePackagesComplete)
.catch(getAvailablePackagesFailed)
}
var data = {"package": "test", "version": "1"}
$httpBackend.whenGET('/1.0/get-available-packages').respond(function(method, url, data) {
// return [200,data, {}] // this works
return $http.get('app/home/fixtures/mydata.json'); // contains {"package: "test", "version": "1"}
}); //this doesn't work
As it is currently, $httpBackend (from ngMockE2E) does not support promises within its .respond - See AngularJS GitHub Issue #11245. As $httpBackend should be used to avoid making real HTTP requests, but you could let some requests pass through.
From AngularJS Docs:
This implementation can be used to respond with static or dynamic responses via the when api and its shortcuts (whenGET, whenPOST, etc) and optionally pass through requests to the real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch templates from a webserver).
To work around what you're trying to do though, you could try to have getAvailablePackages() return the HTTP GET for your json file path and defining an $httpBackend.whenGET('pathTo.json').passThrough();
I was hitting the same issue and my use case was building a mock of my entire API in JS so that other people could work off line and develop the UI.
To achieve that I have developed a plugin called angular-mocks-async which decorates the httpBackend and adds the .whenAsync( ) APi to it. Than you can easily mock responses and return promises like so:
var app = ng.module( 'mockApp', [
'ngMockE2E',
'ngMockE2EAsync'
]);
app.run( [ '$httpBackend', '$q', function( $httpBackend, $q ) {
$httpBackend.whenAsync(
'GET',
new RegExp( 'http://api.example.com/user/.+$' )
).respond( function( method, url, data, config ) {
var re = /.*\/user\/(\w+)/;
var userId = parseInt(url.replace(re, '$1'), 10);
var response = $q.defer();
setTimeout( function() {
var data = {
userId: userId
};
response.resolve( [ 200, "mock response", data ] );
}, 1000 );
return response.promise;
});
}]);
You can return promises from http interceptors. Here is an example of delaying an HTTP call for 1 second. You can add this in your app.config(....)
$httpProvider.interceptors.push(function($q, $timeout) {
return {
'response': function(response) {
var defer = $q.defer();
$timeout(function() {
defer.resolve(response);
}, 1000);
return defer.promise;
}
};
});

Angularjs - handling 401's for entire app

I have the following code in one of my Controllers to handle a 401 gracefully:
ChannelsService.query(function(response) {
$scope.channels = response;
}, function(error) {
if (error.status == 401) {
$state.go('login');
}
});
and my corresponding service:
myServices.factory('ChannelsService', function ($resource) {
return $resource('/channels', {}, {
query: { method: 'GET', isArray: true },
create: { method: 'POST' }
})
});
I would like to know how to handle 401's globally so that I don't have to work this logic into every controller. Is it an interceptor that I need and if so could someone share some code?
Thanks
For purposes of global error handling, authentication, or any kind of synchronous or asynchronous pre-processing of request or postprocessing of responses, it is desirable to be able to intercept requests before they are handed to the server and responses before they are handed over to the application code that initiated these requests. The interceptors leverage the promise APIs to fulfill this need for both synchronous and asynchronous pre-processing.
You can add an interceptor to the $httpProvider when configuring your application
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push(function($q) {
return {
'responseError': function(rejection){
var defer = $q.defer();
if(rejection.status == 401){
console.dir(rejection);
}
defer.reject(rejection);
return defer.promise;
}
};
});
}]);
As the name already suggests, this will intercept each request and call the provided function if there is a responseError (You could add interceptors for succeeded requests, too)
For further information, see the $http docs

Cancelling a request with a $http interceptor?

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

Delay an angular.js $http service

I have some angular factories for making ajax calls towards legacy ASP.NET .asmx web services like so:
module.factory('productService', ["$http",
function ($http) {
return {
getSpecialProducts: function (data) {
return $http.post('/ajax/Products.asmx/GetSpecialProducs', data);
}
}
} ]);
I'm testing on a local network so response times are "too" good. Is there a smart way of delaying the $http a couple of seconds from making the call to simulate a bad connection?
Or do I need to wrap all calls to the factory methods in a $timeout ?
$timeout(function() {
productService.getSpecialProducs(data).success(success).error(error);
}, $scope.MOCK_ajaxDelay);
Interesting question!
As you mentioned yourself, $timeout is the most logical choice for a delayed call. Instead of having $timeout calls everywhere, you could push a response interceptor that wraps the $http promise in a $timeout promise, as conceptually outlined in the documentation of $http, and register it in one of your configuration blocks. This means all $http calls are affected by the $timeout delay. Something along the lines of:
$httpProvider.interceptors.push(function($timeout) {
return {
"response": function (response) {
return $timeout(function() {
return response;
}, 2500);
}
};
});
As a bonus to your "to simulate a bad connection?", you could reject or do absolutely nothing randomly, too. Heh heh heh.
The new chrome device emulator has a network throttling function:
To get there: In Google Chrome, press F12 to open the Developer Tools. Then, on the top left corner, click the "Toggle device mode" icon (left to the "Elements" menu).
Developing more on the answer of #stevuu
responseInterceptors seems to be depreceted (as of 1.2.20) I have modified the code to work on the interceptors mechanism:
$httpProvider.interceptors.push(function($q, $timeout) {
return {
'response': function(response) {
var defer = $q.defer();
$timeout(function() {
defer.resolve(response);
}, 2300);
return defer.promise;
}
};
});
You could use the $q service for defer().promise pattern:
function someFunction(MOCK_ajaxDelay) {
var deferred = $q.defer();
$http.post('/ajax/Products.asmx/GetSpecialProducs', data).success(function(response) {
$timeout(function() {deferred.resolve({ success: true, response: response })}, MOCK_ajaxDelay);
}).error(function() {
$timeout(function() {deferred.resolve({ success: true, response: response } }, MOCK_ajaxDelay);
});
return deferred.promise;
}
someService.someFunction(500).then(function(data) {
if (data.success) {
$scope.items = data.response.d;
}
});
But if you are really mock testing, the better solution is to look into ngMock: http://docs.angularjs.org/api/ngMock.$httpBackend
While #stevuu's answer is correct, the syntax has changed in the newer AngularJS versions since then. The updated syntax is:
$httpProvider.interceptors.push(["$q", "$timeout", function ($q, $timeout) {
function slower(response) {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(response);
}, 2000);
return deferred.promise;
}
return {
'response': slower
};
}]);
You can achieve this using the promise api combined with a $timeout. The $http.post function returns a promise from which you can call .success and .error (these are http specific methods). This promise is resolved when the http request is complete. If you build your own promise then you can tell it to delay 2 seconds and then resolve when the http request is complete:
module.factory('productService', function ($http, $q, $timeout) {
return {
getSpecialProducts: function (data) {
var defer = $q.defer();
$http.post('/ajax/Products.asmx/GetSpecialProducs', data).success(
function(data) {
// successful http request, resolve after two seconds
$timeout(function() {
defer.resolve(data);
}, 2000)
}).error(function() {
defer.reject("Http Error");
})
return defer.promise;
}
}
});
But note - you will have to use promise.then(successCallback, errorCallback) functionality - that is, you'll lose the ability to access http headers, status & config from your controllers/directives unless you explicitly supply them to the object passed to defer.resolve({})
Links:
Defer/Promise Api
Http/Promise Api
Resolve egghead video
In response to the testing aspect of your question, Fiddler has a really useful function that helps when you need to simulate delays:
Click on the AutoResponders tab in Fiddler.
Add a rule with a regex that matches the URL of the request you want to delay.
Set the "respond with" to "*delay:1000" where the number is the delay in milliseconds.
The AutoResponder functionality in Fiddler is extremely useful for testing JS that involves a lot of http requests. You can set it to respond with particular http error codes, block responses, etc.
If you are using a service that returns a promise, then inside you should put a return before the $timeout as well because that returns just another promise.
return dataService.loadSavedItem({
save_id: item.save_id,
context: item.context
}).then(function (data) {
// timeout returns a promise
return $timeout(function () {
return data;
},2000);
});
Hope it helps someone!

Resources