Skip to content

Commit 2131f06

Browse files
Michael Krotscheckgkalpak
Michael Krotscheck
authored andcommitted
feat($resource): add support for request and requestError interceptors
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 angular#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 2131f06

File tree

2 files changed

+281
-9
lines changed

2 files changed

+281
-9
lines changed

src/ngResource/resource.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ 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
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. In addition, the
191191
* resource instance or array object is accessible by the `resource` property of the
192-
* `http response` object.
192+
* `http response` object passed to response interceptors.
193193
* Keep in mind that the associated promise will be resolved with the value returned by the
194194
* response interceptor, if one is specified. The default response interceptor returns
195195
* `response.resource` (i.e. the resource instance or array).
@@ -707,6 +707,9 @@ angular.module('ngResource', ['ng']).
707707
var isInstanceCall = this instanceof Resource;
708708
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
709709
var httpConfig = {};
710+
var requestInterceptor = action.interceptor && action.interceptor.request || undefined;
711+
var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
712+
undefined;
710713
var responseInterceptor = action.interceptor && action.interceptor.response ||
711714
defaultResponseInterceptor;
712715
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
@@ -743,7 +746,14 @@ angular.module('ngResource', ['ng']).
743746
extend({}, extractParams(data, action.params || {}), params),
744747
action.url);
745748

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

