I'm using CouchDBs bundled $.couch API within an Angular application. I'd like to intercept ALL ajax requests, whether from Angular's $http service or jQuery's $.ajax (which is what $.couch uses). I have an interceptor set up in angular as follows:
$httpProvider.interceptors.push(['$location', '$q', function($location, $q) {
return {
'request': function(request) {
return request;
},
'responseError': function(response) {
if (response.status === 401) {
console.log("UH OH")
}
// otherwise, default behaviour
return $q.reject(response);
}
};
}]);
and what I've noticed is that requests and response errors from the $http angular service are caught, but the $.ajax calls and errors are not. So my question is, what's the proper way for me to intercept all ajax requests in my app from in and out of angular? Ideally I'd like to have one handler for both.
don't have a true answer, but surely interceptors are an array of property specific of angular's $httpProvider object. None similar is present in jquery. No way that code of yours could know something about $.ajax calls. I don't know, maybe you could wrap jquery ajax in a promise, re-creating something like interceptors.
in this post is described how interceptors are made under the hood in angular. This could be an hint for you.
what are interceptors
more info
Hope it helps!
Related
I have an http interceptor for my angularjs app that catches any http exceptions and handles them. There are a few cases where i would like to catch the error from the request and handle them there, preventing the error from bubbling to the interceptor. Does anyone know how this might be possible?
Here is the interceptor:
angular.module('x')
.factory('HttpErrorHandlerFactory', ['$q', '$rootScope', function ($q, $rootScope) {
return {
'responseError': function (rejection) {
if (rejection.status == 0) return $q.reject(rejection);
if (rejection.data === '')
rejection.data = 'Error';
$rootScope.Message.Value = rejection.data;
return $q.reject(rejection);
}
};
}]);
This is set at the app level, so any http requests that go through are caught by this. if they error, it catches them and displays the message.
Usually, we have calls like this:
someFactory.GetSomething(scope.id)
.success(function (result) {
//do something with result
}).error(function(error){
//MY QUESTION IS, HOW TO GET THIS ERROR TO NOT BUBBBLE UP TO THE INTERCEPTOR
});
To my best knowledge you cant really prevent the interceptor from catching the error before the caller function does. indeed that is annoying.
My solution was to add a config object to specific http requests (the ones i wanted to handle their error from the caller).
for example when using restangular:
myRestangularObject.get().withHttpConfig({handleByCaller: true})
and on my interceptor:
if (rejection.config.handleByCaller && rejection.config.handleByCaller === true) {
return $q.reject(rejection);
}
I know that this is a hack and doesn't answer your question but as i said, i don't think you can avoid the interceptor from being executed first.
In my controller, I use a method from a factory to update some data. For example, I'm trying to fetch an updated array of users. Should I be returning the promise itself from the factory? Or should I be returning the data from the promise (not sure if I phrased that correctly)?
I ask because I've seen it both ways, and some people say that the controller shouldn't have to handle whether the request was successful or if it failed. I'm specifically talking about the promise returned from $http in this case.
Or maybe in other words, should I be using the then() method inside the factory, or should I be using it in the controller after returning from the factory?
I've tried to handle the success and error callbacks (using the this() method) from within the service, but when I return the data to the controller, the users array is not properly updated. I'm assuming that's because of the request being async. So in the controller, it would look something like this:
vm.users = userFactory.getUsers();
If I handle the promise from within the controller, and set the users array within the then() method, it works fine. But this goes back to where I should be using then():
userFactory.getUsers().then(
function(data){
vm.users = data;
}, ...
Hopefully someone would be able to shed some light on this or provide some input. Thanks!
There's no way you can return the data from the factory (since it's an async call) without using either a callback approach (discouraged):
userFactory.prototype.getUsers = function(callback){
$http.get('users').then(function (response) {
callback(response.data);
});
};
Or the promise approach.
If you're worried about handling the errors on the controller, then worry not! You can handle errors on the service:
userFactory.prototype.getUsers = function(){
return $http.get('users').then(function(response) {
return response.data;
}, function(error) {
// Handle your error here
return [];
});
};
You can return the results of then and it will be chained. So things from service will execute and then, later on, Controller ones.
I have no problem with controller deciding what to do basing on response failing/succeding. In fact it lets you easily handle different cases and doesn't add a lot of overhead to the controller (controller should be as small as possible and focused on current task, but for me going different path whether request failed is the part of its task).
Anyway, in Angular HTTP requests are wrapped in promises internally (I remember that in the previous versions there was a way to automatically unwrap them), so returning from service/factory always returns a promise, which has to be resolved.
I prefer returning a promise from a service/factory because I tend to let other classes decide what to do with the response.
So this my controller:
app.controller('dbCtrl', function($scope, $http) {
$http.get("http://private-abc.apiary-mock.com/bus")
.success(function(response) {
$scope.network = response.networkupdates;});
});
What I wanted to do next is call a 2nd HTTP request, I guess in terms of best practice would it be best to create a 2nd controller to call the 2nd HTTP or would it be best to include the 2nd HTTP call in this current controller (and if so, how?)
Thanks.
So one of the cool aspects of using promises is that they can be chained. So in your case, you are calling:
$http.get("http://private-abc.apiary-mock.com/bus")
Which returns a promise that you can then chain to another promise like so:
var requests = $http.get("http://private-abc.apiary-mock.com/bus").then(function(response) {
$scope.network = response.networkupdates;
// make second get call and return it to chain the promises
return $http.get("some-other-endpoint").then(function(otherResponse) {
// you can do something here with the response data
return otherResponse;
});
});
What you have now is two chained promises that will return the final value, so if you call this later:
requests.then(function(otherResponse) {
// or you can do something here
doSomething(otherResponse);
});
As far as best practices for Angular, I would say you are better off creating a service or factory to handle any and all http requests. Controllers are really just meant to bind data to the view; services are where your business logic and data population should happen.
You can make your $http calls in the same controller.
The $http service is a function which takes a single argument — a configuration object — that is used to generate an HTTP request and returns a promise with two $http specific methods: success and error. It internally uses $q (a promise/deferred implementation inspired by Kris Kowal's Q).
If your two $http are independent of each other you use the $q.all to "join" the results of your http calls.
Example :
$q.all([
$http.get("http://private-abc.apiary-mock.com/bus"),
$http.get('/someUrl')
]).then(function(results) {
$scope.network = results[0];
$scope.whatevername= results[1]
});
}
If your http calls are dependent on one another then you can use the concept of chaining.
for example:
$http.get("http://private-abc.apiary-mock.com/bus").then(function(result) {
$scope.network = result.networkupdates;
return $http.get("someurl").then(function(res) {
return res;
});
});
For refernce of q you can see https://github.com/kriskowal/q
For refernce of $q service you can see https://docs.angularjs.org/api/ng/service/$q
On route change, I need to abort ALL pending requests from previous route so that I don't run into problems of responses from previous route messing up data on my current route (it happens sometimes when responses from previous route take long time to finish).
I have thought about using http interceptor for this:
$httpProvider.interceptors.push(function($q) {
return {
'request': function(config) {
},
'response': function(response) {
}
};
});
In the request function, I could modify the config.timeout with a promise as suggested here and store all the deferred objects in a global cache so that I could cancel all of them.
The problem with this approach is that it may override config.timeout set in other places in the code.
I think another solution could be to cancel all ajax requests at XMLHttpRequest level, but I don't know how to do it.
Any suggestions? Thanks.
As you say, timeout is the only API we have of use right now to cancel a running $http request. I think you're right on the money with an interceptor coupled with a cancel promise.
What you could do is attach the full deferred object on the $http request, and cancel all pendingRequests in your route change handler.
Something like this could (perhaps*) work?
angular.module('module').config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q) {
return {
request: function (config) {
if (!config.timeout) {
config.cancel = $q.defer();
config.timeout = config.cancel.promise;
}
return config;
}
}
});
});
angular.module('module').run(function ($rootScope, $http) {
$rootScope.$on('$stateChangeStart', function () {
$http.pendingRequests.forEach(function (pendingReq) {
if (pendingReq.cancel) {
pendingReq.cancel.resolve('Cancel!');
}
});
});
});
*: I say perhaps, because I had success with this approach, but it's seldom you find a silver bullet to something like this.
edit
If you need to bypass the error handler of the cancelled promise, hook into responseError property and do manual labour there.
angular.module('module').config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q) {
return {
responseError: function (response) {
if (response.config.timeout.$$state.value === 'Cancel!') {
// debugger;
return $q.when('bypassed');
}
}
}
});
});
I'm starting to think that there is no generic/'cool' solution to reach the end result you desire. Treading some odd ground here : )
edit2:
Testing the above myself now. Returning $q.when('something') in the responseError will effectively bypass the error callback of the cancelled $http request. Let me know if it works out for you.
I am attempting to implement an async cache on top of $resource using $cacheFactory.
Poking around in the $http source it appears to support returning promises from the cache. I would assume that after that I could simply resolve the promise with cache data or reject it and let $http do its thing, which then would put data back in the cache. Problem is.. I just doesn't work. Does $http ACTUALLY support promises?
https://github.com/angular/angular.js/blob/master/src/ng/http.js#L895
if (cache) {
cachedResp = cache.get(url);
if (isDefined(cachedResp)) {
if (isPromiseLike(cachedResp)) {
// cached request has already been sent, but there is no response yet
cachedResp.then(removePendingReq, removePendingReq);
return cachedResp;
} else {
// serving from cache
if (isArray(cachedResp)) {
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
} else {
resolvePromise(cachedResp, 200, {}, 'OK');
}
}
} else {
// put the promise for the non-transformed response into cache as a placeholder
cache.put(url, promise);
}
}
This is were $http handles the caching, as you can see it does actually check if a promise is returning (Line #898). But it appears that both resolving or reject simply clears the request from the pending queue. How do I actually send the data or single $http to continue the request?
Here is a Plunker of about what I'm trying to accomplish.
http://plnkr.co/edit/TwXumrAunG9b5JKo5OlB?p=preview
There is a bug in AngularJS. When resolving (or rejecting) a promised return from the cache $http does not handle the data in any way.
Related Bug
https://github.com/angular/angular.js/pull/6534