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

Merge pull request #452 from nickuraltsev/cancel

Adding support for request cancellation
language: node_js
node_js:
- node
email:
on_failure: change
on_success: never
......
......@@ -305,7 +305,12 @@ These are the available config options for making requests. Only the `url` is re
proxy: {
host: '127.0.0.1',
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', {
})
```
## 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
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 {
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig;
cancelToken?: CancelToken;
}
export interface AxiosResponse {
......@@ -66,6 +67,34 @@ export interface Promise<V> {
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> {
use(onFulfilled: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
eject(id: number): void;
......@@ -90,6 +119,9 @@ export interface AxiosStatic extends AxiosInstance {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
create(config?: AxiosRequestConfig): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
isCancel(value: any): boolean;
all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
}
......
......@@ -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);
......
......@@ -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;
......
......@@ -34,6 +34,11 @@ axios.create = function create(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
axios.all = function 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 @@
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
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.
*
......@@ -11,6 +21,8 @@ var defaults = require('../defaults');
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
......@@ -38,6 +50,8 @@ module.exports = function dispatchRequest(config) {
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
......@@ -46,16 +60,20 @@ module.exports = function dispatchRequest(config) {
);
return response;
}, function onAdapterRejection(error) {
// Transform response data
if (error && error.response) {
error.response.data = transformData(
error.response.data,
error.response.headers,
config.transformResponse
);
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
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);
});
};
......@@ -34,6 +34,12 @@ describe('static api', function () {
it('should have factory method', 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 () {
......
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 () {
var instance = axios.create();
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;
}
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';
const config: AxiosRequestConfig = {
......@@ -30,7 +41,8 @@ const config: AxiosRequestConfig = {
proxy: {
host: '127.0.0.1',
port: 9000
}
},
cancelToken: new axios.CancelToken((cancel: Canceler) => {})
};
const handleResponse = (response: AxiosResponse) => {
......@@ -210,3 +222,18 @@ axios.get('/user')
axios.get('/user')
.catch((error: any) => Promise.resolve('foo'))
.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 = {
});
});
});
},
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.
先完成此消息的编辑!
想要评论请 注册