diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 4d76a01..95e4e8c 100644 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -185,6 +185,15 @@ module.exports = function httpAdapter(config) { }, config.timeout); } + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + req.abort(); + reject(cancel); + aborted = true; + }); + } + // Send the request if (utils.isStream(data)) { data.pipe(req); diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index b0035d7..1fb890b 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -153,6 +153,15 @@ module.exports = function xhrAdapter(config) { request.upload.addEventListener('progress', config.onUploadProgress); } + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + request.abort(); + reject(cancel); + // Clean up request + request = null; + }); + } if (requestData === undefined) { requestData = null; diff --git a/lib/axios.js b/lib/axios.js index 8937b80..3fc1f32 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -34,6 +34,10 @@ axios.create = function create(defaultConfig) { return createInstance(defaultConfig); }; +// Expose Cancel & CancelToken +axios.Cancel = require('./cancel/Cancel'); +axios.CancelToken = require('./cancel/CancelToken'); + // Expose all/spread axios.all = function all(promises) { return Promise.all(promises); diff --git a/lib/core/dispatchRequest.js b/lib/core/dispatchRequest.js index fc52c03..5aa86d9 100644 --- a/lib/core/dispatchRequest.js +++ b/lib/core/dispatchRequest.js @@ -2,6 +2,16 @@ var utils = require('./../utils'); var transformData = require('./transformData'); +var Cancel = require('../cancel/Cancel'); + +/** + * Throws a `Cancel` if cancellation has been requested. + */ +function throwIfCancellationRequested(config) { + if (config.cancelToken) { + config.cancelToken.throwIfRequested(); + } +} /** * Dispatch a request to the server using whichever adapter @@ -11,6 +21,8 @@ var transformData = require('./transformData'); * @returns {Promise} The Promise to be fulfilled */ module.exports = function dispatchRequest(config) { + throwIfCancellationRequested(config); + // Ensure headers exist config.headers = config.headers || {}; @@ -52,6 +64,8 @@ module.exports = function dispatchRequest(config) { // Wrap synchronous adapter errors and pass configuration .then(adapter) .then(function onFulfilled(response) { + throwIfCancellationRequested(config); + // Transform response data response.data = transformData( response.data, @@ -60,16 +74,20 @@ module.exports = function dispatchRequest(config) { ); return response; - }, function onRejected(error) { - // Transform response data - if (error && error.response) { - error.response.data = transformData( - error.response.data, - error.response.headers, - config.transformResponse - ); + }, function onRejected(reason) { + if (!(reason instanceof Cancel)) { + throwIfCancellationRequested(config); + + // Transform response data + if (reason && reason.response) { + reason.response.data = transformData( + reason.response.data, + reason.response.headers, + config.transformResponse + ); + } } - return Promise.reject(error); + return Promise.reject(reason); }); }; diff --git a/test/specs/cancel.spec.js b/test/specs/cancel.spec.js new file mode 100644 index 0000000..d02d8e3 --- /dev/null +++ b/test/specs/cancel.spec.js @@ -0,0 +1,66 @@ +var Cancel = axios.Cancel; +var CancelToken = axios.CancelToken; + +describe('cancel', function() { + beforeEach(function() { + jasmine.Ajax.install(); + }); + + afterEach(function() { + jasmine.Ajax.uninstall(); + }); + + describe('when called before sending request', function() { + it('rejects Promise with a Cancel object', function (done) { + var source = CancelToken.source(); + source.cancel('Operation has been canceled.'); + axios.get('/foo', { + cancelToken: source.token + }).catch(function (thrown) { + expect(thrown).toEqual(jasmine.any(Cancel)); + expect(thrown.message).toBe('Operation has been canceled.'); + done(); + }); + }); + }); + + describe('when called after request has been sent', function() { + it('rejects Promise with a Cancel object', function (done) { + var source = CancelToken.source(); + axios.get('/foo/bar', { + cancelToken: source.token + }).catch(function (thrown) { + expect(thrown).toEqual(jasmine.any(Cancel)); + expect(thrown.message).toBe('Operation has been canceled.'); + done(); + }); + + getAjaxRequest().then(function (request) { + // call cancel() when the request has been sent, but a response has not been received + source.cancel('Operation has been canceled.'); + request.respondWith({ + status: 200, + responseText: 'OK' + }); + }); + }); + + it('calls abort on request object', function (done) { + var source = CancelToken.source(); + var request; + axios.get('/foo/bar', { + cancelToken: source.token + }).catch(function() { + // jasmine-ajax sets statusText to 'abort' when request.abort() is called + expect(request.statusText).toBe('abort'); + done(); + }); + + getAjaxRequest().then(function (req) { + // call cancel() when the request has been sent, but a response has not been received + source.cancel(); + request = req; + }); + }); + }); +}); diff --git a/test/specs/instance.spec.js b/test/specs/instance.spec.js index 4fac13d..be34230 100644 --- a/test/specs/instance.spec.js +++ b/test/specs/instance.spec.js @@ -11,7 +11,14 @@ describe('instance', function () { var instance = axios.create(); for (var prop in axios) { - if (['Axios', 'create', 'all', 'spread', 'default'].indexOf(prop) > -1) { + if ([ + 'Axios', + 'create', + 'Cancel', + 'CancelToken', + 'all', + 'spread', + 'default'].indexOf(prop) > -1) { continue; } expect(typeof instance[prop]).toBe(typeof axios[prop]); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index ba59558..1e41a63 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -320,5 +320,21 @@ module.exports = { }); }); }); + }, + + testCancel: function(test) { + var source = axios.CancelToken.source(); + server = http.createServer(function (req, res) { + // call cancel() when the request has been sent, but a response has not been received + source.cancel('Operation has been canceled.'); + }).listen(4444, function() { + axios.get('http://localhost:4444/', { + cancelToken: source.token + }).catch(function (thrown) { + test.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a Cancel obejct'); + test.equal(thrown.message, 'Operation has been canceled.'); + test.done(); + }); + }); } };