mirror of
https://github.com/tenrok/axios.git
synced 2026-06-05 16:42:32 +03:00
Merge pull request #452 from nickuraltsev/cancel
Adding support for request cancellation
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
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.
|
||||
|
||||
Vendored
+32
@@ -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);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'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;
|
||||
@@ -0,0 +1,57 @@
|
||||
'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;
|
||||
@@ -0,0 +1,5 @@
|
||||
'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 () {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
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]);
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user