From 1e4352aa402c4e3dfc7b7276cfcde6349e24416d Mon Sep 17 00:00:00 2001 From: ak71845 Date: Wed, 5 Feb 2020 13:35:44 -0500 Subject: [PATCH] issue#2609 | Sasha | predictable axios requests - axios requests are not delayed by pre-emptive promise creation by default - add options to interceptors api ("synchronous" and "runWhen") - add documentation and unit tests --- README.md | 28 ++++++ lib/core/Axios.js | 57 +++++++++-- lib/core/InterceptorManager.js | 6 +- test/specs/adapter.spec.js | 15 +-- test/specs/interceptors.spec.js | 168 +++++++++++++++++++++++++++++++- 5 files changed, 253 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8be5ff6b51..4af7b32dcf 100755 --- a/README.md +++ b/README.md @@ -530,6 +530,34 @@ const instance = axios.create(); instance.interceptors.request.use(function () {/*...*/}); ``` +When you add request interceptors, they are presumed to be asynchronous by default. This can cause a delay +in the execution of your axios request when the main thread is blocked (a promise is created under the hood for +the interceptor and your request gets put on the bottom of the call stack). If your request interceptors are synchronous you can add a flag +to the options object that will tell axios to run the code synchronously and avoid any delays in request execution. + +```js +axios.interceptors.request.use(function (config) { + config.headers.test = 'I am only a header!'; + return config; +}, null, { synchronous: true }); +``` + +If you want to execute a particular interceptor based on a runtime check, +you can add a `runWhen` function to the options object. The interceptor will not be executed if the return +of `runWhen` is `false`. The function will be called with the config +object (don't forget that you can bind your own arguments to it as well.) This can be handy when you have an +asynchronous request interceptor that only needs to run at certain times. + +```js +function onGetCall(config) { + return config.method === 'get'; +} +axios.interceptors.request.use(function (config) { + config.headers.test = 'special get headers'; + return config; +}, null, { runWhen: onGetCall }); +``` + ## Handling Errors ```js diff --git a/lib/core/Axios.js b/lib/core/Axios.js index fb34aced66..38e32d08bd 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -45,20 +45,63 @@ Axios.prototype.request = function request(config) { config.method = 'get'; } - // Hook up interceptors middleware - var chain = [dispatchRequest, undefined]; - var promise = Promise.resolve(config); + var requestCancelled = config.cancelToken && config.cancelToken.reason; + // filter out skipped interceptors + var requestInterceptorChain = []; + var synchronousRequestInterceptors = true; this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { - chain.unshift(interceptor.fulfilled, interceptor.rejected); + if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { + return; + } + + synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; + + requestInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); + var responseInterceptorChain = []; this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { - chain.push(interceptor.fulfilled, interceptor.rejected); + responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); - while (chain.length) { - promise = promise.then(chain.shift(), chain.shift()); + var promise; + + if (requestCancelled || !synchronousRequestInterceptors) { + var chain = [dispatchRequest, undefined]; + + Array.prototype.unshift.apply(chain, requestInterceptorChain); + chain.concat(responseInterceptorChain); + + promise = Promise.resolve(config); + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()); + } + + return promise; + } + + + var newConfig = config; + while (requestInterceptorChain.length) { + var onFulfilled = requestInterceptorChain.shift(); + var onRejected = requestInterceptorChain.shift(); + try { + newConfig = onFulfilled(newConfig); + } catch (error) { + onRejected(error); + break; + } + } + + try { + promise = dispatchRequest(newConfig); + } catch (error) { + throw error; + } + + while (responseInterceptorChain.length) { + promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); } return promise; diff --git a/lib/core/InterceptorManager.js b/lib/core/InterceptorManager.js index 50d667bb44..900f44880d 100644 --- a/lib/core/InterceptorManager.js +++ b/lib/core/InterceptorManager.js @@ -14,10 +14,12 @@ function InterceptorManager() { * * @return {Number} An ID used to remove interceptor later */ -InterceptorManager.prototype.use = function use(fulfilled, rejected) { +InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { this.handlers.push({ fulfilled: fulfilled, - rejected: rejected + rejected: rejected, + synchronous: options ? options.synchronous : false, + runWhen: options ? options.runWhen : null }); return this.handlers.length - 1; }; diff --git a/test/specs/adapter.spec.js b/test/specs/adapter.spec.js index 6a00bc9df2..9d048d39b0 100644 --- a/test/specs/adapter.spec.js +++ b/test/specs/adapter.spec.js @@ -2,18 +2,19 @@ var axios = require('../../index'); describe('adapter', function () { it('should support custom adapter', function (done) { - var called = false; - axios('/foo', { - adapter: function (config) { - called = true; + adapter: function(config) { + return new Promise(function (resolve) { + setTimeout(function () { + config.headers.async = 'promise'; + resolve(config); + }, 100); + }); } - }); + }).catch(console.log); setTimeout(function () { - expect(called).toBe(true); done(); }, 100); }); }); - diff --git a/test/specs/interceptors.spec.js b/test/specs/interceptors.spec.js index effbcde258..4c1e7b60af 100644 --- a/test/specs/interceptors.spec.js +++ b/test/specs/interceptors.spec.js @@ -9,7 +9,8 @@ describe('interceptors', function () { axios.interceptors.response.handlers = []; }); - it('should add a request interceptor', function (done) { + it('should add a request interceptor (asynchronous by default)', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); axios.interceptors.request.use(function (config) { config.headers.test = 'added by interceptor'; return config; @@ -18,16 +19,148 @@ describe('interceptors', function () { axios('/foo'); getAjaxRequest().then(function (request) { - request.respondWith({ - status: 200, - responseText: 'OK' - }); + expect(promiseResolveSpy).toHaveBeenCalled(); + expect(request.requestHeaders.test).toBe('added by interceptor'); + done(); + }); + }); + + it('should add a request interceptor (explicitly flagged as asynchronous)', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); + axios.interceptors.request.use(function (config) { + config.headers.test = 'added by interceptor'; + return config; + }, null, { synchronous: false }); + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(promiseResolveSpy).toHaveBeenCalled(); expect(request.requestHeaders.test).toBe('added by interceptor'); done(); }); }); + it('should add a request interceptor that is executed synchronously when flag is provided', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); + axios.interceptors.request.use(function (config) { + config.headers.test = 'added by synchronous interceptor'; + return config; + }, null, { synchronous: true }); + + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(promiseResolveSpy).not.toHaveBeenCalled(); + expect(request.requestHeaders.test).toBe('added by synchronous interceptor'); + done(); + }); + }); + + it('should execute asynchronously when not all interceptors are explicitly flagged as synchronous', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); + axios.interceptors.request.use(function (config) { + config.headers.foo = 'uh oh, async'; + return config; + }); + + axios.interceptors.request.use(function (config) { + config.headers.test = 'added by synchronous interceptor'; + return config; + }, null, { synchronous: true }); + + axios.interceptors.request.use(function (config) { + config.headers.test = 'uh oh, async also'; + return config; + }); + + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(promiseResolveSpy).toHaveBeenCalled(); + expect(request.requestHeaders.foo).toBe('uh oh, async'); + expect(request.requestHeaders.test).toBe('uh oh, async also'); + done(); + }); + }); + + it('runs the interceptor if runWhen function is provided and resolves to true', function (done) { + function onGetCall(config) { + return config.method === 'get'; + } + axios.interceptors.request.use(function (config) { + config.headers.test = 'special get headers'; + return config; + }, null, { runWhen: onGetCall }); + + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders.test).toBe('special get headers'); + done(); + }); + }); + + it('does not run the interceptor if runWhen function is provided and resolves to false', function (done) { + function onPostCall(config) { + return config.method === 'post'; + } + axios.interceptors.request.use(function (config) { + config.headers.test = 'special get headers'; + return config; + }, null, { runWhen: onPostCall }); + + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders.test).toBeUndefined() + done(); + }); + }); + + it('does not run async interceptor if runWhen function is provided and resolves to false (and run synchronously)', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); + + function onPostCall(config) { + return config.method === 'post'; + } + axios.interceptors.request.use(function (config) { + config.headers.test = 'special get headers'; + return config; + }, null, { synchronous: false, runWhen: onPostCall }); + + axios.interceptors.request.use(function (config) { + config.headers.sync = 'hello world'; + return config; + }, null, { synchronous: true }); + + axios('/foo'); + + getAjaxRequest().then(function (request) { + expect(promiseResolveSpy).not.toHaveBeenCalled() + expect(request.requestHeaders.test).toBeUndefined() + expect(request.requestHeaders.sync).toBe('hello world') + done(); + }); + }); + + it('should add a request interceptor with an onRejected block that is called if interceptor code fails', function (done) { + var rejectedSpy = jasmine.createSpy('rejectedSpy'); + var error = new Error('deadly error'); + axios.interceptors.request.use(function () { + throw error; + }, function() { + rejectedSpy(error); + }, { synchronous: true }); + + axios('/foo'); + + getAjaxRequest().then(function () { + expect(rejectedSpy).toHaveBeenCalledWith(error); + done(); + }); + }); + it('should add a request interceptor that returns a new config object', function (done) { axios.interceptors.request.use(function () { return { @@ -237,6 +370,31 @@ describe('interceptors', function () { }); }); + it('should remove async interceptor before making request and execute synchronously', function (done) { + var promiseResolveSpy = spyOn(window.Promise, 'resolve').and.callThrough(); + var asyncIntercept = axios.interceptors.request.use(function (config) { + config.headers.async = 'async it!'; + return config; + }, null, { synchronous: false }); + + var syncIntercept = axios.interceptors.request.use(function (config) { + config.headers.sync = 'hello world'; + return config; + }, null, { synchronous: true }); + + + axios.interceptors.request.eject(asyncIntercept); + + axios('/foo') + + getAjaxRequest().then(function (request) { + expect(promiseResolveSpy).not.toHaveBeenCalled(); + expect(request.requestHeaders.async).toBeUndefined(); + expect(request.requestHeaders.sync).toBe('hello world'); + done() + }); + }); + it('should execute interceptors before transformers', function (done) { axios.interceptors.request.use(function (config) { config.data.baz = 'qux';