Skip to content

Commit

Permalink
feat($resource): add support for request and requestError interce…
Browse files Browse the repository at this point in the history
…ptors

This patch 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
  • Loading branch information
Michael Krotscheck authored and gkalpak committed Dec 5, 2017
1 parent 5e64d2a commit 26897bd
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 9 deletions.
21 changes: 16 additions & 5 deletions src/ngResource/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,11 @@ function shallowClearAndCopy(src, dst) {
* for more information.
* - **`responseType`** - `{string}` - see
* [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
* with `http response` object. See {@link ng.$http $http interceptors}. In addition, the
* - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods -
* `request`, `requestError`, `response`, and `responseError`. See
* {@link ng.$http $http interceptors} for details. In addition, the
* resource instance or array object is accessible by the `resource` property of the
* `http response` object.
* `http response` object passed to response interceptors.
* Keep in mind that the associated promise will be resolved with the value returned by the
* response interceptor, if one is specified. The default response interceptor returns
* `response.resource` (i.e. the resource instance or array).
Expand Down Expand Up @@ -707,6 +707,10 @@ angular.module('ngResource', ['ng']).
var isInstanceCall = this instanceof Resource;
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
var httpConfig = {};
var requestInterceptor = action.interceptor && action.interceptor.request ||
undefined;
var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
undefined;
var responseInterceptor = action.interceptor && action.interceptor.response ||
defaultResponseInterceptor;
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
Expand Down Expand Up @@ -743,7 +747,14 @@ angular.module('ngResource', ['ng']).
extend({}, extractParams(data, action.params || {}), params),
action.url);

var promise = $http(httpConfig).then(function(response) {
// Start the promise chain
var promise = $q.
resolve(httpConfig).
then(requestInterceptor).
catch(requestErrorInterceptor).
then($http);

promise = promise.then(function(response) {
var data = response.data;

if (data) {
Expand Down
270 changes: 266 additions & 4 deletions test/ngResource/resourceSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
describe('resource', function() {

describe('basic usage', function() {
var $resource, CreditCard, callback, $httpBackend, resourceProvider;
var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q;

beforeEach(module('ngResource'));

Expand All @@ -14,6 +14,7 @@ describe('basic usage', function() {
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$resource = $injector.get('$resource');
$q = $injector.get('$q');
CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, {
charge:{
method:'post',
Expand Down Expand Up @@ -1129,6 +1130,187 @@ describe('basic usage', function() {
});


describe('requestInterceptor', function() {
var rejectReason = {'lol':'cat'};
var successSpy, failureSpy;

beforeEach(function() {
successSpy = jasmine.createSpy('successSpy');
failureSpy = jasmine.createSpy('failureSpy');
});

it('should allow per action request interceptor that gets full configuration', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function(httpConfig) {
callback(httpConfig);
return httpConfig;
}
}
}
});

$httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);

$httpBackend.flush();
expect(callback).toHaveBeenCalledOnce();
expect(successSpy).toHaveBeenCalledOnce();
expect(failureSpy).not.toHaveBeenCalled();

expect(callback).toHaveBeenCalledWith({
'method': 'get',
'url': '/CreditCard'
});
});

it('should call $http with the value returned from requestInterceptor', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function(httpConfig) {
httpConfig.url = '/DebitCard';
return httpConfig;
}
}
}
});

$httpBackend.expect('GET', '/DebitCard').respond([{id: 1}]);

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);

$httpBackend.flush();
expect(successSpy).toHaveBeenCalledOnce();
expect(failureSpy).not.toHaveBeenCalled();
});

it('should abort the operation if the requestInterceptor rejects the operation', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function() {
return $q.reject(rejectReason);
}
}
}
});

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);

// Make sure all promises resolve.
$rootScope.$apply();

// Ensure the resource promise was rejected
expect(resource.$resolved).toBeTruthy();
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalledOnce();
expect(failureSpy).toHaveBeenCalledWith(rejectReason);

// Ensure that no requests were made.
$httpBackend.verifyNoOutstandingRequest();
});

it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function() {
return $q.reject(rejectReason);
},
requestError: function(rejection) {
callback(rejection);
return $q.reject(rejection);
}
}
}
});

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);
$rootScope.$digest();

expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith(rejectReason);
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalledOnce();
expect(failureSpy).toHaveBeenCalledWith(rejectReason);

