提交 4882ce53 编写于 作者: M Matt Zabriskie 提交者: GitHub

Merge pull request #452 from nickuraltsev/cancel

Adding support for request cancellation
language: node_js language: node_js
node_js:
- node
email: email:
on_failure: change on_failure: change
on_success: never on_success: never
......
...@@ -305,7 +305,12 @@ These are the available config options for making requests. Only the `url` is re ...@@ -305,7 +305,12 @@ These are the available config options for making requests. Only the `url` is re
proxy: { proxy: {
host: '127.0.0.1', host: '127.0.0.1',
port: 9000 port: 9000
} },
// `cancelToken` specifies a cancel token that can be used to cancel the request
// (see Cancellation section below for details)
cancelToken: new CancelToken(function (cancel) {
})
} }
``` ```
...@@ -457,6 +462,49 @@ axios.get('/user/12345', { ...@@ -457,6 +462,49 @@ axios.get('/user/12345', {
}) })
``` ```
## Cancellation
You can cancel a request using a *cancel token*.
> The axios cancel token API is based on the [cancelable promises proposal](https://github.com/tc39/proposal-cancelable-promises), which is currently at Stage 1.
You can create a cancel token using the `CancelToken.source` factory as shown below:
```js
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
```
You can also create a cancel token by passing an executor function to the `CancelToken` constructor:
```js
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
```
## Semver ## Semver
Until axios reaches a `1.0` release, breaking changes will be released with a new minor version. For example `0.5.1`, and `0.5.4` will have the same API, but `0.6.0` will have breaking changes. Until axios reaches a `1.0` release, breaking changes will be released with a new minor version. For example `0.5.1`, and `0.5.4` will have the same API, but `0.6.0` will have breaking changes.
......
...@@ -41,6 +41,7 @@ export interface AxiosRequestConfig { ...@@ -41,6 +41,7 @@ export interface AxiosRequestConfig {
httpAgent?: any; httpAgent?: any;
httpsAgent?: any; httpsAgent?: any;
proxy?: AxiosProxyConfig; proxy?: AxiosProxyConfig;
cancelToken?: CancelToken;
} }
export interface AxiosResponse { export interface AxiosResponse {
...@@ -66,6 +67,34 @@ export interface Promise<V> { ...@@ -66,6 +67,34 @@ export interface Promise<V> {
export interface AxiosPromise extends Promise<AxiosResponse> { export interface AxiosPromise extends Promise<AxiosResponse> {
} }
export interface CancelStatic {
new (message?: string): Cancel;
}
export interface Cancel {
message: string;
}
export interface Canceler {
(message?: string): void;
}
export interface CancelTokenStatic {
new (executor: (cancel: Canceler) => void): CancelToken;
source(): CancelTokenSource;
}
export interface CancelToken {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
}
export interface CancelTokenSource {
token: CancelToken;
cancel: Canceler;
}
export interface AxiosInterceptorManager<V> { export interface AxiosInterceptorManager<V> {
use(onFulfilled: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number; use(onFulfilled: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
eject(id: number): void; eject(id: number): void;
...@@ -90,6 +119,9 @@ export interface AxiosStatic extends AxiosInstance { ...@@ -90,6 +119,9 @@ export interface AxiosStatic extends AxiosInstance {
(config: AxiosRequestConfig): AxiosPromise; (config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise; (url: string, config?: AxiosRequestConfig): AxiosPromise;
create(config?: AxiosRequestConfig): AxiosInstance; create(config?: AxiosRequestConfig): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
isCancel(value: any): boolean;
all<T>(values: (T | Promise<T>)[]): Promise<T[]>; all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R; spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
} }
......
...@@ -185,6 +185,15 @@ module.exports = function httpAdapter(config) { ...@@ -185,6 +185,15 @@ module.exports = function httpAdapter(config) {
}, config.timeout); }, config.timeout);
} }
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
req.abort();
reject(cancel);
aborted = true;
});
}
// Send the request // Send the request
if (utils.isStream(data)) { if (utils.isStream(data)) {
data.pipe(req); data.pipe(req);
......
...@@ -153,6 +153,15 @@ module.exports = function xhrAdapter(config) { ...@@ -153,6 +153,15 @@ module.exports = function xhrAdapter(config) {
request.upload.addEventListener('progress', config.onUploadProgress); 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) { if (requestData === undefined) {
requestData = null; requestData = null;
......
...@@ -34,6 +34,11 @@ axios.create = function create(defaultConfig) { ...@@ -34,6 +34,11 @@ axios.create = function create(defaultConfig) {
return createInstance(defaultConfig); return createInstance(defaultConfig);
}; };
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread // Expose all/spread
axios.all = function all(promises) { axios.all = function all(promises) {
return Promise.all(promises); return Promise.all(promises);
......
'use strict';
/**
* A `Cancel` is an object that is thrown when an operation is canceled.
*
* @class
* @param {string=} message The message.
*/
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
'use strict';
var Cancel = require('./Cancel');
/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @class
* @param {Function} executor The executor function.
*/
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* Throws a `Cancel` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
'use strict';
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
...@@ -2,8 +2,18 @@ ...@@ -2,8 +2,18 @@
var utils = require('./../utils'); var utils = require('./../utils');
var transformData = require('./transformData'); var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults'); var defaults = require('../defaults');
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/** /**
* Dispatch a request to the server using the configured adapter. * Dispatch a request to the server using the configured adapter.
* *
...@@ -11,6 +21,8 @@ var defaults = require('../defaults'); ...@@ -11,6 +21,8 @@ var defaults = require('../defaults');
* @returns {Promise} The Promise to be fulfilled * @returns {Promise} The Promise to be fulfilled
*/ */
module.exports = function dispatchRequest(config) { module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist // Ensure headers exist
config.headers = config.headers || {}; config.headers = config.headers || {};
...@@ -38,6 +50,8 @@ module.exports = function dispatchRequest(config) { ...@@ -38,6 +50,8 @@ module.exports = function dispatchRequest(config) {
var adapter = config.adapter || defaults.adapter; var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) { return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data // Transform response data
response.data = transformData( response.data = transformData(
response.data, response.data,
...@@ -46,16 +60,20 @@ module.exports = function dispatchRequest(config) { ...@@ -46,16 +60,20 @@ module.exports = function dispatchRequest(config) {
); );
return response; return response;
}, function onAdapterRejection(error) { }, function onAdapterRejection(reason) {
// Transform response data if (!isCancel(reason)) {
if (error && error.response) { throwIfCancellationRequested(config);
error.response.data = transformData(
error.response.data, // Transform response data
error.response.headers, if (reason && reason.response) {
config.transformResponse reason.response.data = transformData(
); reason.response.data,
reason.response.headers,
config.transformResponse
);
}
} }
return Promise.reject(error); return Promise.reject(reason);
}); });
}; };
...@@ -34,6 +34,12 @@ describe('static api', function () { ...@@ -34,6 +34,12 @@ describe('static api', function () {
it('should have factory method', function () { it('should have factory method', function () {
expect(typeof axios.create).toEqual('function'); expect(typeof axios.create).toEqual('function');
}); });
it('should have Cancel, CancelToken, and isCancel properties', function () {
expect(typeof axios.Cancel).toEqual('function');
expect(typeof axios.CancelToken).toEqual('function');
expect(typeof axios.isCancel).toEqual('function');
});
}); });
describe('instance api', function () { describe('instance api', function () {
......
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;
});
});
});
});
var Cancel = require('../../../lib/cancel/Cancel');
describe('Cancel', function() {
describe('toString', function() {
it('returns correct result when message is not specified', function() {
var cancel = new Cancel();
expect(cancel.toString()).toBe('Cancel');
});
it('returns correct result when message is specified', function() {
var cancel = new Cancel('Operation has been canceled.');
expect(cancel.toString()).toBe('Cancel: Operation has been canceled.');
});
});
});
var CancelToken = require('../../../lib/cancel/CancelToken');
var Cancel = require('../../../lib/cancel/Cancel');
describe('CancelToken', function() {
describe('constructor', function() {
it('throws when executor is not specified', function() {
expect(function() {
new CancelToken();
}).toThrowError(TypeError, 'executor must be a function.');
});
it('throws when executor is not a function', function() {
expect(function() {
new CancelToken(123);
}).toThrowError(TypeError, 'executor must be a function.');
});
});
describe('reason', function() {
it('returns a Cancel if cancellation has been requested', function() {
var cancel;
var token = new CancelToken(function(c) {
cancel = c;
});
cancel('Operation has been canceled.');
expect(token.reason).toEqual(jasmine.any(Cancel));
expect(token.reason.message).toBe('Operation has been canceled.');
});
it('returns undefined if cancellation has not been requested', function() {
var token = new CancelToken(function() {});
expect(token.reason).toBeUndefined();
});
});
describe('promise', function() {
it('returns a Promise that resolves when cancellation is requested', function(done) {
var cancel;
var token = new CancelToken(function(c) {
cancel = c;
});
token.promise.then(function onFulfilled(value) {
expect(value).toEqual(jasmine.any(Cancel));
expect(value.message).toBe('Operation has been canceled.');
done();
});
cancel('Operation has been canceled.');
});
});
describe('throwIfRequested', function() {
it('throws if cancellation has been requested', function() {
// Note: we cannot use expect.toThrowError here as Cancel does not inherit from Error
var cancel;
var token = new CancelToken(function(c) {
cancel = c;
});
cancel('Operation has been canceled.');
try {
token.throwIfRequested();
fail('Expected throwIfRequested to throw.');
} catch (thrown) {
if (!(thrown instanceof Cancel)) {
fail('Expected throwIfRequested to throw a Cancel, but it threw ' + thrown + '.');
}
expect(thrown.message).toBe('Operation has been canceled.');
}
});
it('does not throw if cancellation has not been requested', function() {
var token = new CancelToken(function() {});
token.throwIfRequested();
});
});
describe('source', function() {
it('returns an object containing token and cancel function', function() {
var source = CancelToken.source();
expect(source.token).toEqual(jasmine.any(CancelToken));
expect(source.cancel).toEqual(jasmine.any(Function));
expect(source.token.reason).toBeUndefined();
source.cancel('Operation has been canceled.');
expect(source.token.reason).toEqual(jasmine.any(Cancel));
expect(source.token.reason.message).toBe('Operation has been canceled.');
});
});
});
var isCancel = require('../../../lib/cancel/isCancel');
var Cancel = require('../../../lib/cancel/Cancel');
describe('isCancel', function() {
it('returns true if value is a Cancel', function() {
expect(isCancel(new Cancel())).toBe(true);
});
it('returns false if value is not a Cancel', function() {
expect(isCancel({ foo: 'bar' })).toBe(false);
});
});
...@@ -11,7 +11,15 @@ describe('instance', function () { ...@@ -11,7 +11,15 @@ describe('instance', function () {
var instance = axios.create(); var instance = axios.create();
for (var prop in axios) { for (var prop in axios) {
if (['Axios', 'create', 'all', 'spread', 'default'].indexOf(prop) > -1) { if ([
'Axios',
'create',
'Cancel',
'CancelToken',
'isCancel',
'all',
'spread',
'default'].indexOf(prop) > -1) {
continue; continue;
} }
expect(typeof instance[prop]).toBe(typeof axios[prop]); expect(typeof instance[prop]).toBe(typeof axios[prop]);
......
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError, AxiosInstance, AxiosAdapter } from '../../'; import axios, {
AxiosRequestConfig,
AxiosResponse,
AxiosError,
AxiosInstance,
AxiosAdapter,
Cancel,
CancelToken,
CancelTokenSource,
Canceler
} from '../../';
import { Promise } from 'es6-promise'; import { Promise } from 'es6-promise';
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
...@@ -30,7 +41,8 @@ const config: AxiosRequestConfig = { ...@@ -30,7 +41,8 @@ const config: AxiosRequestConfig = {
proxy: { proxy: {
host: '127.0.0.1', host: '127.0.0.1',
port: 9000 port: 9000
} },
cancelToken: new axios.CancelToken((cancel: Canceler) => {})
}; };
const handleResponse = (response: AxiosResponse) => { const handleResponse = (response: AxiosResponse) => {
...@@ -210,3 +222,18 @@ axios.get('/user') ...@@ -210,3 +222,18 @@ axios.get('/user')
axios.get('/user') axios.get('/user')
.catch((error: any) => Promise.resolve('foo')) .catch((error: any) => Promise.resolve('foo'))
.then((value: string) => {}); .then((value: string) => {});
// Cancellation
const source: CancelTokenSource = axios.CancelToken.source();
axios.get('/user', {
cancelToken: source.token
}).catch((thrown: AxiosError | Cancel) => {
if (axios.isCancel(thrown)) {
const cancel: Cancel = thrown;
console.log(cancel.message);
}
});
source.cancel('Operation has been canceled.');
...@@ -320,5 +320,21 @@ module.exports = { ...@@ -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();
});
});
} }
}; };
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册