diff --git a/README.md b/README.md index c5ff569..1a72061 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ These are the available config options for making requests. Only the `url` is re // `transformRequest` allows changes to the request data before it is sent to the server // This is only applicable for request methods 'PUT', 'POST', and 'PATCH' - // The last function in the array must return a string or an ArrayBuffer + // The last function in the array must return a string, an ArrayBuffer, or a Stream transformRequest: [function (data) { // Do whatever you want to transform the data @@ -222,7 +222,7 @@ These are the available config options for making requests. Only the `url` is re // `data` is the data to be sent as the request body // Only applicable for request methods 'PUT', 'POST', and 'PATCH' - // When no `transformRequest` is set, must be a string, an ArrayBuffer or a hash + // When no `transformRequest` is set, must be a string, an ArrayBuffer, a hash, or a Stream data: { firstName: 'Fred' }, @@ -250,7 +250,7 @@ These are the available config options for making requests. Only the `url` is re } // `responseType` indicates the type of data that the server will respond with - // options are 'arraybuffer', 'blob', 'document', 'json', 'text' + // options are 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' responseType: 'json', // default // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 3c3970a..84be87b 100644 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -10,6 +10,13 @@ var zlib = require('zlib'); var pkg = require('./../../package.json'); var Buffer = require('buffer').Buffer; +// Resolve or reject the Promise based on the status +function settle(resolve, reject, response) { + (response.status >= 200 && response.status < 300 ? + resolve : + reject)(response); +} + /*eslint consistent-return:0*/ module.exports = function httpAdapter(resolve, reject, config) { var data = config.data; @@ -24,13 +31,13 @@ module.exports = function httpAdapter(resolve, reject, config) { headers['User-Agent'] = 'axios/' + pkg.version; } - if (data) { + 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(new Error('Data after transformation must be a string or an ArrayBuffer')); + return reject(new Error('Data after transformation must be a string, an ArrayBuffer, or a Stream')); } // Add Content-Length header if data exists @@ -93,39 +100,37 @@ module.exports = function httpAdapter(resolve, reject, config) { break; } - var responseBuffer = []; - stream.on('data', function handleStreamData(chunk) { - responseBuffer.push(chunk); + var response = { + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + config: config, + request: req + }; - // make sure the content length is not over the maxContentLength if specified - if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { - reject(new Error('maxContentLength size of ' + config.maxContentLength + ' exceeded')); - } - }); + if (config.responseType === 'stream') { + response.data = stream; + settle(resolve, reject, response); + } else { + var responseBuffer = []; + stream.on('data', function handleStreamData(chunk) { + responseBuffer.push(chunk); - stream.on('end', function handleStreamEnd() { - var d = Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - d = d.toString('utf8'); - } - var response = { - data: transformData( - d, - res.headers, - config.transformResponse - ), - status: res.statusCode, - statusText: res.statusMessage, - headers: res.headers, - config: config, - request: req - }; + // make sure the content length is not over the maxContentLength if specified + if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { + reject(new Error('maxContentLength size of ' + config.maxContentLength + ' exceeded')); + } + }); - // Resolve or reject the Promise based on the status - (res.statusCode >= 200 && res.statusCode < 300 ? - resolve : - reject)(response); - }); + stream.on('end', function handleStreamEnd() { + var responseData = Buffer.concat(responseBuffer); + if (config.responseType !== 'arraybuffer') { + responseData = responseData.toString('utf8'); + } + response.data = transformData(responseData, res.headers, config.transformResponse); + settle(resolve, reject, response); + }); + } }); // Handle errors @@ -147,5 +152,9 @@ module.exports = function httpAdapter(resolve, reject, config) { } // Send the request - req.end(data); + if (utils.isStream(data)) { + data.pipe(req); + } else { + req.end(data); + } }; diff --git a/lib/defaults.js b/lib/defaults.js index 45910c8..6bbfcbf 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -8,11 +8,8 @@ var DEFAULT_CONTENT_TYPE = { }; module.exports = { - transformRequest: [function transformRequestJSON(data, headers) { - if (utils.isFormData(data)) { - return data; - } - if (utils.isArrayBuffer(data)) { + transformRequest: [function transformRequest(data, headers) { + if (utils.isFormData(data) || utils.isArrayBuffer(data) || utils.isStream(data)) { return data; } if (utils.isArrayBufferView(data)) { @@ -36,7 +33,7 @@ module.exports = { return data; }], - transformResponse: [function transformResponseJSON(data) { + transformResponse: [function transformResponse(data) { /*eslint no-param-reassign:0*/ if (typeof data === 'string') { data = data.replace(PROTECTION_PREFIX, ''); diff --git a/lib/utils.js b/lib/utils.js index 0d418ae..33f2d4d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -122,6 +122,26 @@ function isBlob(val) { return toString.call(val) === '[object Blob]'; } +/** + * Determine if a value is a Function + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Function, otherwise false + */ +function isFunction(val) { + return toString.call(val) === '[object Function]'; +} + +/** + * Determine if a value is a Stream + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Stream, otherwise false + */ +function isStream(val) { + return isObject(val) && isFunction(val.pipe); +} + /** * Trim excess whitespace off the beginning and end of a string * @@ -237,6 +257,8 @@ module.exports = { isDate: isDate, isFile: isFile, isBlob: isBlob, + isFunction: isFunction, + isStream: isStream, isStandardBrowserEnv: isStandardBrowserEnv, forEach: forEach, merge: merge, diff --git a/test/specs/utils/isX.spec.js b/test/specs/utils/isX.spec.js index 88a0f2b..d58d113 100644 --- a/test/specs/utils/isX.spec.js +++ b/test/specs/utils/isX.spec.js @@ -1,4 +1,5 @@ var utils = require('../../../lib/utils'); +var Stream = require('stream'); describe('utils::isX', function () { it('should validate Array', function () { @@ -67,5 +68,14 @@ describe('utils::isX', function () { expect(utils.isDate(new Date())).toEqual(true); expect(utils.isDate(Date.now())).toEqual(false); }); -}); + it('should validate Function', function () { + expect(utils.isFunction(function () {})).toEqual(true); + expect(utils.isFunction('function')).toEqual(false); + }); + + it('should validate Stream', function () { + expect(utils.isStream(new Stream.Readable())).toEqual(true); + expect(utils.isStream({ foo: 'bar' })).toEqual(false); + }); +}); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index b7f2086..acd0b30 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -2,6 +2,7 @@ var axios = require('../../../index'); var http = require('http'); var url = require('url'); var zlib = require('zlib'); +var fs = require('fs'); var server; module.exports = { @@ -127,7 +128,7 @@ module.exports = { }); }); }, - + testMaxContentLength: function(test) { var str = Array(100000).join('ж'); @@ -145,7 +146,7 @@ module.exports = { error = res; failure = true; }); - + setTimeout(function () { test.equal(success, false, 'request should not succeed'); test.equal(failure, true, 'request should fail'); @@ -153,5 +154,26 @@ module.exports = { test.done(); }, 100); }); + }, + + testStream: function(test) { + server = http.createServer(function (req, res) { + req.pipe(res); + }).listen(4444, function () { + axios.post('http://localhost:4444/', + fs.createReadStream(__filename), { + responseType: 'stream' + }).then(function (res) { + var stream = res.data; + var string = ''; + stream.on('data', function (chunk) { + string += chunk.toString('utf8'); + }); + stream.on('end', function () { + test.equal(string, fs.readFileSync(__filename, 'utf8')); + test.done(); + }); + }); + }); } };