// Ensure that no requests were made.
$httpBackend.verifyNoOutstandingRequest();
});

it('should abort the operation if a requestErrorInterceptor rejects the operation', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function() {
return $q.reject(rejectReason);
},
requestError: function(rejection) {
return $q.reject(rejection);
}
}
}
});

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);
$rootScope.$apply();

expect(resource.$resolved).toBeTruthy();
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalledOnce();
expect(failureSpy).toHaveBeenCalledWith(rejectReason);

// Ensure that no requests were made.
$httpBackend.verifyNoOutstandingRequest();
});

it('should continue the operation if a requestErrorInterceptor rescues it', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function(httpConfig) {
return $q.reject(httpConfig);
},
requestError: function(httpConfig) {
return $q.resolve(httpConfig);
}
}
}
});

$httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);
$httpBackend.flush();

expect(resource.$resolved).toBeTruthy();
expect(successSpy).toHaveBeenCalledOnce();
expect(failureSpy).not.toHaveBeenCalled();
$httpBackend.verifyNoOutstandingRequest();
});
});

it('should allow per action response interceptor that gets full response', function() {
CreditCard = $resource('/CreditCard', {}, {
query: {
Expand Down Expand Up @@ -1584,6 +1766,7 @@ describe('extra params', function() {
var $http;
var $httpBackend;
var $resource;
var $rootScope;

beforeEach(module('ngResource'));

Expand All @@ -1593,10 +1776,11 @@ describe('extra params', function() {
});
}));

beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_) {
beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_, _$rootScope_) {
$http = _$http_;
$httpBackend = _$httpBackend_;
$resource = _$resource_;
$rootScope = _$rootScope_;
}));

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

$rootScope.$digest();
expect($http).toHaveBeenCalledWith(jasmine.objectContaining({params: {baz: 'qux'}}));
});

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

describe('errors', function() {
var $httpBackend, $resource, $q;
var $httpBackend, $resource, $q, $rootScope;

beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
Expand All @@ -1636,6 +1821,7 @@ describe('errors', function() {
$httpBackend = $injector.get('$httpBackend');
$resource = $injector.get('$resource');
$q = $injector.get('$q');
$rootScope = $injector.get('$rootScope');
}));


Expand Down Expand Up @@ -1838,6 +2024,81 @@ describe('handling rejections', function() {
expect($exceptionHandler.errors[0]).toMatch(/^Error: should be caught/);
}
);

describe('requestInterceptor', function() {
var rejectReason = {'lol':'cat'};
var $q, $rootScope;
var successSpy, failureSpy, callback;

beforeEach(inject(function(_$q_, _$rootScope_) {
$q = _$q_;
$rootScope = _$rootScope_;

successSpy = jasmine.createSpy('successSpy');
failureSpy = jasmine.createSpy('failureSpy');
callback = jasmine.createSpy();
}));

it('should call requestErrorInterceptor if requestInterceptor throws an error', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function() {
throw rejectReason;
},
requestError: function(rejection) {
callback(rejection);
return $q.reject(rejection);
}
}
}
});

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);
$rootScope.$apply();

expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith(rejectReason);
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalledOnce();
expect(failureSpy).toHaveBeenCalledWith(rejectReason);

// Ensure that no requests were made.
$httpBackend.verifyNoOutstandingRequest();
});

it('should abort the operation if a requestErrorInterceptor throws an exception', function() {
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'get',
isArray: true,
interceptor: {
request: function() {
return $q.reject();
},
requestError: function() {
throw rejectReason;
}
}
}
});

var resource = CreditCard.query();
resource.$promise.then(successSpy, failureSpy);
$rootScope.$apply();

expect(resource.$resolved).toBeTruthy();
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalledOnce();
expect(failureSpy).toHaveBeenCalledWith(rejectReason);

// Ensure that no requests were made.
$httpBackend.verifyNoOutstandingRequest();
});
});
});

describe('cancelling requests', function() {
Expand Down Expand Up @@ -1902,7 +2163,7 @@ describe('cancelling requests', function() {
);

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

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

var creditCard = CreditCard.get();
$rootScope.$digest();
expect(creditCard.$cancelRequest).toBeDefined();
expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q));
expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();
Expand Down

0 comments on commit 26897bd

Please sign in to comment.