749759
if (data) {

test/ngResource/resourceSpec.js

+266-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,187 @@ 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).toHaveBeenCalledOnce();
1193+
expect(failureSpy).not.toHaveBeenCalled();
1194+
});
1195+
1196+
it('should abort the operation if the requestInterceptor rejects the operation', function() {
1197+
var CreditCard = $resource('/CreditCard', {}, {
1198+
query: {
1199+
method: 'get',
1200+
isArray: true,
1201+
interceptor: {
1202+
request: function() {
1203+
return $q.reject(rejectReason);
1204+
}
1205+
}
1206+
}
1207+
});
1208+
1209+
var resource = CreditCard.query();
1210+
resource.$promise.then(successSpy, failureSpy);
1211+
1212+
// Make sure all promises resolve.
1213+
$rootScope.$apply();
1214+
1215+
// Ensure the resource promise was rejected
1216+
expect(resource.$resolved).toBeTruthy();
1217+
expect(successSpy).not.toHaveBeenCalled();
1218+
expect(failureSpy).toHaveBeenCalledOnce();
1219+
expect(failureSpy).toHaveBeenCalledWith(rejectReason);
1220+
1221+
// Ensure that no requests were made.
1222+
$httpBackend.verifyNoOutstandingRequest();
1223+
});
1224+
1225+
it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() {
1226+
var CreditCard = $resource('/CreditCard', {}, {
1227+
query: {
1228+
method: 'get',
1229+
isArray: true,
1230+
interceptor: {
1231+
request: function() {
1232+
return $q.reject(rejectReason);
1233+
},
1234+
requestError: function(rejection) {
1235+
callback(rejection);
1236+
return $q.reject(rejection);
1237+
}
1238+
}
1239+
}
1240+
});
1241+
1242+
var resource = CreditCard.query();
1243+
resource.$promise.then(successSpy, failureSpy);
1244+
$rootScope.$digest();
1245+
1246+
expect(callback).toHaveBeenCalledOnce();
1247+
expect(callback).toHaveBeenCalledWith(rejectReason);
1248+
expect(successSpy).not.toHaveBeenCalled();
1249+
expect(failureSpy).toHaveBeenCalledOnce();
1250+
expect(failureSpy).toHaveBeenCalledWith(rejectReason);
1251+
1252+
// Ensure that no requests were made.
1253+
$httpBackend.verifyNoOutstandingRequest();
1254+
});
1255+
1256+
it('should abort the operation if a requestErrorInterceptor rejects the operation', function() {
1257+
var CreditCard = $resource('/CreditCard', {}, {
1258+
query: {
1259+
method: 'get',
1260+
isArray: true,
1261+
interceptor: {
1262+
request: function() {
1263+
return $q.reject(rejectReason);
1264+
},
1265+
requestError: function(rejection) {
1266+
return $q.reject(rejection);
1267+
}
1268+
}
1269+
}
1270+
});
1271+
1272+
var resource = CreditCard.query();
1273+
resource.$promise.then(successSpy, failureSpy);
1274+
$rootScope.$apply();
1275+
1276+
expect(resource.$resolved).toBeTruthy();
1277+
expect(successSpy).not.toHaveBeenCalled();
1278+
expect(failureSpy).toHaveBeenCalledOnce();
1279+
expect(failureSpy).toHaveBeenCalledWith(rejectReason);
1280+
1281+
// Ensure that no requests were made.
1282+
$httpBackend.verifyNoOutstandingRequest();
1283+
});
1284+
1285+
it('should continue the operation if a requestErrorInterceptor rescues it', function() {
1286+
var CreditCard = $resource('/CreditCard', {}, {
1287+
query: {
1288+
method: 'get',
1289+
isArray: true,
1290+
interceptor: {
1291+
request: function(httpConfig) {
1292+
return $q.reject(httpConfig);
1293+
},
1294+
requestError: function(httpConfig) {
1295+
return $q.resolve(httpConfig);
1296+
}
1297+
}
1298+
}
1299+
});
1300+
1301+
$httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);
1302+
1303+
var resource = CreditCard.query();
1304+
resource.$promise.then(successSpy, failureSpy);
1305+
$httpBackend.flush();
1306+
1307+
expect(resource.$resolved).toBeTruthy();
1308+
expect(successSpy).toHaveBeenCalledOnce();
1309+
expect(failureSpy).not.toHaveBeenCalled();
1310+
$httpBackend.verifyNoOutstandingRequest();
1311+
});
1312+
});
1313+
11321314
it('should allow per action response interceptor that gets full response', function() {
11331315
CreditCard = $resource('/CreditCard', {}, {
11341316
query: {
@@ -1584,6 +1766,7 @@ describe('extra params', function() {
15841766
var $http;
15851767
var $httpBackend;
15861768
var $resource;
1769+
var $rootScope;
15871770

15881771
beforeEach(module('ngResource'));
15891772

@@ -1593,10 +1776,11 @@ describe('extra params', function() {
15931776
});
15941777
}));
15951778

1596-
beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_) {
1779+
beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_, _$rootScope_) {
15971780
$http = _$http_;
15981781
$httpBackend = _$httpBackend_;
15991782
$resource = _$resource_;
1783+
$rootScope = _$rootScope_;
16001784
}));
16011785

16021786
afterEach(function() {
@@ -1610,6 +1794,7 @@ describe('extra params', function() {
16101794
var R = $resource('/:foo');
16111795
R.get({foo: 'bar', baz: 'qux'});
16121796

1797+
$rootScope.$digest();
16131798
expect($http).toHaveBeenCalledWith(jasmine.objectContaining({params: {baz: 'qux'}}));
16141799
});
16151800

@@ -1624,7 +1809,7 @@ describe('extra params', function() {
16241809
});
16251810

16261811
describe('errors', function() {
1627-
var $httpBackend, $resource, $q;
1812+
var $httpBackend, $resource, $q, $rootScope;
16281813

16291814
beforeEach(module(function($exceptionHandlerProvider) {
16301815
$exceptionHandlerProvider.mode('log');
@@ -1636,6 +1821,7 @@ describe('errors', function() {
16361821
$httpBackend = $injector.get('$httpBackend');
16371822
$resource = $injector.get('$resource');
16381823
$q = $injector.get('$q');
1824+
$rootScope = $injector.get('$rootScope');
16391825
}));
16401826

16411827

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

18432104
describe('cancelling requests', function() {
@@ -1902,7 +2163,7 @@ describe('cancelling requests', function() {
19022163
);
19032164

19042165
it('should use `cancellable` value if passed a non-numeric `timeout` in an action',
1905-
inject(function($log, $q) {
2166+
inject(function($log, $q, $rootScope) {
19062167
spyOn($log, 'debug');
19072168
$httpBackend.whenGET('/CreditCard').respond({});
19082169

@@ -1915,6 +2176,7 @@ describe('cancelling requests', function() {
19152176
});
19162177

19172178
var creditCard = CreditCard.get();
2179+
$rootScope.$digest();
19182180
expect(creditCard.$cancelRequest).toBeDefined();
19192181
expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q));
19202182
expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();

0 commit comments

Comments
 (0)