Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 240a3dd

Browse files
gkalpakpetebacondarwin
authored andcommitted
feat($resource): add support for request and requestError interceptors (#15674)
This commit adds `request` and `requestError` interceptors for `$resource`, as per the documentation found for `$http` interceptors. It is important to note that returning an error at this stage of the request - before the call to `$http` - will completely bypass any global interceptors and/or recovery handlers, as those are added to a separate context. This is intentional; intercepting a request before it is passed to `$http` indicates that the resource itself has made a decision, and that it accepts the responsibility for recovery. Closes #5146 BREAKING CHANGE: Previously, calling a `$resource` method would synchronously call `$http`. Now, it will be called asynchronously (regardless if a `request`/`requestError` interceptor has been defined. This is not expected to affect applications at runtime, since the overall operation is asynchronous already, but may affect assertions in tests. For example, if you want to assert that `$http` has been called with specific arguments as a result of a `$resource` call, you now need to run a `$digest` first, to ensure the (possibly empty) request interceptor promise has been resolved. Before: ```js it('...', function() { $httpBackend.expectGET('/api/things').respond(...); var Things = $resource('/api/things'); Things.query(); expect($http).toHaveBeenCalledWith(...); }); ``` After: ```js it('...', function() { $httpBackend.expectGET('/api/things').respond(...); var Things = $resource('/api/things'); Things.query(); $rootScope.$digest(); expect($http).toHaveBeenCalledWith(...); }); ```
1 parent 7df2952 commit 240a3dd

File tree

2 files changed

+284
-10
lines changed

2 files changed

+284
-10
lines changed

src/ngResource/resource.js

+17-6
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,12 @@ function shallowClearAndCopy(src, dst) {
185185
* for more information.
186186
* - **`responseType`** - `{string}` - see
187187
* [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
188-
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
189-
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
190-
* with `http response` object. See {@link ng.$http $http interceptors}. In addition, the
191-
* resource instance or array object is accessible by the `resource` property of the
192-
* `http response` object.
188+
* - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods -
189+
* `request`, `requestError`, `response`, and `responseError`. See
190+
* {@link ng.$http $http interceptors} for details. Note that `request`/`requestError`
191+
* interceptors are applied before calling `$http`, thus before any global `$http` interceptors.
192+
* The resource instance or array object is accessible by the `resource` property of the
193+
* `http response` object passed to response interceptors.
193194
* Keep in mind that the associated promise will be resolved with the value returned by the
194195
* response interceptor, if one is specified. The default response interceptor returns
195196
* `response.resource` (i.e. the resource instance or array).
@@ -707,6 +708,9 @@ angular.module('ngResource', ['ng']).
707708
var isInstanceCall = this instanceof Resource;
708709
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
709710
var httpConfig = {};
711+
var requestInterceptor = action.interceptor && action.interceptor.request || undefined;
712+
var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
713+
undefined;
710714
var responseInterceptor = action.interceptor && action.interceptor.response ||
711715
defaultResponseInterceptor;
712716
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
@@ -743,7 +747,14 @@ angular.module('ngResource', ['ng']).
743747
extend({}, extractParams(data, action.params || {}), params),
744748
action.url);
745749

746-
var promise = $http(httpConfig).then(function(response) {
750+
// Start the promise chain
751+
var promise = $q.
752+
resolve(httpConfig).
753+
then(requestInterceptor).
754+
catch(requestErrorInterceptor).
755+
then($http);
756+
757+
promise = promise.then(function(response) {
747758
var data = response.data;
748759

749760
if (data) {

test/ngResource/resourceSpec.js

+267-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
describe('resource', function() {
44

55
describe('basic usage', function() {
6-
var $resource, CreditCard, callback, $httpBackend, resourceProvider;
6+
var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q;
77

88
beforeEach(module('ngResource'));
99

@@ -14,6 +14,7 @@ describe('basic usage', function() {
1414
beforeEach(inject(function($injector) {
1515
$httpBackend = $injector.get('$httpBackend');
1616
$resource = $injector.get('$resource');
17+
$q = $injector.get('$q');
1718
CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, {
1819
charge:{
1920
method:'post',
@@ -1129,6 +1130,188 @@ describe('basic usage', function() {
11291130
});
11301131

11311132

1133+
describe('requestInterceptor', function() {
1134+
var rejectReason = {'lol':'cat'};
1135+
var successSpy, failureSpy;
1136+
1137+
beforeEach(function() {
1138+
successSpy = jasmine.createSpy('successSpy');
1139+
failureSpy = jasmine.createSpy('failureSpy');
1140+
});
1141+
1142+
it('should allow per action request interceptor that gets full configuration', function() {
1143+
var CreditCard = $resource('/CreditCard', {}, {
1144+
query: {
1145+
method: 'get',
1146+
isArray: true,
1147+
interceptor: {
1148+
request: function(httpConfig) {
1149+
callback(httpConfig);
1150+
return httpConfig;
1151+
}
1152+
}
1153+
}
1154+
});
1155+
1156+
$httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);
1157+
1158+
var resource = CreditCard.query();
1159+
resource.$promise.then(successSpy, failureSpy);
1160+
1161+
$httpBackend.flush();
1162+
expect(callback).toHaveBeenCalledOnce();
1163+
expect(successSpy).toHaveBeenCalledOnce();
1164+
expect(failureSpy).not.toHaveBeenCalled();
1165+
1166+
expect(callback).toHaveBeenCalledWith({
1167+
'method': 'get',
1168+
'url': '/CreditCard'
1169+
});
1170+
});
1171+
1172+
it('should call $http with the value returned from requestInterceptor', function() {
1173+
var CreditCard = $resource('/CreditCard', {}, {
1174+
query: {
1175+
method: 'get',
1176+
isArray: true,
1177+
interceptor: {
1178+
request: function(httpConfig) {
1179+
httpConfig.url = '/DebitCard';
1180+
return httpConfig;
1181+
}
1182+
}
1183+
}
1184+
});
1185+
1186+
$httpBackend.expect('GET', '/DebitCard').respond([{id: 1}]);
1187+
1188+
var resource = CreditCard.query();
1189+
resource.$promise.then(successSpy, failureSpy);
1190+
1191+
$httpBackend.flush();
1192+
expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([
1193+
jasmine.objectContaining({id: 1})
1194+
]));
1195+
expect(failureSpy).not.toHaveBeenCalled();
1196+
});
1197+
1198+
it('should abort the operation if the requestInterceptor rejects the operation', function() {
1199+
var CreditCard = $resource('/CreditCard', {}, {
1200+
query: {
1201+
method: 'get',
1202+
isArray: true,
1203+
interceptor: {
1204+
request: function() {
1205+
return $q.reject(rejectReason);
1206+
}
1207+
}
1208+
}
1209+
});
1210+
1211+
var resource = CreditCard.query();
1212+
resource.$promise.then(successSpy, failureSpy);
1213+
1214+
// Make sure all promises resolve.
1215+
$rootScope.$apply();
1216+
1217+
// Ensure the resource promise was rejected
1218+
expect(resource.$resolved).toBeTruthy();
1219+
expect(successSpy).not.toHaveBeenCalled();
1220+
expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
1221+
1222+
// Ensure that no requests were made.
1223+
$httpBackend.verifyNoOutstandingRequest();
1224+
});
1225+
1226+
it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() {
1227+
var CreditCard = $resource('/CreditCard', {}, {
1228+
query: {
1229+
method: 'get',
1230+
isArray: true,
1231+
interceptor: {
1232+
request: function() {
1233+
return $q.reject(rejectReason);
1234+
},
1235+
requestError: function(rejection) {
1236+
callback(rejection);
1237+
return $q.reject(rejection);
1238+
}
1239+
}
1240+
}
1241+
});
1242+
1243+
var resource = CreditCard.query();
1244+
resource.$promise.then(successSpy, failureSpy);
1245+
$rootScope.$digest();
1246+
1247+
expect(callback).toHaveBeenCalledOnceWith(rejectReason);
1248+
expect(successSpy).not.toHaveBeenCalled();
1249+
expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
1250+
1251+
// Ensure that no requests were made.
1252+
$httpBackend.verifyNoOutstandingRequest();
1253+
});
1254+
1255+
it('should abort the operation if a requestErrorInterceptor rejects the operation', function() {
1256+
var CreditCard = $resource('/CreditCard', {}, {
1257+
query: {
1258+
method: 'get',
1259+
isArray: true,
1260+
interceptor: {
1261+
request: function() {
1262+
return $q.reject(rejectReason);
1263+
},
1264+
requestError: function(rejection) {
1265+
return $q.reject(rejection);
1266+
}
1267+
}
1268+
}
1269+
});
1270+
1271+
var resource = CreditCard.query();
1272+
resource.$promise.then(successSpy, failureSpy);
1273+
$rootScope.$apply();
1274+
1275+
expect(resource.$resolved).toBeTruthy();
1276+
expect(successSpy).not.toHaveBeenCalled();
1277+
expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
1278+
1279+
// Ensure that no requests were made.
1280+
$httpBackend.verifyNoOutstandingRequest();
1281+
});
1282+
1283+
it('should continue the operation if a requestErrorInterceptor rescues it', function() {
1284+
var CreditCard = $resource('/CreditCard', {}, {
1285+
query: {
1286+
method: 'get',
1287+
isArray: true,
1288+
interceptor: {
1289+
request: function(httpConfig) {
1290+
return $q.reject(httpConfig);
1291+
},
1292+
requestError: function(httpConfig) {
1293+
return $q.resolve(httpConfig);
1294+
}
1295+
}
1296+
}
1297+
});
1298+
1299+
$httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);
1300+
1301+
var resource = CreditCard.query();
1302+
resource.$promise.then(successSpy, failureSpy);
1303+
$httpBackend.flush();
1304+
1305+
expect(resource.$resolved).toBeTruthy();
1306+
expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([
1307+
jasmine.objectContaining({id: 1})
1308+
]));
1309+
expect(failureSpy).not.toHaveBeenCalled();
1310+
1311+
$httpBackend.verifyNoOutstandingRequest();
1312+
});
1313+
});
1314+
11321315
it('should allow per action response interceptor that gets full response', function() {
11331316
CreditCard = $resource('/CreditCard', {}, {
11341317
query: {
@@ -1584,6 +1767,7 @@ describe('extra params', function() {
15841767
var $http;
15851768
var $httpBackend;
15861769
var $resource;
1770+
var $rootScope;
15871771

15881772
beforeEach(module('ngResource'));
15891773

@@ -1593,10 +1777,11 @@ describe('extra params', function() {
15931777
});
15941778
}));
15951779

1596-
beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_) {
1780+
beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_, _$rootScope_) {
15971781
$http = _$http_;
15981782
$httpBackend = _$httpBackend_;
15991783
$resource = _$resource_;
1784+
$rootScope = _$rootScope_;
16001785
}));
16011786

16021787
afterEach(function() {
@@ -1610,6 +1795,7 @@ describe('extra params', function() {
16101795
var R = $resource('/:foo');
16111796
R.get({foo: 'bar', baz: 'qux'});
16121797

1798+
$rootScope.$digest();
16131799
expect($http).toHaveBeenCalledWith(jasmine.objectContaining({params: {baz: 'qux'}}));
16141800
});
16151801

@@ -1624,7 +1810,7 @@ describe('extra params', function() {
16241810
});
16251811

16261812
describe('errors', function() {
1627-
var $httpBackend, $resource, $q;
1813+
var $httpBackend, $resource, $q, $rootScope;
16281814

16291815
beforeEach(module(function($exceptionHandlerProvider) {
16301816
$exceptionHandlerProvider.mode('log');
@@ -1636,6 +1822,7 @@ describe('errors', function() {
16361822
$httpBackend = $injector.get('$httpBackend');
16371823
$resource = $injector.get('$resource');
16381824
$q = $injector.get('$q');
1825+
$rootScope = $injector.get('$rootScope');
16391826
}));
16401827

16411828

@@ -1838,6 +2025,81 @@ describe('handling rejections', function() {
18382025
expect($exceptionHandler.errors[0]).toMatch(/^Error: should be caught/);
18392026
}
18402027
);
2028+
2029+
describe('requestInterceptor', function() {
2030+
var rejectReason = {'lol':'cat'};
2031+
var $q, $rootScope;
2032+
var successSpy, failureSpy, callback;
2033+
2034+
beforeEach(inject(function(_$q_, _$rootScope_) {
2035+
$q = _$q_;
2036+
$rootScope = _$rootScope_;
2037+
2038+
successSpy = jasmine.createSpy('successSpy');
2039+
failureSpy = jasmine.createSpy('failureSpy');
2040+
callback = jasmine.createSpy();
2041+
}));
2042+
2043+
it('should call requestErrorInterceptor if requestInterceptor throws an error', function() {
2044+
var CreditCard = $resource('/CreditCard', {}, {
2045+
query: {
2046+
method: 'get',
2047+
isArray: true,
2048+
interceptor: {
2049+
request: function() {
2050+
throw rejectReason;
2051+
},
2052+
requestError: function(rejection) {
2053+
callback(rejection);
2054+
return $q.reject(rejection);
2055+
}
2056+
}
2057+
}
2058+
});
2059+
2060+
var resource = CreditCard.query();
2061+
resource.$promise.then(successSpy, failureSpy);
2062+
$rootScope.$apply();
2063+
2064+
expect(callback).toHaveBeenCalledOnce();
2065+
expect(callback).toHaveBeenCalledWith(rejectReason);
2066+
expect(successSpy).not.toHaveBeenCalled();
2067+
expect(failureSpy).toHaveBeenCalledOnce();
2068+
expect(failureSpy).toHaveBeenCalledWith(rejectReason);
2069+
2070+
// Ensure that no requests were made.
2071+
$httpBackend.verifyNoOutstandingRequest();
2072+
});
2073+
2074+
it('should abort the operation if a requestErrorInterceptor throws an exception', function() {
2075+
var CreditCard = $resource('/CreditCard', {}, {
2076+
query: {
2077+
method: 'get',
2078+
isArray: true,
2079+
interceptor: {
2080+
request: function() {
2081+
return $q.reject();
2082+
},
2083+
requestError: function() {
2084+
throw rejectReason;
2085+
}
2086+
}
2087+
}
2088+
});
2089+
2090+
var resource = CreditCard.query();
2091+
resource.$promise.then(successSpy, failureSpy);
2092+
$rootScope.$apply();
2093+
2094+
expect(resource.$resolved).toBeTruthy();
2095+
expect(successSpy).not.toHaveBeenCalled();
2096+
expect(failureSpy).toHaveBeenCalledOnce();
2097+
expect(failureSpy).toHaveBeenCalledWith(rejectReason);
2098+
2099+
// Ensure that no requests were made.
2100+
$httpBackend.verifyNoOutstandingRequest();
2101+
});
2102+
});
18412103
});
18422104

18432105
describe('cancelling requests', function() {
@@ -1902,7 +2164,7 @@ describe('cancelling requests', function() {
19022164
);
19032165

19042166
it('should use `cancellable` value if passed a non-numeric `timeout` in an action',
1905-
inject(function($log, $q) {
2167+
inject(function($log, $q, $rootScope) {
19062168
spyOn($log, 'debug');
19072169
$httpBackend.whenGET('/CreditCard').respond({});
19082170

@@ -1915,6 +2177,7 @@ describe('cancelling requests', function() {
19152177
});
19162178

19172179
var creditCard = CreditCard.get();
2180+
$rootScope.$digest();
19182181
expect(creditCard.$cancelRequest).toBeDefined();
19192182
expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q));
19202183
expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();

0 commit comments

Comments
 (0)