-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($resource): fix interceptors and success/error callbacks #16446
Conversation
So that means if I do:
catch won't be called? I think the commit message should make this clearer. I also don't see any tests for this behavior, is this intentional? You could also add a note to the (general) commit message that you've replaced use of callbacks in the docs with promise, which is a nice touch |
Does this PR obsolete #6865? |
Yes. It is up for debate, but I think it is reasonable.
👍
There weren't any tests for the exact behavior before, so I played along. If we agree that this is the behavior we want, I can add tests 😃
Yes! (I didn't know about that one. Thx for pointing it out 👍) |
If find the new behavior very reasonable. A callback isn't (usually) chainable like a promise. So you have my 👍 for adding tests :D |
14275c2
to
7b23ce5
Compare
Added a couple of tests and updated the commit message with some examples. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great stuff @gkalpak. Just a few minor change requests. Per instance interceptors would also look good in $http, I wonder if we should implement them.
I think @petebacondarwin should also reviw this.
src/ngResource/resource.js
Outdated
* global `$http` interceptors. Also, rejecting or throwing an error inside the `request` | ||
* interceptor will result in calling the `responseError` interceptor. | ||
The resource instance or collection is available on the `resource` property of the | ||
* `http response` object passed to `response`/`responseError` interceptors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you link here to the example below?
$q.reject; | ||
var successCallback = onSuccess ? function(val) { | ||
onSuccess(val, response.headers, response.status, response.statusText); | ||
} : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't noop
better than undefined
here? (and in line 773)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is better for these to be undefined
since there is different behaviour in the call to then(...)
function if onError
is a defined function. But this only would make a difference if we returned the promise that is created at https://github.com/angular/angular.js/pull/16446/files#diff-67ae87630c0e91cba343677ed730daafR861.
As it stands undefined
and noop
have the same result below.
} | ||
|
||
describe('responseInterceptor', function() { | ||
it('should allow per action response interceptor that gets full response', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"full response" here means no headers or statusText, right? Because you don't assert for them. Could we add them if they are missing?
src/ngResource/resource.js
Outdated
@@ -294,46 +301,54 @@ function shallowClearAndCopy(src, dst) { | |||
* | |||
* ### Credit card resource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if this example and the next one had names that tell what they are showing, e.g. for this one "Basic Usage" and for the second one maybe "Accessing the http response / Promises and Callbacks"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
src/ngResource/resource.js
Outdated
* - **`timeout`** – `{number}` – timeout in milliseconds.<br /> | ||
* - **`cache`** – `{boolean|Cache}` – If true, a default `$http` cache will be used to cache the | ||
* GET request, otherwise if a cache instance built with {@link ng.$cacheFactory $cacheFactory} | ||
* is supplied, this cache will be used for caching. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what else would a "cache" be used for? 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question ¯\_(ツ)_/¯ - I'm just formatting short lines here 😛
I copied the description from $http
:
src/ngResource/resource.js
Outdated
* response interceptors. Make sure you return an appropriate value and not the `response` | ||
* object passed as input. For example, the default `response` interceptor (which gets applied | ||
* if you don't specify a custom one) returns `response.resource`. | ||
* - **`hasBody`** - `{boolean}` - Allows to specify if a request body should be included or not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I prefer more explicit definitions:
- **`hasBody`** - `{boolean}` - If true, then the request will have a body.
If not specified, then only POST, PUT and PATCH requrest will have a body.
* var User = $resource('/user/:userId', {userId:'@id'}); | ||
* var user = User.get({userId:123}, function() { | ||
* var User = $resource('/user/:userId', {userId: '@id'}); | ||
* User.get({userId: 123}).$promise.then(function(user) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+100
} : | ||
undefined); | ||
// Run the `success`/`error` callbacks, but do not let them affect the returned promise. | ||
promise.then(successCallback, errorCallback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you explicitly choose for the success and error callbacks not to affect the returned promise, thus causing a breaking change.
Is this BC really necessary? Why not allow them to pass through to returned promise, given that this was the previous behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be clear, this commit will be a BC regardless if we propagate callback errors to $promise
or not.
Previously, callbacks did not 100% affect $promise
. E.g. their returned value was ignored. It just so happened that throwing was reflected on $promise
as a rejection (since it happened inside a then()
callback).
Changing the general behavior (e.g. forwarding the value returned by callbacks to $promise
) would be unexpected and confusing imo. I am fine with both the current state of the PR as well as having thrown errors propagate to $promise
as rejections. (I just chose to not preserve that particular aspect of the previous behavior, because it felt unintentional.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
@@ -2026,6 +2143,41 @@ describe('handling rejections', function() { | |||
} | |||
); | |||
|
|||
|
|||
it('should not propagate exceptions in success callback to the returned promise', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it a bad thing for this to happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo, the success callback should not interfere with the promise chain, since it's strictly just a callback. Error handling should only happen in the interceptors. Do you think this will affect many apps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that in an ideal world this should be the behaviour - I am just wondering if it is so important that it should cause a BC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0f1ab76
to
c5f0cfe
Compare
Added another commit. PTAL, @Narretz & @petebacondarwin ❤️ |
travis only failed because of the templaterequest errors. ;) |
Previously, action-specific interceptors and `success`/`error` callbacks were executed in inconsistent relative orders and in a way that did not meet the general expectation for interceptor behavior (e.g. ability to recover from errors, performing asynchronous operations, etc). This commit fixes the behavior to make it more consistent and expected. The main differences are that `success`/`error` callbacks will now be run _after_ `response`/`responseError` interceptors complete (even if interceptors return a promise) and the correct callback will be called based on the result of the interceptor (e.g. if the `responseError` interceptor recovers from an error, the `success` callback will be called). See also angular#9334 (comment). This commit also replaces the use of `success`/`error` callbacks in the docs with using the returned promise. Fixes angular#6731 Fixes angular#9334 Closes angular#6865 BREAKING CHANGE: If you are not using `success` or `error` callbacks with `$resource`, your app should not be affected by this change. If you are using `success` or `error` callbacks (with or without response interceptors), one (subtle) difference is that throwing an error inside the callbacks will not propagate to the returned `$promise`. Therefore, you should try to use the promises whenever possible. E.g.: ```js // Avoid User.query(function onSuccess(users) { throw new Error(); }). $promise. catch(function onError() { /* Will not be called. */ }); // Prefer User.query(). $promise. then(function onSuccess(users) { throw new Error(); }). catch(function onError() { /* Will be called. */ }); ``` Finally, if you are using `success` or `error` callbacks with response interceptors, the callbacks will now always run _after_ the interceptors (and wait for them to resolve in case they return a promise). Previously, the `error` callback was called before the `responseError` interceptor and the `success` callback was synchronously called after the `response` interceptor. E.g.: ```js var User = $resource('/api/users/:id', {id: '@id'}, { get: { method: 'get', interceptor: { response: function(response) { console.log('responseInterceptor-1'); return $timeout(1000).then(function() { console.log('responseInterceptor-2'); return response.resource; }); }, responseError: function(response) { console.log('responseErrorInterceptor-1'); return $timeout(1000).then(function() { console.log('responseErrorInterceptor-2'); return $q.reject('Ooops!'); }); } } } }); var onSuccess = function(value) { console.log('successCallback', value); }; var onError = function(error) { console.log('errorCallback', error); }; // Assuming the following call is successful... User.get({id: 1}, onSuccess, onError); // Old behavior: // responseInterceptor-1 // successCallback, {/* Promise object */} // responseInterceptor-2 // New behavior: // responseInterceptor-1 // responseInterceptor-2 // successCallback, {/* User object */} // Assuming the following call returns an error... User.get({id: 2}, onSuccess, onError); // Old behavior: // errorCallback, {/* Response object */} // responseErrorInterceptor-1 // responseErrorInterceptor-2 // New behavior: // responseErrorInterceptor-1 // responseErrorInterceptor-2 // errorCallback, Ooops! ```
c5f0cfe
to
cad5a77
Compare
cad5a77
to
a2df3a1
Compare
The sitemap.xml might also prevent the indexing, as the partials are not listed. Related to angular#16432 Closes angular#16457 Closes angular#16446
Previously, action-specific interceptors and
success
/error
callbacks were executed in inconsistent relative orders and in a way that did not meet the general expectation for interceptor behavior (e.g. ability to recover from errors, performing asynchronous operations, etc).This commit fixes the behavior to make it more consistent and expected. The main differences are that
success
/error
callbacks will now be run afterresponse
/responseError
interceptors complete (even if interceptors return a promise) and the correct callback will be called based on the result of the interceptor (e.g. if theresponseError
interceptor recovers from an error, thesuccess
callback will becalled).
See also #9334 (comment).
This commit also replaces the use of
success
/error
callbacks in the docs with using the returned promise.Fixes #6731
Fixes #9334
Closes #6865
BREAKING CHANGE:
If you are not using
success
orerror
callbacks with$resource
, your app should not be affected by this change.If you are using
success
orerror
callbacks (with or without response interceptors), one (subtle) difference is that throwing an error inside the callbacks will not propagate to the returned$promise
. Therefore, you should try to use the promises whenever possible. E.g.:Finally, if you are using
success
orerror
callbacks with response interceptors, the callbacks will now always run after the interceptors (and wait for them to resolve in case they return a promise).Previously, the
error
callback was called before theresponseError
interceptor and thesuccess
callback was synchronously called after theresponse
interceptor. E.g.: