From 157efd5615890301824e3121cc6c9d2f9b21f94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Sat, 9 Jul 2016 21:30:16 +0200 Subject: [PATCH] Changing adapter signature to receive config and return promises --- README.md | 4 +- lib/adapters/README.md | 31 ++-- lib/adapters/http.js | 286 ++++++++++++++++++------------------ lib/adapters/xhr.js | 256 ++++++++++++++++---------------- lib/core/dispatchRequest.js | 52 +++---- test/specs/requests.spec.js | 29 ---- 6 files changed, 315 insertions(+), 343 deletions(-) diff --git a/README.md b/README.md index 5d80cfa1a8..e66db3c0d5 100644 --- a/README.md +++ b/README.md @@ -247,8 +247,8 @@ These are the available config options for making requests. Only the `url` is re withCredentials: false, // default // `adapter` allows custom handling of requests which makes testing easier. - // Call `resolve` or `reject` and supply a valid response (see [response docs](#response-api)). - adapter: function (resolve, reject, config) { + // Return a promise and supply a valid response (see [response docs](#response-api)). + adapter: function (config) { /* ... */ }, diff --git a/lib/adapters/README.md b/lib/adapters/README.md index d831a47d29..f389ccde05 100644 --- a/lib/adapters/README.md +++ b/lib/adapters/README.md @@ -1,13 +1,13 @@ # axios // adapters -The modules under `adapters/` are modules that handle dispatching a request and settling a `Promise` once a response is received. +The modules under `adapters/` are modules that handle dispatching a request and settling a returned `Promise` once a response is received. ## Example ```js var settle = require('./../core/settle'); -module.exports myAdapter(resolve, reject, config) { +module.exports myAdapter(config) { // At this point: // - config has been merged with defaults // - request transformers have already run @@ -15,20 +15,23 @@ module.exports myAdapter(resolve, reject, config) { // Make the request using config provided // Upon response settle the Promise + + return new Promise(function(resolve, reject) { - var response = { - data: responseData, - status: request.status, - statusText: request.statusText, - headers: responseHeaders, - config: config, - request: request - }; + var response = { + data: responseData, + status: request.status, + statusText: request.statusText, + headers: responseHeaders, + config: config, + request: request + }; - settle(resolve, reject, response); + settle(resolve, reject, response); - // From here: - // - response transformers will run - // - response interceptors will run + // From here: + // - response transformers will run + // - response interceptors will run + }); } ``` diff --git a/lib/adapters/http.js b/lib/adapters/http.js index d0589db3ea..959bed92f8 100644 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -15,158 +15,160 @@ var createError = require('../core/createError'); var enhanceError = require('../core/enhanceError'); /*eslint consistent-return:0*/ -module.exports = function httpAdapter(resolve, reject, config) { - var data = config.data; - var headers = config.headers; - var timer; - var aborted = false; - - // Set User-Agent (required by some servers) - // Only set header if it hasn't been set in config - // See https://github.com/mzabriskie/axios/issues/69 - if (!headers['User-Agent'] && !headers['user-agent']) { - headers['User-Agent'] = 'axios/' + pkg.version; - } - - if (data && !utils.isStream(data)) { - if (utils.isArrayBuffer(data)) { - data = new Buffer(new Uint8Array(data)); - } else if (utils.isString(data)) { - data = new Buffer(data, 'utf-8'); - } else { - return reject(createError( - 'Data after transformation must be a string, an ArrayBuffer, or a Stream', - config - )); +module.exports = function httpAdapter(config) { + return new Promise(function dispatchHttpRequest(resolve, reject) { + var data = config.data; + var headers = config.headers; + var timer; + var aborted = false; + + // Set User-Agent (required by some servers) + // Only set header if it hasn't been set in config + // See https://github.com/mzabriskie/axios/issues/69 + if (!headers['User-Agent'] && !headers['user-agent']) { + headers['User-Agent'] = 'axios/' + pkg.version; } - // Add Content-Length header if data exists - headers['Content-Length'] = data.length; - } - - // HTTP basic authentication - var auth = undefined; - if (config.auth) { - var username = config.auth.username || ''; - var password = config.auth.password || ''; - auth = username + ':' + password; - } - - // Parse url - var parsed = url.parse(config.url); - if (!auth && parsed.auth) { - var urlAuth = parsed.auth.split(':'); - var urlUsername = urlAuth[0] || ''; - var urlPassword = urlAuth[1] || ''; - auth = urlUsername + ':' + urlPassword; - } - var options = { - hostname: parsed.hostname, - port: parsed.port, - path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), - method: config.method, - headers: headers, - agent: config.agent, - auth: auth - }; - - if (config.proxy) { - options.host = config.proxy.host; - options.port = config.proxy.port; - options.path = parsed.protocol + '//' + parsed.hostname + options.path; - } - - var transport; - if (config.maxRedirects === 0) { - transport = parsed.protocol === 'https:' ? https : http; - } else { - if (config.maxRedirects) { - options.maxRedirects = config.maxRedirects; + if (data && !utils.isStream(data)) { + if (utils.isArrayBuffer(data)) { + data = new Buffer(new Uint8Array(data)); + } else if (utils.isString(data)) { + data = new Buffer(data, 'utf-8'); + } else { + return reject(createError( + 'Data after transformation must be a string, an ArrayBuffer, or a Stream', + config + )); + } + + // Add Content-Length header if data exists + headers['Content-Length'] = data.length; } - transport = parsed.protocol === 'https:' ? httpsFollow : httpFollow; - } - - // Create the request - var req = transport.request(options, function handleResponse(res) { - if (aborted) return; - - // Response has been received so kill timer that handles request timeout - clearTimeout(timer); - timer = null; - - // uncompress the response body transparently if required - var stream = res; - switch (res.headers['content-encoding']) { - /*eslint default-case:0*/ - case 'gzip': - case 'compress': - case 'deflate': - // add the unzipper to the body stream processing pipeline - stream = stream.pipe(zlib.createUnzip()); - - // remove the content-encoding in order to not confuse downstream operations - delete res.headers['content-encoding']; - break; + + // HTTP basic authentication + var auth = undefined; + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password || ''; + auth = username + ':' + password; } - var response = { - status: res.statusCode, - statusText: res.statusMessage, - headers: res.headers, - config: config, - request: req + // Parse url + var parsed = url.parse(config.url); + if (!auth && parsed.auth) { + var urlAuth = parsed.auth.split(':'); + var urlUsername = urlAuth[0] || ''; + var urlPassword = urlAuth[1] || ''; + auth = urlUsername + ':' + urlPassword; + } + var options = { + hostname: parsed.hostname, + port: parsed.port, + path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), + method: config.method, + headers: headers, + agent: config.agent, + auth: auth }; - if (config.responseType === 'stream') { - response.data = stream; - settle(resolve, reject, response); + if (config.proxy) { + options.host = config.proxy.host; + options.port = config.proxy.port; + options.path = parsed.protocol + '//' + parsed.hostname + options.path; + } + + var transport; + if (config.maxRedirects === 0) { + transport = parsed.protocol === 'https:' ? https : http; } else { - var responseBuffer = []; - stream.on('data', function handleStreamData(chunk) { - responseBuffer.push(chunk); - - // make sure the content length is not over the maxContentLength if specified - if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { - reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', config)); - } - }); - - stream.on('error', function handleStreamError(err) { - if (aborted) return; - reject(enhanceError(err, config)); - }); - - stream.on('end', function handleStreamEnd() { - var responseData = Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - responseData = responseData.toString('utf8'); - } - - response.data = responseData; + if (config.maxRedirects) { + options.maxRedirects = config.maxRedirects; + } + transport = parsed.protocol === 'https:' ? httpsFollow : httpFollow; + } + + // Create the request + var req = transport.request(options, function handleResponse(res) { + if (aborted) return; + + // Response has been received so kill timer that handles request timeout + clearTimeout(timer); + timer = null; + + // uncompress the response body transparently if required + var stream = res; + switch (res.headers['content-encoding']) { + /*eslint default-case:0*/ + case 'gzip': + case 'compress': + case 'deflate': + // add the unzipper to the body stream processing pipeline + stream = stream.pipe(zlib.createUnzip()); + + // remove the content-encoding in order to not confuse downstream operations + delete res.headers['content-encoding']; + break; + } + + var response = { + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + config: config, + request: req + }; + + if (config.responseType === 'stream') { + response.data = stream; settle(resolve, reject, response); - }); + } else { + var responseBuffer = []; + stream.on('data', function handleStreamData(chunk) { + responseBuffer.push(chunk); + + // make sure the content length is not over the maxContentLength if specified + if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { + reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', config)); + } + }); + + stream.on('error', function handleStreamError(err) { + if (aborted) return; + reject(enhanceError(err, config)); + }); + + stream.on('end', function handleStreamEnd() { + var responseData = Buffer.concat(responseBuffer); + if (config.responseType !== 'arraybuffer') { + responseData = responseData.toString('utf8'); + } + + response.data = responseData; + settle(resolve, reject, response); + }); + } + }); + + // Handle errors + req.on('error', function handleRequestError(err) { + if (aborted) return; + reject(enhanceError(err, config)); + }); + + // Handle request timeout + if (config.timeout && !timer) { + timer = setTimeout(function handleRequestTimeout() { + req.abort(); + reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED')); + aborted = true; + }, config.timeout); } - }); - // Handle errors - req.on('error', function handleRequestError(err) { - if (aborted) return; - reject(enhanceError(err, config)); + // Send the request + if (utils.isStream(data)) { + data.pipe(req); + } else { + req.end(data); + } }); - - // Handle request timeout - if (config.timeout && !timer) { - timer = setTimeout(function handleRequestTimeout() { - req.abort(); - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED')); - aborted = true; - }, config.timeout); - } - - // Send the request - if (utils.isStream(data)) { - data.pipe(req); - } else { - req.end(data); - } }; diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index 22dd06f1a0..957eac57e6 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -8,151 +8,153 @@ var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); var createError = require('../core/createError'); var btoa = (typeof window !== 'undefined' && window.btoa) || require('./../helpers/btoa'); -module.exports = function xhrAdapter(resolve, reject, config) { - var requestData = config.data; - var requestHeaders = config.headers; - - if (utils.isFormData(requestData)) { - delete requestHeaders['Content-Type']; // Let the browser set it - } - - var request = new XMLHttpRequest(); - var loadEvent = 'onreadystatechange'; - var xDomain = false; - - // For IE 8/9 CORS support - // Only supports POST and GET calls and doesn't returns the response headers. - // DON'T do this for testing b/c XMLHttpRequest is mocked, not XDomainRequest. - if (process.env.NODE_ENV !== 'test' && - typeof window !== 'undefined' && - window.XDomainRequest && !('withCredentials' in request) && - !isURLSameOrigin(config.url)) { - request = new window.XDomainRequest(); - loadEvent = 'onload'; - xDomain = true; - request.onprogress = function handleProgress() {}; - request.ontimeout = function handleTimeout() {}; - } - - // HTTP basic authentication - if (config.auth) { - var username = config.auth.username || ''; - var password = config.auth.password || ''; - requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); - } - - request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true); - - // Set the request timeout in MS - request.timeout = config.timeout; - - // Listen for ready state - request[loadEvent] = function handleLoad() { - if (!request || (request.readyState !== 4 && !xDomain)) { - return; +module.exports = function xhrAdapter(config) { + return new Promise(function dispatchXhrRequest(resolve, reject) { + var requestData = config.data; + var requestHeaders = config.headers; + + if (utils.isFormData(requestData)) { + delete requestHeaders['Content-Type']; // Let the browser set it } - // The request errored out and we didn't get a response, this will be - // handled by onerror instead - if (request.status === 0) { - return; + var request = new XMLHttpRequest(); + var loadEvent = 'onreadystatechange'; + var xDomain = false; + + // For IE 8/9 CORS support + // Only supports POST and GET calls and doesn't returns the response headers. + // DON'T do this for testing b/c XMLHttpRequest is mocked, not XDomainRequest. + if (process.env.NODE_ENV !== 'test' && + typeof window !== 'undefined' && + window.XDomainRequest && !('withCredentials' in request) && + !isURLSameOrigin(config.url)) { + request = new window.XDomainRequest(); + loadEvent = 'onload'; + xDomain = true; + request.onprogress = function handleProgress() {}; + request.ontimeout = function handleTimeout() {}; } - // Prepare the response - var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; - var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; - var response = { - data: responseData, - // IE sends 1223 instead of 204 (https://github.com/mzabriskie/axios/issues/201) - status: request.status === 1223 ? 204 : request.status, - statusText: request.status === 1223 ? 'No Content' : request.statusText, - headers: responseHeaders, - config: config, - request: request - }; + // HTTP basic authentication + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password || ''; + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + } + + request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true); + + // Set the request timeout in MS + request.timeout = config.timeout; - settle(resolve, reject, response); + // Listen for ready state + request[loadEvent] = function handleLoad() { + if (!request || (request.readyState !== 4 && !xDomain)) { + return; + } - // Clean up request - request = null; - }; + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + if (request.status === 0) { + return; + } - // Handle low level network errors - request.onerror = function handleError() { - // Real errors are hidden from us by the browser - // onerror should only fire if it's a network error - reject(createError('Network Error', config)); + // Prepare the response + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; + var response = { + data: responseData, + // IE sends 1223 instead of 204 (https://github.com/mzabriskie/axios/issues/201) + status: request.status === 1223 ? 204 : request.status, + statusText: request.status === 1223 ? 'No Content' : request.statusText, + headers: responseHeaders, + config: config, + request: request + }; + + settle(resolve, reject, response); + + // Clean up request + request = null; + }; - // Clean up request - request = null; - }; + // Handle low level network errors + request.onerror = function handleError() { + // Real errors are hidden from us by the browser + // onerror should only fire if it's a network error + reject(createError('Network Error', config)); - // Handle timeout - request.ontimeout = function handleTimeout() { - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED')); + // Clean up request + request = null; + }; - // Clean up request - request = null; - }; + // Handle timeout + request.ontimeout = function handleTimeout() { + reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED')); - // Add xsrf header - // This is only done if running in a standard browser environment. - // Specifically not if we're in a web worker, or react-native. - if (utils.isStandardBrowserEnv()) { - var cookies = require('./../helpers/cookies'); + // Clean up request + request = null; + }; // Add xsrf header - var xsrfValue = config.withCredentials || isURLSameOrigin(config.url) ? - cookies.read(config.xsrfCookieName) : - undefined; + // This is only done if running in a standard browser environment. + // Specifically not if we're in a web worker, or react-native. + if (utils.isStandardBrowserEnv()) { + var cookies = require('./../helpers/cookies'); + + // Add xsrf header + var xsrfValue = config.withCredentials || isURLSameOrigin(config.url) ? + cookies.read(config.xsrfCookieName) : + undefined; + + if (xsrfValue) { + requestHeaders[config.xsrfHeaderName] = xsrfValue; + } + } - if (xsrfValue) { - requestHeaders[config.xsrfHeaderName] = xsrfValue; + // Add headers to the request + if ('setRequestHeader' in request) { + utils.forEach(requestHeaders, function setRequestHeader(val, key) { + if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { + // Remove Content-Type if data is undefined + delete requestHeaders[key]; + } else { + // Otherwise add header to the request + request.setRequestHeader(key, val); + } + }); } - } - - // Add headers to the request - if ('setRequestHeader' in request) { - utils.forEach(requestHeaders, function setRequestHeader(val, key) { - if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { - // Remove Content-Type if data is undefined - delete requestHeaders[key]; - } else { - // Otherwise add header to the request - request.setRequestHeader(key, val); - } - }); - } - - // Add withCredentials to request if needed - if (config.withCredentials) { - request.withCredentials = true; - } - - // Add responseType to request if needed - if (config.responseType) { - try { - request.responseType = config.responseType; - } catch (e) { - if (request.responseType !== 'json') { - throw e; + + // Add withCredentials to request if needed + if (config.withCredentials) { + request.withCredentials = true; + } + + // Add responseType to request if needed + if (config.responseType) { + try { + request.responseType = config.responseType; + } catch (e) { + if (request.responseType !== 'json') { + throw e; + } } } - } - - // Handle progress if needed - if (typeof config.progress === 'function') { - if (config.method === 'post' || config.method === 'put') { - request.upload.addEventListener('progress', config.progress); - } else if (config.method === 'get') { - request.addEventListener('progress', config.progress); + + // Handle progress if needed + if (typeof config.progress === 'function') { + if (config.method === 'post' || config.method === 'put') { + request.upload.addEventListener('progress', config.progress); + } else if (config.method === 'get') { + request.addEventListener('progress', config.progress); + } } - } - if (requestData === undefined) { - requestData = null; - } + if (requestData === undefined) { + requestData = null; + } - // Send the request - request.send(requestData); + // Send the request + request.send(requestData); + }); }; diff --git a/lib/core/dispatchRequest.js b/lib/core/dispatchRequest.js index 7bbb45df1a..099f2c792c 100644 --- a/lib/core/dispatchRequest.js +++ b/lib/core/dispatchRequest.js @@ -2,7 +2,6 @@ var utils = require('./../utils'); var transformData = require('./transformData'); -var enhanceError = require('./enhanceError'); /** * Dispatch a request to the server using whichever adapter @@ -36,35 +35,30 @@ module.exports = function dispatchRequest(config) { } ); - return new Promise(function executor(resolve, reject) { - try { - var adapter; + var adapter; - if (typeof config.adapter === 'function') { - // For custom adapter support - adapter = config.adapter; - } else if (typeof XMLHttpRequest !== 'undefined') { - // For browsers use XHR adapter - adapter = require('../adapters/xhr'); - } else if (typeof process !== 'undefined') { - // For node use HTTP adapter - adapter = require('../adapters/http'); - } + if (typeof config.adapter === 'function') { + // For custom adapter support + adapter = config.adapter; + } else if (typeof XMLHttpRequest !== 'undefined') { + // For browsers use XHR adapter + adapter = require('../adapters/xhr'); + } else if (typeof process !== 'undefined') { + // For node use HTTP adapter + adapter = require('../adapters/http'); + } - if (typeof adapter === 'function') { - adapter(resolve, reject, config); - } - } catch (e) { - reject(enhanceError(e, config)); - } - }).then(function onFulfilled(response) { - // Transform response data - response.data = transformData( - response.data, - response.headers, - config.transformResponse - ); + return Promise.resolve(config) + // Wrap synchronous adapter errors and pass configuration + .then(adapter) + .then(function onFulfilled(response) { + // Transform response data + response.data = transformData( + response.data, + response.headers, + config.transformResponse + ); - return response; - }); + return response; + }); }; diff --git a/test/specs/requests.spec.js b/test/specs/requests.spec.js index 6e9745985e..0a81d16f0a 100644 --- a/test/specs/requests.spec.js +++ b/test/specs/requests.spec.js @@ -36,35 +36,6 @@ describe('requests', function () { }); }); - it('should reject on adapter errors', function (done) { - // disable jasmine.Ajax since we're hitting a non-existant server anyway - jasmine.Ajax.uninstall(); - - var resolveSpy = jasmine.createSpy('resolve'); - var rejectSpy = jasmine.createSpy('reject'); - - var adapterError = new Error('adapter error'); - var adapterThatFails = function () { - throw adapterError; - }; - - var finish = function () { - expect(resolveSpy).not.toHaveBeenCalled(); - expect(rejectSpy).toHaveBeenCalled(); - var reason = rejectSpy.calls.first().args[0]; - expect(reason).toBe(adapterError); - expect(reason.config.method).toBe('get'); - expect(reason.config.url).toBe('/foo'); - - done(); - }; - - axios('/foo', { - adapter: adapterThatFails - }).then(resolveSpy, rejectSpy) - .then(finish, finish); - }); - it('should reject on network errors', function (done) { // disable jasmine.Ajax since we're hitting a non-existant server anyway jasmine.Ajax.uninstall();