-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($http): promise-based request interceptors #1701
feat($http): promise-based request interceptors #1701
Conversation
@splondike the headers in the config object should be safe despite the shallow copy: we're actually changing the value of Of course, if my header object contains a reference to another object, the request interceptors would be able to mutate this referenced object. So given the following $http call:
The object referenced by x could be modified by a request interceptor (the value of a would be safe) – however, I don't think that this is a valid use case. (To defend against this, we'd have to make a deep-copy of the config for every single request). Nevertheless, this is a good point! I'll definitely add a test case to show that regular headers will not be mangled. @pkozlowski-opensource Yes, this PR adds the features of #929 with the added benefit of using promises. |
@inukshuk Yeah, a deep clone would probably be going a bit far. These patchsets look like they cover everything. Well done! |
Thanks for your contribution! In order for us to be able to accept it, we ask you to sign our CLA (contributor's license agreement). CLA is important for us to be able to avoid legal troubles down the road. For individuals (a simple click-through form): For corporations (print, sign and scan+email, fax or mail): |
@mhevery thanks! I just signed the CLA online. |
@mhevery is there any update on this? I signed the CLA online a while ago, please let me know in case there were any problems or if you require anything else. Would hate for this to be delayed due to licensing issues. Thanks! |
This is exactly what I was looking for. My use case: I'm using the excellent http-auth-interceptor however unfortunately the API I am consuming passes the auth-token in the response body rather than as a cookie so I need a way of injecting the new token into the config when the authService retries the buffered requests. It also means I can abstract this auth mechanism from the API calls in my code. |
@inukshuk Sorry about the delay. We are getting swamped. Could you help by squashing this into single CL and rebasing it onto master. My attempts of rebasing have failed. I have flagged this PR in my inbox, so I will not miss it once you update it, and I will get right on it. |
@mhevery I squashed the commit and rebased on master. There was a little conflict that was ultimately easy to resolve, however, I now have four ngResource tests failing. The tests fail because they put expectations on the entire config object in the response. We decided to make a defensive copy of the config object and added both the headers and the data to that copy to make it easier to pass around and, more importantly, to expose both to the interceptors. This way, an interceptor can alter the headers, for example, without altering the original config object. Perhaps you could quickly review this to let me know how to proceed? For all the tests to pass I'd have to add the headers and data to the expected data object. Here are the relevant parts in the code for your reference: Here we add the headers and data to the config object instead of using local variables. (The config object is beeing copied a few lines above): This is one of the tests that are failing: https://github.com/angular/angular.js/blob/master/test/ngResource/resourceSpec.js#L468 As you can see, the tests fails because it does not expect the config object to contain the headers and data. Finally, I have included a test that makes sure that the config object originally passed to $http is not altered by all this: |
IMHO these tests should not make assumptions on response.config value. |
I'm looking into the issues today – at the moment, using the rebased PR I see some strange behaviour in regard to the new $promise feature added to $resource which I haven't quite wrapped my head around. Specifically, the headersGetter and the the $then proxy are present in the response when I run the tests – the ngResource tests do not expect them to be there, though, and I have yet to understand why this is. I'll report back once this is is resolved. I would also change the ngResource expectations a little to be less restrictive if that's OK. |
Request interceptors are promise-based and can be registered analogously to response interceptors. For backwards compatibility, requests are handled exactly as before when no interceptors are registered. When request interceptors have been registered, the interceptors are prependend to the promise chain returned by $http. The interceptor promises will be resolved or rejected before the request is sent out and passed the request's transformed config object; therefore, the interceptors are able to change everything about the request before it is actually sent. This makes Angular play nice with many advanced requirements (e.g., complex authentication or analytics). Because the interceptors use promises, this approach is extremely flexible. For instance, it is possible to make additional requests before sending a requests by injecting new promises into the chain. Closes angular#929 This commit also adds tests that cover the initial implementation. Since request interceptors utilize promisesi, tests involving the mocked $httpBackend need to make an extra call to $rootScope.$apply for the intercepted requests to register with $httpBackend.
@mhevery I worked around thee issue now: I left the basic approach as is, i.e., we're making a defensive copy of the config object and add the headers to it. This copy is passed to the request interceptors as a promise. When the promise is resolved, the config object is passed on to sedReq which removes both data and headers from the config object again in order to be backwards compatible. IMO we should still adjust the expectations in the the ngResource tests, because they really test a specific implementation and not the specification if they make such restrictive assumptions. I'll gladly make the adjustments in a separate PR if you give me the green light. Meanwhile, this PR is rebased on master once more and all tests should pass (waiting anxiously for Travis to finish). I added one more test to make sure that intercepted headers are actually applied, too. |
@pkozlowski-opensource thanks for your support on IRC earlier – I think this is ready for review now. Let me know if anything's amiss! |
This is exactly what I need for a current project to inject the OAuth 1.0A signature into my $resource requests. Great job @inukshuk and I'll be using your branch until this gets merged into master...! |
Happy that this is implemented ! good job ! |
Fixes the request interceptor code sample and adds examples for intercepting the reuqest's URL, headers, or data.
Guys, thanks for spotting the bad example! I've updated the docs and also added more examples that illustrate how to intercept the request's URL, headers, or data. |
Nice work! Don't forget to squash both commits into one again. |
Our new, shiny checklist:
|
IE 8/9 on ci.angularjs.org don't seem to be happy. Any ideas? |
While reviewing #1851 I had a brief look at this PR as well and started thinking that we should handle goals of this as well as #1851 PR in a single generic way and that could be via "around" interceptors. The idea is that instead of having two different kinds of interceptors that are defined in two different places we could have a single interceptor that allows us to hook into both request and response promise chains. The biggest benefit of this is that these two hooks can share a per-request context via a closure. proposed api (names to be discussed):
there are several important things going on here:
The resolution chain then looks like this:
I believe that this would solve your use case as well as many more others. How do you feel about it? |
@IgorMinar I already tried to stream-line the interceptor logic a little by using a generic function to resolve them (see https://github.com/angular/angular.js/pull/1701/files#L0L809); combining them completely takes that a big step further of course. You mentioned the biggest benefit as having the interceptors to share closure scope. This is something that's possible now, too, (simply wrap request and response interceptor in an immediate function for instance) but I take it you also like that this way they can be defined as a service together, having explicit shared dependencies etc.? What I am a little concerned with is testing: in the current implementation we're skipping the config promise entirely if there are no interceptors – this was done for improved backwards compatibility but also because the tests relying on the mocked HTTP backend do no expect that there is another promise that needs to be resolved. Currently, it looks like one promise chain but there are of course two deferreds involved (plus, interceptors could inject their own, too) – would wiring up the promise chain the way you suggest take care of this issue? Otherwise we need to be aware that we might have to adjust the mocked HTTP backend for this feature. Having said that, I like the idea of having a single interceptor feature – regardless of whether I want to intercept requests or responses. It makes for a cleaner API I think. I'll gladly take a stab at this if you guys prefer this approach – if only to earn the t-shirt I got at RuPy last year! : ) |
@jbdeboer the failed tests on IE should be easy to fix – I'll hold off for now though, since we'll be going for the single interceptor approach. |
How about adding a "promise chain" concept: an object which holds a chain of promises, and can be "wrapped" or included to another promise chain. It could be something along the lines of: myApp.factory('myInterceptor', function($rootScope, $q) {
return function myInterceptorInstance(httpRequestChain) {
return $q.promiseChain()
.then(function(config) {
return config;
})
.then(httpRequestChain)
.then(function(response) {
return 'ha!';
});
}
} Possible implementation: $q.promiseChain = function(deferred, promise) {
deferred = deferred || $q.defer()
promise = promise || deferred.promise;
// keep beginning (deferred) and end (promise) of a promise chain
return {
deferred: deferred,
promise: promise,
then: function(success, error) {
if (isDefined(success) && !isDefined(error) && success.deferred) {
// Add another promise chain to the end.
anotherChain = success;
promise.then(anotherChain.deferred.resolve, anotherChain.deferred.reject);
return $q.promiseChain(deferred, anotherChain.promise);
} else {
// Usual callbacks.
return $q.promiseChain(deferred, promise.then(success, error));
}
},
};
} In $http: // Create basis http request chain.
chain = $q.requestChain()
.then(sendReq)
.then(transformResponse, transformResponse);
// Wrap chain by interceptors.
forEach(interceptors, function(interceptor) {
chain = interceptor(chain);
});
// Start the chain.
chain.deferred.resolve(config);
// If we only need promise interface, return underlying promise.
return chain.promise; PROs:
CONs:
|
Thanks for your hard work. I have used your tests, but we have done a slightly different implementation here: #2130 which we believe is a bit more consistant. |
@ashtuchkin it's quite important for me that we don't leak deferred objects |
I'm closing this PR since it has been superseded by #2130 |
Currently there is no easy way to globally intercept requests sent with $http – similar to the way responses can be intercepted with response interceptors. Furthermore, there are many legitimate scenarios and use-cases where the need for request interception arises; for example
Please see #929 for a further discussion (and use-cases) on this topic.
Implementation
The request interceptor registration works exactly like the registration of response interceptors (in fact, I have put this into a separate routine to make it more explicit). If request interceptors are present, they are prepended to the promise chain returned by $http – this was mostly devised and written by @splondike – this gives the interceptors a lot of flexibility and fits very well into the Angular API.
Tests and Documentation
This pull requests includes both tests and documentation of the new feature.
A note on testing: a separate call to $rootScope.$apply is necessary to test the interceptors with the mocked http backend.
Example
Compatibility
When no request interceptors are registered, no promises are prepended to the chain. Therefore, this feature should be completely backwards compatible – if you register not interceptors everything stays the same.