From a238917aa2965b7addb46ec6f100f208cf14ff37 Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Fri, 29 Apr 2022 15:16:11 +0300 Subject: [PATCH 1/2] Draft --- .eslintrc.js | 6 +- lib/adapters/http.js | 33 +-- lib/adapters/xhr.js | 27 +- lib/core/AxiosHeaders.js | 309 +++++++++++++++++++++ lib/helpers/parseHeaders.js | 6 +- lib/utils.js | 83 +++++- test/specs/helpers/parseHeaders.spec.js | 43 --- test/unit/adapters/http.js | 2 +- test/unit/core/AxiosHeaders.js | 352 ++++++++++++++++++++++++ 9 files changed, 773 insertions(+), 88 deletions(-) create mode 100644 lib/core/AxiosHeaders.js create mode 100644 test/unit/core/AxiosHeaders.js diff --git a/.eslintrc.js b/.eslintrc.js index d3bfa24c05..cfb3c4453e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,7 +54,7 @@ module.exports = { /** * Best practices */ - 'consistent-return': 2, // http://eslint.org/docs/rules/consistent-return + 'consistent-return': 0, // http://eslint.org/docs/rules/consistent-return 'curly': [2, 'multi-line'], // http://eslint.org/docs/rules/curly 'default-case': 2, // http://eslint.org/docs/rules/default-case 'dot-notation': [2, { // http://eslint.org/docs/rules/dot-notation @@ -114,7 +114,9 @@ module.exports = { }], 'comma-style': [2, 'last'], // http://eslint.org/docs/rules/comma-style 'eol-last': 2, // http://eslint.org/docs/rules/eol-last - 'func-names': 1, // http://eslint.org/docs/rules/func-names + 'func-names': [ + 1, 'as-needed' + ], // http://eslint.org/docs/rules/func-names 'key-spacing': [2, { // http://eslint.org/docs/rules/key-spacing 'beforeColon': false, 'afterColon': true diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 04a9aacf84..649b698e47 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -14,6 +14,7 @@ var VERSION = require('./../env/data').version; var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); +var AxiosHeaders = require('../core/AxiosHeaders'); var isHttps = /https:?/; @@ -68,29 +69,17 @@ module.exports = function httpAdapter(config) { rejectPromise(value); }; var data = config.data; - var headers = config.headers; - var headerNames = {}; - - Object.keys(headers).forEach(function storeLowerName(name) { - headerNames[name.toLowerCase()] = name; - }); + var headers = new AxiosHeaders(config.headers); // Set User-Agent (required by some servers) // See https://github.com/axios/axios/issues/69 - if ('user-agent' in headerNames) { - // User-Agent is specified; handle case where no UA header is desired - if (!headers[headerNames['user-agent']]) { - delete headers[headerNames['user-agent']]; - } - // Otherwise, use specified value - } else { - // Only set header if it hasn't been set in config - headers['User-Agent'] = 'axios/' + VERSION; - } + // User-Agent is specified; handle case where no UA header is desired + // Only set header if it hasn't been set in config + headers.set('User-Agent', 'axios/' + VERSION, false); // support for https://www.npmjs.com/package/form-data api if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { - Object.assign(headers, data.getHeaders()); + headers.set(data.getHeaders()); } else if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) { // Nothing to do... @@ -115,9 +104,7 @@ module.exports = function httpAdapter(config) { } // Add Content-Length header if data exists - if (!headerNames['content-length']) { - headers['Content-Length'] = data.length; - } + headers.set('Content-Length', data.length, false); } // HTTP basic authentication @@ -148,9 +135,7 @@ module.exports = function httpAdapter(config) { auth = urlUsername + ':' + urlPassword; } - if (auth && headerNames.authorization) { - delete headers[headerNames.authorization]; - } + auth && headers.delete('authorization'); var isHttpsRequest = isHttps.test(protocol); var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; @@ -168,7 +153,7 @@ module.exports = function httpAdapter(config) { var options = { path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), method: config.method.toUpperCase(), - headers: headers, + headers: headers.toJSON(), agent: agent, agents: { http: config.httpAgent, https: config.httpsAgent }, auth: auth diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index 76d7e7ac1d..1dcd683039 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -5,17 +5,17 @@ var settle = require('./../core/settle'); var cookies = require('./../helpers/cookies'); var buildURL = require('./../helpers/buildURL'); var buildFullPath = require('../core/buildFullPath'); -var parseHeaders = require('./../helpers/parseHeaders'); var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); var parseProtocol = require('../helpers/parseProtocol'); +var AxiosHeaders = require('../core/AxiosHeaders'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; - var requestHeaders = config.headers; + var requestHeaders = new AxiosHeaders(config.headers); var responseType = config.responseType; var onCanceled; function done() { @@ -29,7 +29,7 @@ module.exports = function xhrAdapter(config) { } if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) { - delete requestHeaders['Content-Type']; // Let the browser set it + requestHeaders.delete('Content-Type'); // Let the browser set it } var request = new XMLHttpRequest(); @@ -38,7 +38,7 @@ module.exports = function xhrAdapter(config) { if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; - requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password)); } var fullPath = buildFullPath(config.baseURL, config.url); @@ -53,7 +53,9 @@ module.exports = function xhrAdapter(config) { return; } // Prepare the response - var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseHeaders = 'getAllResponseHeaders' in request ? + AxiosHeaders.parse(request.getAllResponseHeaders(), true) : null; + var responseData = !responseType || responseType === 'text' || responseType === 'json' ? request.responseText : request.response; var response = { @@ -149,20 +151,17 @@ module.exports = function xhrAdapter(config) { undefined; if (xsrfValue) { - requestHeaders[config.xsrfHeaderName] = xsrfValue; + requestHeaders.set(config.xsrfHeaderName, xsrfValue); } } + // Remove Content-Type if data is undefined + + requestData === undefined && requestHeaders.delete('content-type'); // 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); - } + utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) { + request.setRequestHeader(key, val); }); } diff --git a/lib/core/AxiosHeaders.js b/lib/core/AxiosHeaders.js new file mode 100644 index 0000000000..8f9d1dfedb --- /dev/null +++ b/lib/core/AxiosHeaders.js @@ -0,0 +1,309 @@ +'use strict'; + +var utils = require('../utils'); + +var $internals = Symbol('internals'); + +var hasOwnProperty = utils.hasOwnProperty; + +function tokensParser(str) { + var tokens = Object.create(null); + var tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g; + var match; + + while ((match = tokensRE.exec(str))) { + tokens[match[1]] = match[2]; + } + + return tokens; +} + +function normalizeHeaderCase(header) { + return header && String(header).trim().toLowerCase(); +} + +function filterHeaderValue(value, filter, header, headers) { + if (utils.isFunction(filter)) { + return filter.call(this, value, header, headers); + } + + if (value === false) return; + + if (utils.isString(filter)) { + return value.indexOf(filter) !== -1; + } + + if (utils.isRegExp(filter)) { + return filter.test(value); + } +} + +function normalizeName(header) { + return header + .toLowerCase().replace(/([a-z\d])(\w*)/g, function(w, char, str) { + return char.toUpperCase() + str; + }); +} + +function AxiosHeaders(headers) { + this[$internals] = { + headers: {}, + names: {}, + accessors: {} + }; + + headers && this.set(headers); +} + +Object.assign(AxiosHeaders.prototype, { + set: function(header, valueOrRewrite, rewrite) { + var internals = this[$internals]; + var headers = internals.headers; + var names = internals.names; + + function setHeader(value, header, _rewrite) { + var lHeader = normalizeHeaderCase(header); + + if (!lHeader) { + throw new Error('header name can not be an empty string'); + } + + var has; + + if ( + hasOwnProperty(headers, lHeader) && + (_rewrite === false || (_rewrite === undefined && headers[lHeader] === false)) + ) { + return; + } + + if (value === null) { + has && this.delete(lHeader); + return; + } + + if (value !== false && !utils.isArray(value)) { + value = String(value).trim(); + } + + headers[lHeader] = value; + + if (!hasOwnProperty(names, lHeader)) { + names[lHeader] = header; + } + } + + if (utils.isString(header)) { + setHeader(valueOrRewrite, header, rewrite); + } else if (utils.isPlainObject(header)) { + utils.forEach(header, function(value, header) { + setHeader(value, header, valueOrRewrite); + }); + } else { + throw new TypeError('Header must be a string or an plain object'); + } + + return this; + }, + + get: function(header, parser) { + var internals = this[$internals]; + var headers = internals.headers; + header = normalizeHeaderCase(header); + + if (!hasOwnProperty(headers, header)) { + return undefined; + } + + var value = headers[header]; + + if (!parser || value === false) { + return value; + } + + if (parser === true) { + return tokensParser(value); + } + + if (utils.isFunction(parser)) { + return parser.call(this, value, header, headers) ? value : null; + } + + if (utils.isRegExp(parser)) { + return parser.exec(value); + } + + throw TypeError('parser must be boolean|regexp|function'); + }, + + has: function(header, filter) { + var headers = this[$internals].headers; + header = normalizeHeaderCase(header); + + return !!(header && hasOwnProperty(headers, header) && + (!filter || filterHeaderValue.call(this, headers[header], filter, header, headers))); + }, + + delete: function(header, filter) { + var self = this; + var internals = self[$internals]; + var headers = internals.headers; + var deleted = false; + + function deleteHeader(header) { + header = normalizeHeaderCase(header); + + if (header && hasOwnProperty(headers, header) && + (!filter || filterHeaderValue.call(self, headers[header], filter, header, headers))) { + delete headers[header]; + delete internals.names[header]; + deleted = true; + return true; + } + + return false; + } + + if (utils.isArray(header)) { + header.forEach(deleteHeader); + return deleted; + } + + return deleteHeader(header); + }, + + accessor: function(header) { + var self = this; + var internals = self[$internals]; + var accessors = internals.accessors; + + function defineAccessor(_header) { + var lHeader = normalizeHeaderCase(_header); + + if (!accessors[lHeader]) { + var names = internals.names; + var accessorName = utils.toCamelCase(' ' + _header); + + Object.defineProperty(self, 'get' + accessorName, { + value: function(parser) { + return this.get(lHeader, parser); + }, + configurable: true + }); + + Object.defineProperty(self, 'set' + accessorName, { + value: function(value, rewrite) { + return this.set(lHeader, value, rewrite); + }, + configurable: true + }); + + accessors[lHeader] = true; + + if (!hasOwnProperty(names, lHeader)) { + names[lHeader] = _header; + } + } + } + + if (header == null) { + header = internals.names; + } + + if (utils.isPlainObject(header)) { + utils.forEach(header, defineAccessor); + } else { + defineAccessor(header); + } + + return this; + }, + + normalize: function(formatter) { + var internals = this[$internals]; + + if (formatter == null) { + formatter = normalizeName; + } + + utils.forEach(internals.headers, function(value, header) { + internals.names[header] = formatter.call(this, header, value); + }); + + return this; + }, + + toJSON: function(asStringValues) { + var internals = this[$internals]; + var names = internals.names; + var obj = Object.create(null); + + utils.forEach(internals.headers, function(value, header) { + value !== false && (obj[names[header] || header] = asStringValues !== false && + utils.isArray(value) ? value.join(', ') : value); + }); + + return obj; + } +}); + +// AxiosHeaders whose duplicates are ignored by node +// c.f. https://nodejs.org/api/http.html#http_message_headers +var ignoreDuplicateOf = utils.toObjectSet([ + 'age', 'authorization', 'content-length', 'content-type', 'etag', + 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', + 'last-modified', 'location', 'max-forwards', 'proxy-authorization', + 'referer', 'retry-after', 'user-agent' +]); + +Object.assign(AxiosHeaders, { + /** + * Parse rawHeaders into an object + * + * ``` + * Date: Wed, 27 Aug 2014 08:58:49 GMT + * Content-Type: application/json + * Connection: keep-alive + * Transfer-Encoding: chunked + * ``` + * + * @param {String} rawHeaders AxiosHeaders needing to be parsed + * @param {Boolean} asPlainObject return a plain object instead of AxiosHeader instance + * @returns {Object} AxiosHeaders parsed into an object + */ + parse: function(rawHeaders, asPlainObject) { + var parsed = {}; + var key; + var val; + var i; + + if (!rawHeaders) { return asPlainObject ? parsed : new this(); } + + rawHeaders.split('\n').forEach(function parser(line) { + i = line.indexOf(':'); + key = line.substring(0, i).trim().toLowerCase(); + val = line.substring(i + 1).trim(); + + if (!key || (parsed[key] && ignoreDuplicateOf[key])) { + return; + } + + if (key === 'set-cookie') { + if (parsed[key]) { + parsed[key].push(val); + } else { + parsed[key] = [val]; + } + } else { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + }); + + return asPlainObject ? parsed : new this(parsed); + } +}); + +utils.freezeMethods(AxiosHeaders.prototype); +utils.freezeMethods(AxiosHeaders); + +module.exports = AxiosHeaders; diff --git a/lib/helpers/parseHeaders.js b/lib/helpers/parseHeaders.js index 8af2cc7f1b..68368c295c 100644 --- a/lib/helpers/parseHeaders.js +++ b/lib/helpers/parseHeaders.js @@ -2,7 +2,7 @@ var utils = require('./../utils'); -// Headers whose duplicates are ignored by node +// AxiosHeaders whose duplicates are ignored by node // c.f. https://nodejs.org/api/http.html#http_message_headers var ignoreDuplicateOf = [ 'age', 'authorization', 'content-length', 'content-type', 'etag', @@ -21,8 +21,8 @@ var ignoreDuplicateOf = [ * Transfer-Encoding: chunked * ``` * - * @param {String} headers Headers needing to be parsed - * @returns {Object} Headers parsed into an object + * @param {String} headers AxiosHeaders needing to be parsed + * @returns {Object} AxiosHeaders parsed into an object */ module.exports = function parseHeaders(headers) { var parsed = {}; diff --git a/lib/utils.js b/lib/utils.js index 18de154541..69f0cbe587 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -436,6 +436,81 @@ var isTypedArray = (function(TypedArray) { }; })(typeof Uint8Array !== 'undefined' && Object.getPrototypeOf(Uint8Array)); +var toCamelCase = function(str) { + return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g, + function replacer(m, p1, p2) { + return p1.toUpperCase() + p2; + } + ); +}; + +var hasOwnProperty = (function resolver(_hasOwnProperty) { + return function hasOwnProperty(obj, prop) { + return _hasOwnProperty.call(obj, prop); + }; +})(Object.prototype.hasOwnProperty); + +/** + * Determine if a value is a RegExp object + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a RegExp object, otherwise false + */ +var isRegExp = kindOfTest('RegExp'); + +function reduceDescriptors(obj, reducer) { + var descriptors = Object.getOwnPropertyDescriptors(obj); + var reducedDescriptors = {}; + + forEach(descriptors, function(descriptor, name) { + if (reducer(descriptor, name, obj) !== false) { + reducedDescriptors[name] = descriptor; + } + }); + + Object.defineProperties(obj, reducedDescriptors); +} + +/** + * Makes all methods read-only + * @param {Object} obj + */ + +function freezeMethods(obj) { + reduceDescriptors(obj, function(descriptor, name) { + var value = obj[name]; + + if (!isFunction(value)) return; + + descriptor.enumerable = false; + + if ('writable' in descriptor) { + descriptor.writable = false; + return; + } + + if (!descriptor.set) { + descriptor.set = function() { + throw Error('Can not read-only method \'' + name + '\''); + }; + } + }); +} + +function toObjectSet(arrayOrString, delimiter) { + var obj = {}; + + function define(arr) { + arr.forEach(function(value) { + obj[value] = true; + }); + } + + isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter)); + + return obj; +} + module.exports = { isArray: isArray, isArrayBuffer: isArrayBuffer, @@ -450,6 +525,7 @@ module.exports = { isDate: isDate, isFile: isFile, isBlob: isBlob, + isRegExp: isRegExp, isFunction: isFunction, isStream: isStream, isURLSearchParams: isURLSearchParams, @@ -466,5 +542,10 @@ module.exports = { endsWith: endsWith, toArray: toArray, isTypedArray: isTypedArray, - isFileList: isFileList + isFileList: isFileList, + toCamelCase: toCamelCase, + hasOwnProperty: hasOwnProperty, + reduceDescriptors: reduceDescriptors, + freezeMethods: freezeMethods, + toObjectSet: toObjectSet }; diff --git a/test/specs/helpers/parseHeaders.spec.js b/test/specs/helpers/parseHeaders.spec.js index 254d1d61ef..e425fca8b7 100644 --- a/test/specs/helpers/parseHeaders.spec.js +++ b/test/specs/helpers/parseHeaders.spec.js @@ -1,45 +1,2 @@ var parseHeaders = require('../../../lib/helpers/parseHeaders'); -describe('helpers::parseHeaders', function () { - it('should parse headers', function () { - var date = new Date(); - var parsed = parseHeaders( - 'Date: ' + date.toISOString() + '\n' + - 'Content-Type: application/json\n' + - 'Connection: keep-alive\n' + - 'Transfer-Encoding: chunked' - ); - - expect(parsed['date']).toEqual(date.toISOString()); - expect(parsed['content-type']).toEqual('application/json'); - expect(parsed['connection']).toEqual('keep-alive'); - expect(parsed['transfer-encoding']).toEqual('chunked'); - }); - - it('should use array for set-cookie', function() { - var parsedZero = parseHeaders(''); - var parsedSingle = parseHeaders( - 'Set-Cookie: key=val;' - ); - var parsedMulti = parseHeaders( - 'Set-Cookie: key=val;\n' + - 'Set-Cookie: key2=val2;\n' - ); - - expect(parsedZero['set-cookie']).toBeUndefined(); - expect(parsedSingle['set-cookie']).toEqual(['key=val;']); - expect(parsedMulti['set-cookie']).toEqual(['key=val;', 'key2=val2;']); - }); - - it('should handle duplicates', function() { - var parsed = parseHeaders( - 'Age: age-a\n' + // age is in ignore duplicates blocklist - 'Age: age-b\n' + - 'Foo: foo-a\n' + - 'Foo: foo-b\n' - ); - - expect(parsed['age']).toEqual('age-a'); - expect(parsed['foo']).toEqual('foo-a, foo-b'); - }); -}); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 5d757a65be..f918e48b25 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -1145,7 +1145,7 @@ describe('supports http with nodejs', function () { }).listen(4444, function () { axios.get('http://localhost:4444/', { headers: { - "User-Agent": null + "User-Agent": false } } ).then(function (res) { diff --git a/test/unit/core/AxiosHeaders.js b/test/unit/core/AxiosHeaders.js new file mode 100644 index 0000000000..327459849b --- /dev/null +++ b/test/unit/core/AxiosHeaders.js @@ -0,0 +1,352 @@ +var AxiosHeaders = require('../../../lib/core/AxiosHeaders'); +var assert = require('assert'); + + +describe('AxiosHeaders', function () { + it('should support headers argument', function () { + var headers = new AxiosHeaders({ + x: 1, + y: 2 + }); + + assert.strictEqual(headers.get('x'), '1'); + assert.strictEqual(headers.get('y'), '2'); + }) + + + describe('set', function () { + it('should support adding a single header', function(){ + var headers = new AxiosHeaders(); + + headers.set('foo', 'bar'); + + assert.strictEqual(headers.get('foo'), 'bar'); + }) + + it('should support adding multiple headers', function(){ + var headers = new AxiosHeaders(); + + headers.set({ + foo: 'value1', + bar: 'value2', + }); + + assert.strictEqual(headers.get('foo'), 'value1'); + assert.strictEqual(headers.get('bar'), 'value2'); + }) + + it('should rewrite header only if rewrite option is set to true or undefined', function(){ + var headers = new AxiosHeaders(); + + headers.set('foo', 'value1'); + + headers.set('foo', 'value2', false); + + assert.strictEqual(headers.get('foo'), 'value1'); + + headers.set('foo', 'value2'); + + assert.strictEqual(headers.get('foo'), 'value2'); + + headers.set('foo', 'value3', true); + + assert.strictEqual(headers.get('foo'), 'value3'); + }); + + it('should not rewrite the header if its value is false, unless rewrite options is set to true', function(){ + var headers = new AxiosHeaders(); + + headers.set('foo', false); + headers.set('foo', 'value2'); + + assert.strictEqual(headers.get('foo'), false); + + headers.set('foo', 'value2', true); + + assert.strictEqual(headers.get('foo'), 'value2'); + }) + }); + + describe('get', function () { + describe('filter', function() { + it('should support RegExp', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.get('foo', /^bar=(\w+)/)[1], 'value1'); + assert.strictEqual(headers.get('foo', /^foo=/), null); + }); + + it('should support function', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.get('foo', (value, header) => { + assert.strictEqual(value, 'bar=value1'); + assert.strictEqual(header, 'foo'); + return true; + }), 'bar=value1'); + assert.strictEqual(headers.get('foo', () => false), null); + }); + }); + }); + + describe('has', function () { + it('should return true if the header is defined, otherwise false', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo'), true); + assert.strictEqual(headers.has('bar'), false); + }); + + describe('filter', function () { + it('should support RegExp', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo', /^bar=(\w+)/), true); + assert.strictEqual(headers.has('foo', /^foo=/), false); + }); + + it('should support function', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo', (value, header, headers)=> { + assert.strictEqual(value, 'bar=value1'); + assert.strictEqual(header, 'foo'); + return true; + }), true); + assert.strictEqual(headers.has('foo', ()=> false), false); + }); + + it('should support string pattern', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo', 'value1'), true); + assert.strictEqual(headers.has('foo', 'value2'), false); + }); + }); + }); + + describe('delete', function () { + it('should delete the header', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo'), true); + + headers.delete('foo'); + + assert.strictEqual(headers.has('foo'), false); + }); + + it('should return true if the header has been deleted, otherwise false', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.delete('bar'), false); + + assert.strictEqual(headers.delete('foo'), true); + }); + + it('should support headers array', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'x'); + headers.set('bar', 'y'); + headers.set('baz', 'z'); + + assert.strictEqual(headers.delete(['foo', 'baz']), true); + + assert.strictEqual(headers.has('foo'), false); + assert.strictEqual(headers.has('bar'), true); + assert.strictEqual(headers.has('baa'), false); + }); + + describe('filter', function () { + it('should support RegExp', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo'), true); + + headers.delete('foo', /baz=/); + + assert.strictEqual(headers.has('foo'), true); + + headers.delete('foo', /bar=/); + + assert.strictEqual(headers.has('foo'), false); + }); + + it('should support function', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + headers.delete('foo', (value, header)=> { + assert.strictEqual(value, 'bar=value1'); + assert.strictEqual(header, 'foo'); + return false; + }); + + assert.strictEqual(headers.has('foo'), true); + + assert.strictEqual(headers.delete('foo', ()=> true), true); + + assert.strictEqual(headers.has('foo'), false); + }); + + it('should support string pattern', function () { + const headers = new AxiosHeaders(); + + headers.set('foo', 'bar=value1'); + + assert.strictEqual(headers.has('foo'), true); + + headers.delete('foo', 'baz'); + + assert.strictEqual(headers.has('foo'), true); + + headers.delete('foo', 'bar'); + + assert.strictEqual(headers.has('foo'), false); + }); + }); + }); + + describe('toJSON', function () { + it('should return headers object with original headers case', function () { + const headers = new AxiosHeaders({ + Foo: 'x', + bAr: 'y' + }); + + assert.deepStrictEqual({...headers.toJSON()}, { + Foo: 'x', + bAr: 'y' + }); + }); + }); + + describe('accessors', function () { + it('should support get accessor', function () { + const headers = new AxiosHeaders({ + foo: 1 + }); + + headers.accessor(); + + assert.strictEqual(typeof headers.getFoo, 'function'); + assert.strictEqual(headers.getFoo(), '1'); + }); + + it('should support set accessor', function () { + const headers = new AxiosHeaders({ + foo: 1 + }); + + headers.accessor(); + + assert.strictEqual(typeof headers.setFoo, 'function'); + headers.setFoo(2); + assert.strictEqual(headers.getFoo(), '2'); + }); + }); + + it('should be caseless', function () { + const headers = new AxiosHeaders({ + fOo: 1 + }); + + headers.accessor(); + + assert.strictEqual(headers.get('Foo'), '1'); + assert.strictEqual(headers.get('foo'), '1'); + + headers.set('foo', 2); + + assert.strictEqual(headers.get('foO'), '2'); + assert.strictEqual(headers.get('fOo'), '2'); + + assert.strictEqual(headers.has('fOo'), true); + + headers.delete('FOO'); + + assert.strictEqual(headers.has('fOo'), false); + + }); + + describe('normalize()', function () { + it('should support auto-formatting', function () { + const headers = new AxiosHeaders({ + fOo: 1, + 'x-foo': 2, + 'y-bar-bAz': 3 + }); + + assert.deepStrictEqual({...headers.normalize().toJSON()}, { + Foo: '1', + 'X-Foo': '2', + 'Y-Bar-Baz': '3' + }); + }); + }); + + describe('AxiosHeader.parse', function () { + it('should parse headers', function () { + var date = new Date(); + var headers = AxiosHeaders.parse( + 'Date: ' + date.toISOString() + '\n' + + 'Content-Type: application/json\n' + + 'Connection: keep-alive\n' + + 'Transfer-Encoding: chunked' + ); + + assert.strictEqual(headers.get('date'), date.toISOString()); + assert.strictEqual(headers.get('content-type'),'application/json'); + assert.strictEqual(headers.get('connection'),'keep-alive'); + assert.strictEqual(headers.get('transfer-encoding'),'chunked'); + }); + + it('should use array for set-cookie', function() { + var parsedZero = AxiosHeaders.parse(''); + var parsedSingle = AxiosHeaders.parse( + 'Set-Cookie: key=val;' + ); + var parsedMulti = AxiosHeaders.parse( + 'Set-Cookie: key=val;\n' + + 'Set-Cookie: key2=val2;\n' + ); + + assert.strictEqual(parsedZero.get('set-cookie'), undefined); + assert.deepStrictEqual(parsedSingle.get('set-cookie'), ['key=val;']); + assert.deepStrictEqual(parsedMulti.get('set-cookie'),['key=val;', 'key2=val2;']); + }); + + it('should handle duplicates', function() { + var parsed = AxiosHeaders.parse( + 'Age: age-a\n' + // age is in ignore duplicates blocklist + 'Age: age-b\n' + + 'Foo: foo-a\n' + + 'Foo: foo-b\n' + ); + + assert.strictEqual(parsed.get('age'),'age-a'); + assert.strictEqual(parsed.get('foo'),'foo-a, foo-b'); + }); + }); + +}); From 596bceed9785d6a643358fa80dcf424fcc34db6f Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Sun, 22 May 2022 00:49:44 +0300 Subject: [PATCH 2/2] Added `formDataToJSON` helper; Added `axios.formToJSON` method; Added client tests; --- index.d.ts | 7 + lib/adapters/http.js | 33 +- lib/adapters/xhr.js | 27 +- lib/axios.js | 6 +- lib/core/Axios.js | 3 +- lib/core/AxiosHeaders.js | 309 ------------------- lib/defaults/index.js | 27 +- lib/helpers/formDataToJSON.js | 71 +++++ lib/helpers/parseHeaders.js | 6 +- lib/utils.js | 95 ++---- test/specs/helpers/formDataToJSON.spec.js | 50 +++ test/specs/helpers/parseHeaders.spec.js | 43 +++ test/specs/instance.spec.js | 3 +- test/unit/adapters/http.js | 2 +- test/unit/core/AxiosHeaders.js | 352 ---------------------- 15 files changed, 268 insertions(+), 766 deletions(-) delete mode 100644 lib/core/AxiosHeaders.js create mode 100644 lib/helpers/formDataToJSON.js create mode 100644 test/specs/helpers/formDataToJSON.spec.js delete mode 100644 test/unit/core/AxiosHeaders.js diff --git a/index.d.ts b/index.d.ts index 1f82f5080d..b5e9e0912d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -288,6 +288,12 @@ export interface GenericFormData { append(name: string, value: any, options?: any): any; } +export interface GenericHTMLFormElement { + name: string; + method: string; + submit(): void; +} + export interface AxiosStatic extends AxiosInstance { create(config?: CreateAxiosDefaults): AxiosInstance; Cancel: CancelStatic; @@ -300,6 +306,7 @@ export interface AxiosStatic extends AxiosInstance { spread(callback: (...args: T[]) => R): (array: T[]) => R; isAxiosError(payload: any): payload is AxiosError; toFormData(sourceObj: object, targetFormData?: GenericFormData, options?: FormSerializerOptions): GenericFormData; + formToJSON(form: GenericFormData|GenericHTMLFormElement): object; } declare const axios: AxiosStatic; diff --git a/lib/adapters/http.js b/lib/adapters/http.js index ac79157df8..8d3588b1f9 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -18,7 +18,6 @@ var CanceledError = require('../cancel/CanceledError'); var platform = require('../platform'); var fromDataURI = require('../helpers/fromDataURI'); var stream = require('stream'); -var AxiosHeaders = require('../core/AxiosHeaders'); var isHttps = /https:?/; @@ -161,17 +160,29 @@ module.exports = function httpAdapter(config) { )); } - var headers = new AxiosHeaders(config.headers); + var headers = config.headers; + var headerNames = {}; + + Object.keys(headers).forEach(function storeLowerName(name) { + headerNames[name.toLowerCase()] = name; + }); // Set User-Agent (required by some servers) // See https://github.com/axios/axios/issues/69 - // User-Agent is specified; handle case where no UA header is desired - // Only set header if it hasn't been set in config - headers.set('User-Agent', 'axios/' + VERSION, false); + if ('user-agent' in headerNames) { + // User-Agent is specified; handle case where no UA header is desired + if (!headers[headerNames['user-agent']]) { + delete headers[headerNames['user-agent']]; + } + // Otherwise, use specified value + } else { + // Only set header if it hasn't been set in config + headers['User-Agent'] = 'axios/' + VERSION; + } // support for https://www.npmjs.com/package/form-data api if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { - headers.set(data.getHeaders()); + Object.assign(headers, data.getHeaders()); } else if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) { // Nothing to do... @@ -196,7 +207,9 @@ module.exports = function httpAdapter(config) { } // Add Content-Length header if data exists - headers.set('Content-Length', data.length, false); + if (!headerNames['content-length']) { + headers['Content-Length'] = data.length; + } } // HTTP basic authentication @@ -214,7 +227,9 @@ module.exports = function httpAdapter(config) { auth = urlUsername + ':' + urlPassword; } - auth && headers.delete('authorization'); + if (auth && headerNames.authorization) { + delete headers[headerNames.authorization]; + } try { buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''); @@ -229,7 +244,7 @@ module.exports = function httpAdapter(config) { var options = { path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), method: method, - headers: headers.toJSON(), + headers: headers, agents: { http: config.httpAgent, https: config.httpsAgent }, auth: auth, protocol: protocol, diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index 4362169a9b..8612a77802 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -5,18 +5,18 @@ var settle = require('./../core/settle'); var cookies = require('./../helpers/cookies'); var buildURL = require('./../helpers/buildURL'); var buildFullPath = require('../core/buildFullPath'); +var parseHeaders = require('./../helpers/parseHeaders'); var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); var parseProtocol = require('../helpers/parseProtocol'); var platform = require('../platform'); -var AxiosHeaders = require('../core/AxiosHeaders'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; - var requestHeaders = new AxiosHeaders(config.headers); + var requestHeaders = config.headers; var responseType = config.responseType; var onCanceled; function done() { @@ -30,7 +30,7 @@ module.exports = function xhrAdapter(config) { } if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) { - requestHeaders.delete('Content-Type'); // Let the browser set it + delete requestHeaders['Content-Type']; // Let the browser set it } var request = new XMLHttpRequest(); @@ -39,7 +39,7 @@ module.exports = function xhrAdapter(config) { if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; - requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password)); + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } var fullPath = buildFullPath(config.baseURL, config.url); @@ -54,9 +54,7 @@ module.exports = function xhrAdapter(config) { return; } // Prepare the response - var responseHeaders = 'getAllResponseHeaders' in request ? - AxiosHeaders.parse(request.getAllResponseHeaders(), true) : null; - + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; var responseData = !responseType || responseType === 'text' || responseType === 'json' ? request.responseText : request.response; var response = { @@ -152,17 +150,20 @@ module.exports = function xhrAdapter(config) { undefined; if (xsrfValue) { - requestHeaders.set(config.xsrfHeaderName, xsrfValue); + requestHeaders[config.xsrfHeaderName] = xsrfValue; } } - // Remove Content-Type if data is undefined - - requestData === undefined && requestHeaders.delete('content-type'); // Add headers to the request if ('setRequestHeader' in request) { - utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) { - request.setRequestHeader(key, val); + 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); + } }); } diff --git a/lib/axios.js b/lib/axios.js index cf6583cfe5..bbbe301bf9 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -5,7 +5,7 @@ var bind = require('./helpers/bind'); var Axios = require('./core/Axios'); var mergeConfig = require('./core/mergeConfig'); var defaults = require('./defaults'); - +var formDataToJSON = require('./helpers/formDataToJSON'); /** * Create an instance of Axios * @@ -58,6 +58,10 @@ axios.spread = require('./helpers/spread'); // Expose isAxiosError axios.isAxiosError = require('./helpers/isAxiosError'); +axios.formToJSON = function(thing) { + return formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing); +}; + module.exports = axios; // Allow use of default import syntax in TypeScript diff --git a/lib/core/Axios.js b/lib/core/Axios.js index 3a79171099..ef7e810129 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -25,7 +25,8 @@ function Axios(instanceConfig) { /** * Dispatch a request * - * @param {Object} config The config specific for this request (merged with this.defaults) + * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults) + * @param {?Object} config */ Axios.prototype.request = function request(configOrUrl, config) { /*eslint no-param-reassign:0*/ diff --git a/lib/core/AxiosHeaders.js b/lib/core/AxiosHeaders.js deleted file mode 100644 index 8f9d1dfedb..0000000000 --- a/lib/core/AxiosHeaders.js +++ /dev/null @@ -1,309 +0,0 @@ -'use strict'; - -var utils = require('../utils'); - -var $internals = Symbol('internals'); - -var hasOwnProperty = utils.hasOwnProperty; - -function tokensParser(str) { - var tokens = Object.create(null); - var tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g; - var match; - - while ((match = tokensRE.exec(str))) { - tokens[match[1]] = match[2]; - } - - return tokens; -} - -function normalizeHeaderCase(header) { - return header && String(header).trim().toLowerCase(); -} - -function filterHeaderValue(value, filter, header, headers) { - if (utils.isFunction(filter)) { - return filter.call(this, value, header, headers); - } - - if (value === false) return; - - if (utils.isString(filter)) { - return value.indexOf(filter) !== -1; - } - - if (utils.isRegExp(filter)) { - return filter.test(value); - } -} - -function normalizeName(header) { - return header - .toLowerCase().replace(/([a-z\d])(\w*)/g, function(w, char, str) { - return char.toUpperCase() + str; - }); -} - -function AxiosHeaders(headers) { - this[$internals] = { - headers: {}, - names: {}, - accessors: {} - }; - - headers && this.set(headers); -} - -Object.assign(AxiosHeaders.prototype, { - set: function(header, valueOrRewrite, rewrite) { - var internals = this[$internals]; - var headers = internals.headers; - var names = internals.names; - - function setHeader(value, header, _rewrite) { - var lHeader = normalizeHeaderCase(header); - - if (!lHeader) { - throw new Error('header name can not be an empty string'); - } - - var has; - - if ( - hasOwnProperty(headers, lHeader) && - (_rewrite === false || (_rewrite === undefined && headers[lHeader] === false)) - ) { - return; - } - - if (value === null) { - has && this.delete(lHeader); - return; - } - - if (value !== false && !utils.isArray(value)) { - value = String(value).trim(); - } - - headers[lHeader] = value; - - if (!hasOwnProperty(names, lHeader)) { - names[lHeader] = header; - } - } - - if (utils.isString(header)) { - setHeader(valueOrRewrite, header, rewrite); - } else if (utils.isPlainObject(header)) { - utils.forEach(header, function(value, header) { - setHeader(value, header, valueOrRewrite); - }); - } else { - throw new TypeError('Header must be a string or an plain object'); - } - - return this; - }, - - get: function(header, parser) { - var internals = this[$internals]; - var headers = internals.headers; - header = normalizeHeaderCase(header); - - if (!hasOwnProperty(headers, header)) { - return undefined; - } - - var value = headers[header]; - - if (!parser || value === false) { - return value; - } - - if (parser === true) { - return tokensParser(value); - } - - if (utils.isFunction(parser)) { - return parser.call(this, value, header, headers) ? value : null; - } - - if (utils.isRegExp(parser)) { - return parser.exec(value); - } - - throw TypeError('parser must be boolean|regexp|function'); - }, - - has: function(header, filter) { - var headers = this[$internals].headers; - header = normalizeHeaderCase(header); - - return !!(header && hasOwnProperty(headers, header) && - (!filter || filterHeaderValue.call(this, headers[header], filter, header, headers))); - }, - - delete: function(header, filter) { - var self = this; - var internals = self[$internals]; - var headers = internals.headers; - var deleted = false; - - function deleteHeader(header) { - header = normalizeHeaderCase(header); - - if (header && hasOwnProperty(headers, header) && - (!filter || filterHeaderValue.call(self, headers[header], filter, header, headers))) { - delete headers[header]; - delete internals.names[header]; - deleted = true; - return true; - } - - return false; - } - - if (utils.isArray(header)) { - header.forEach(deleteHeader); - return deleted; - } - - return deleteHeader(header); - }, - - accessor: function(header) { - var self = this; - var internals = self[$internals]; - var accessors = internals.accessors; - - function defineAccessor(_header) { - var lHeader = normalizeHeaderCase(_header); - - if (!accessors[lHeader]) { - var names = internals.names; - var accessorName = utils.toCamelCase(' ' + _header); - - Object.defineProperty(self, 'get' + accessorName, { - value: function(parser) { - return this.get(lHeader, parser); - }, - configurable: true - }); - - Object.defineProperty(self, 'set' + accessorName, { - value: function(value, rewrite) { - return this.set(lHeader, value, rewrite); - }, - configurable: true - }); - - accessors[lHeader] = true; - - if (!hasOwnProperty(names, lHeader)) { - names[lHeader] = _header; - } - } - } - - if (header == null) { - header = internals.names; - } - - if (utils.isPlainObject(header)) { - utils.forEach(header, defineAccessor); - } else { - defineAccessor(header); - } - - return this; - }, - - normalize: function(formatter) { - var internals = this[$internals]; - - if (formatter == null) { - formatter = normalizeName; - } - - utils.forEach(internals.headers, function(value, header) { - internals.names[header] = formatter.call(this, header, value); - }); - - return this; - }, - - toJSON: function(asStringValues) { - var internals = this[$internals]; - var names = internals.names; - var obj = Object.create(null); - - utils.forEach(internals.headers, function(value, header) { - value !== false && (obj[names[header] || header] = asStringValues !== false && - utils.isArray(value) ? value.join(', ') : value); - }); - - return obj; - } -}); - -// AxiosHeaders whose duplicates are ignored by node -// c.f. https://nodejs.org/api/http.html#http_message_headers -var ignoreDuplicateOf = utils.toObjectSet([ - 'age', 'authorization', 'content-length', 'content-type', 'etag', - 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', - 'last-modified', 'location', 'max-forwards', 'proxy-authorization', - 'referer', 'retry-after', 'user-agent' -]); - -Object.assign(AxiosHeaders, { - /** - * Parse rawHeaders into an object - * - * ``` - * Date: Wed, 27 Aug 2014 08:58:49 GMT - * Content-Type: application/json - * Connection: keep-alive - * Transfer-Encoding: chunked - * ``` - * - * @param {String} rawHeaders AxiosHeaders needing to be parsed - * @param {Boolean} asPlainObject return a plain object instead of AxiosHeader instance - * @returns {Object} AxiosHeaders parsed into an object - */ - parse: function(rawHeaders, asPlainObject) { - var parsed = {}; - var key; - var val; - var i; - - if (!rawHeaders) { return asPlainObject ? parsed : new this(); } - - rawHeaders.split('\n').forEach(function parser(line) { - i = line.indexOf(':'); - key = line.substring(0, i).trim().toLowerCase(); - val = line.substring(i + 1).trim(); - - if (!key || (parsed[key] && ignoreDuplicateOf[key])) { - return; - } - - if (key === 'set-cookie') { - if (parsed[key]) { - parsed[key].push(val); - } else { - parsed[key] = [val]; - } - } else { - parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; - } - }); - - return asPlainObject ? parsed : new this(parsed); - } -}); - -utils.freezeMethods(AxiosHeaders.prototype); -utils.freezeMethods(AxiosHeaders); - -module.exports = AxiosHeaders; diff --git a/lib/defaults/index.js b/lib/defaults/index.js index 9f1564af74..d7ed025937 100644 --- a/lib/defaults/index.js +++ b/lib/defaults/index.js @@ -7,6 +7,7 @@ var transitionalDefaults = require('./transitional'); var toFormData = require('../helpers/toFormData'); var toURLEncodedForm = require('../helpers/toURLEncodedForm'); var platform = require('../platform'); +var formDataToJSON = require('../helpers/formDataToJSON'); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -55,8 +56,24 @@ var defaults = { normalizeHeaderName(headers, 'Accept'); normalizeHeaderName(headers, 'Content-Type'); - if (utils.isFormData(data) || - utils.isArrayBuffer(data) || + var contentType = headers && headers['Content-Type'] || ''; + var hasJSONContentType = contentType.indexOf('application/json') > -1; + var isObjectPayload = utils.isObject(data); + + if (isObjectPayload && utils.isHTMLForm(data)) { + data = new FormData(data); + } + + var isFormData = utils.isFormData(data); + + if (isFormData) { + if (!hasJSONContentType) { + return data; + } + return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data; + } + + if (utils.isArrayBuffer(data) || utils.isBuffer(data) || utils.isStream(data) || utils.isFile(data) || @@ -72,8 +89,6 @@ var defaults = { return data.toString(); } - var isObjectPayload = utils.isObject(data); - var contentType = headers && headers['Content-Type'] || ''; var isFileList; if (isObjectPayload) { @@ -81,7 +96,7 @@ var defaults = { return toURLEncodedForm(data, this.formSerializer).toString(); } - if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') !== -1) { + if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) { var _FormData = this.env && this.env.FormData; return toFormData( @@ -92,7 +107,7 @@ var defaults = { } } - if (isObjectPayload || contentType.indexOf('application/json') !== -1) { + if (isObjectPayload || hasJSONContentType ) { setContentTypeIfUnset(headers, 'application/json'); return stringifySafely(data); } diff --git a/lib/helpers/formDataToJSON.js b/lib/helpers/formDataToJSON.js new file mode 100644 index 0000000000..45a10364e4 --- /dev/null +++ b/lib/helpers/formDataToJSON.js @@ -0,0 +1,71 @@ +'use strict'; + +var utils = require('../utils'); + +function parsePropPath(name) { + // foo[x][y][z] + // foo.x.y.z + // foo-x-y-z + // foo x y z + return utils.matchAll(/\w+|\[(\w*)]/g, name).map(function(match) { + return match[0] === '[]' ? '' : match[1] || match[0]; + }); +} + +function arrayToObject(arr) { + var obj = {}; + var keys = Object.keys(arr); + var i; + var len = keys.length; + var key; + for (i = 0; i < len; i++) { + key = keys[i]; + obj[key] = arr[key]; + } + return obj; +} + +function formDataToJSON(formData) { + function buildPath(path, value, target, index) { + var name = path[index++]; + var isNumericKey = Number.isFinite(+name); + var isLast = index >= path.length; + name = !name && utils.isArray(target) ? target.length : name; + + if (isLast) { + if (utils.hasOwnProperty(target, name)) { + target[name] = [target[name], value]; + } else { + target[name] = value; + } + + return !isNumericKey; + } + + if (!target[name] || !utils.isObject(target[name])) { + target[name] = []; + } + + var result = buildPath(path, value, target[name], index); + + if (result && utils.isArray(target[name])) { + target[name] = arrayToObject(target[name]); + } + + return !isNumericKey; + } + + if (utils.isFormData(formData) && utils.isFunction(formData.entries)) { + var obj = {}; + + utils.forEachEntry(formData, function(name, value) { + buildPath(parsePropPath(name), value, obj, 0); + }); + + return obj; + } + + return null; +} + +module.exports = formDataToJSON; diff --git a/lib/helpers/parseHeaders.js b/lib/helpers/parseHeaders.js index 077d9f44ed..9b4bfc6eec 100644 --- a/lib/helpers/parseHeaders.js +++ b/lib/helpers/parseHeaders.js @@ -2,7 +2,7 @@ var utils = require('./../utils'); -// AxiosHeaders whose duplicates are ignored by node +// Headers whose duplicates are ignored by node // c.f. https://nodejs.org/api/http.html#http_message_headers var ignoreDuplicateOf = [ 'age', 'authorization', 'content-length', 'content-type', 'etag', @@ -21,8 +21,8 @@ var ignoreDuplicateOf = [ * Transfer-Encoding: chunked * ``` * - * @param {String} headers AxiosHeaders needing to be parsed - * @returns {Object} AxiosHeaders parsed into an object + * @param {String} headers Headers needing to be parsed + * @returns {Object} Headers parsed into an object */ module.exports = function parseHeaders(headers) { var parsed = {}; diff --git a/lib/utils.js b/lib/utils.js index b63ea35b54..577462f924 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -441,80 +441,37 @@ var isTypedArray = (function(TypedArray) { }; })(typeof Uint8Array !== 'undefined' && Object.getPrototypeOf(Uint8Array)); -var toCamelCase = function(str) { - return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g, - function replacer(m, p1, p2) { - return p1.toUpperCase() + p2; - } - ); -}; - -var hasOwnProperty = (function resolver(_hasOwnProperty) { - return function hasOwnProperty(obj, prop) { - return _hasOwnProperty.call(obj, prop); - }; -})(Object.prototype.hasOwnProperty); - -/** - * Determine if a value is a RegExp object - * @function - * @param {Object} val The value to test - * @returns {boolean} True if value is a RegExp object, otherwise false - */ -var isRegExp = kindOfTest('RegExp'); +function forEachEntry(obj, fn) { + var generator = obj && obj[Symbol.iterator]; -function reduceDescriptors(obj, reducer) { - var descriptors = Object.getOwnPropertyDescriptors(obj); - var reducedDescriptors = {}; + var iterator = generator.call(obj); - forEach(descriptors, function(descriptor, name) { - if (reducer(descriptor, name, obj) !== false) { - reducedDescriptors[name] = descriptor; - } - }); + var result; - Object.defineProperties(obj, reducedDescriptors); + while ((result = iterator.next()) && !result.done) { + var pair = result.value; + fn.call(obj, pair[0], pair[1]); + } } -/** - * Makes all methods read-only - * @param {Object} obj - */ - -function freezeMethods(obj) { - reduceDescriptors(obj, function(descriptor, name) { - var value = obj[name]; - - if (!isFunction(value)) return; +function matchAll(regExp, str) { + var matches; + var arr = []; - descriptor.enumerable = false; - - if ('writable' in descriptor) { - descriptor.writable = false; - return; - } + while ((matches = regExp.exec(str)) !== null) { + arr.push(matches); + } - if (!descriptor.set) { - descriptor.set = function() { - throw Error('Can not read-only method \'' + name + '\''); - }; - } - }); + return arr; } -function toObjectSet(arrayOrString, delimiter) { - var obj = {}; +var isHTMLForm = kindOfTest('HTMLFormElement'); - function define(arr) { - arr.forEach(function(value) { - obj[value] = true; - }); - } - - isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter)); - - return obj; -} +var hasOwnProperty = (function resolver(_hasOwnProperty) { + return function(obj, prop) { + return _hasOwnProperty.call(obj, prop); + }; +})(Object.prototype.hasOwnProperty); module.exports = { isArray: isArray, @@ -530,7 +487,6 @@ module.exports = { isDate: isDate, isFile: isFile, isBlob: isBlob, - isRegExp: isRegExp, isFunction: isFunction, isStream: isStream, isURLSearchParams: isURLSearchParams, @@ -548,9 +504,8 @@ module.exports = { toArray: toArray, isTypedArray: isTypedArray, isFileList: isFileList, - toCamelCase: toCamelCase, - hasOwnProperty: hasOwnProperty, - reduceDescriptors: reduceDescriptors, - freezeMethods: freezeMethods, - toObjectSet: toObjectSet + forEachEntry: forEachEntry, + matchAll: matchAll, + isHTMLForm: isHTMLForm, + hasOwnProperty: hasOwnProperty }; diff --git a/test/specs/helpers/formDataToJSON.spec.js b/test/specs/helpers/formDataToJSON.spec.js new file mode 100644 index 0000000000..69ea1a6d51 --- /dev/null +++ b/test/specs/helpers/formDataToJSON.spec.js @@ -0,0 +1,50 @@ +var formDataToJSON = require('../../../lib/helpers/formDataToJSON'); + +describe('formDataToJSON', function () { + it('should convert a FormData Object to JSON Object', function () { + const formData = new FormData(); + + formData.append('foo[bar][baz]', '123'); + + expect(formDataToJSON(formData)).toEqual({ + foo: { + bar: { + baz: '123' + } + } + }); + }); + + it('should convert repeatable values as an array', function () { + const formData = new FormData(); + + formData.append('foo', '1'); + formData.append('foo', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); + + it('should convert props with empty brackets to arrays', function () { + const formData = new FormData(); + + formData.append('foo[]', '1'); + formData.append('foo[]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); + + it('should supported indexed arrays', function () { + const formData = new FormData(); + + formData.append('foo[0]', '1'); + formData.append('foo[1]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); +}); diff --git a/test/specs/helpers/parseHeaders.spec.js b/test/specs/helpers/parseHeaders.spec.js index e425fca8b7..254d1d61ef 100644 --- a/test/specs/helpers/parseHeaders.spec.js +++ b/test/specs/helpers/parseHeaders.spec.js @@ -1,2 +1,45 @@ var parseHeaders = require('../../../lib/helpers/parseHeaders'); +describe('helpers::parseHeaders', function () { + it('should parse headers', function () { + var date = new Date(); + var parsed = parseHeaders( + 'Date: ' + date.toISOString() + '\n' + + 'Content-Type: application/json\n' + + 'Connection: keep-alive\n' + + 'Transfer-Encoding: chunked' + ); + + expect(parsed['date']).toEqual(date.toISOString()); + expect(parsed['content-type']).toEqual('application/json'); + expect(parsed['connection']).toEqual('keep-alive'); + expect(parsed['transfer-encoding']).toEqual('chunked'); + }); + + it('should use array for set-cookie', function() { + var parsedZero = parseHeaders(''); + var parsedSingle = parseHeaders( + 'Set-Cookie: key=val;' + ); + var parsedMulti = parseHeaders( + 'Set-Cookie: key=val;\n' + + 'Set-Cookie: key2=val2;\n' + ); + + expect(parsedZero['set-cookie']).toBeUndefined(); + expect(parsedSingle['set-cookie']).toEqual(['key=val;']); + expect(parsedMulti['set-cookie']).toEqual(['key=val;', 'key2=val2;']); + }); + + it('should handle duplicates', function() { + var parsed = parseHeaders( + 'Age: age-a\n' + // age is in ignore duplicates blocklist + 'Age: age-b\n' + + 'Foo: foo-a\n' + + 'Foo: foo-b\n' + ); + + expect(parsed['age']).toEqual('age-a'); + expect(parsed['foo']).toEqual('foo-a, foo-b'); + }); +}); diff --git a/test/specs/instance.spec.js b/test/specs/instance.spec.js index e1b9a7c9d3..e29c2f7109 100644 --- a/test/specs/instance.spec.js +++ b/test/specs/instance.spec.js @@ -25,7 +25,8 @@ describe('instance', function () { 'isAxiosError', 'VERSION', 'default', - 'toFormData' + 'toFormData', + 'formToJSON' ].indexOf(prop) > -1) { continue; } diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index b5e9ae22ef..8804bd2d0b 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -1259,7 +1259,7 @@ describe('supports http with nodejs', function () { }).listen(4444, function () { axios.get('http://localhost:4444/', { headers: { - "User-Agent": false + "User-Agent": null } }).then(function (res) { done(); diff --git a/test/unit/core/AxiosHeaders.js b/test/unit/core/AxiosHeaders.js deleted file mode 100644 index 327459849b..0000000000 --- a/test/unit/core/AxiosHeaders.js +++ /dev/null @@ -1,352 +0,0 @@ -var AxiosHeaders = require('../../../lib/core/AxiosHeaders'); -var assert = require('assert'); - - -describe('AxiosHeaders', function () { - it('should support headers argument', function () { - var headers = new AxiosHeaders({ - x: 1, - y: 2 - }); - - assert.strictEqual(headers.get('x'), '1'); - assert.strictEqual(headers.get('y'), '2'); - }) - - - describe('set', function () { - it('should support adding a single header', function(){ - var headers = new AxiosHeaders(); - - headers.set('foo', 'bar'); - - assert.strictEqual(headers.get('foo'), 'bar'); - }) - - it('should support adding multiple headers', function(){ - var headers = new AxiosHeaders(); - - headers.set({ - foo: 'value1', - bar: 'value2', - }); - - assert.strictEqual(headers.get('foo'), 'value1'); - assert.strictEqual(headers.get('bar'), 'value2'); - }) - - it('should rewrite header only if rewrite option is set to true or undefined', function(){ - var headers = new AxiosHeaders(); - - headers.set('foo', 'value1'); - - headers.set('foo', 'value2', false); - - assert.strictEqual(headers.get('foo'), 'value1'); - - headers.set('foo', 'value2'); - - assert.strictEqual(headers.get('foo'), 'value2'); - - headers.set('foo', 'value3', true); - - assert.strictEqual(headers.get('foo'), 'value3'); - }); - - it('should not rewrite the header if its value is false, unless rewrite options is set to true', function(){ - var headers = new AxiosHeaders(); - - headers.set('foo', false); - headers.set('foo', 'value2'); - - assert.strictEqual(headers.get('foo'), false); - - headers.set('foo', 'value2', true); - - assert.strictEqual(headers.get('foo'), 'value2'); - }) - }); - - describe('get', function () { - describe('filter', function() { - it('should support RegExp', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.get('foo', /^bar=(\w+)/)[1], 'value1'); - assert.strictEqual(headers.get('foo', /^foo=/), null); - }); - - it('should support function', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.get('foo', (value, header) => { - assert.strictEqual(value, 'bar=value1'); - assert.strictEqual(header, 'foo'); - return true; - }), 'bar=value1'); - assert.strictEqual(headers.get('foo', () => false), null); - }); - }); - }); - - describe('has', function () { - it('should return true if the header is defined, otherwise false', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo'), true); - assert.strictEqual(headers.has('bar'), false); - }); - - describe('filter', function () { - it('should support RegExp', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo', /^bar=(\w+)/), true); - assert.strictEqual(headers.has('foo', /^foo=/), false); - }); - - it('should support function', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo', (value, header, headers)=> { - assert.strictEqual(value, 'bar=value1'); - assert.strictEqual(header, 'foo'); - return true; - }), true); - assert.strictEqual(headers.has('foo', ()=> false), false); - }); - - it('should support string pattern', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo', 'value1'), true); - assert.strictEqual(headers.has('foo', 'value2'), false); - }); - }); - }); - - describe('delete', function () { - it('should delete the header', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo'), true); - - headers.delete('foo'); - - assert.strictEqual(headers.has('foo'), false); - }); - - it('should return true if the header has been deleted, otherwise false', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.delete('bar'), false); - - assert.strictEqual(headers.delete('foo'), true); - }); - - it('should support headers array', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'x'); - headers.set('bar', 'y'); - headers.set('baz', 'z'); - - assert.strictEqual(headers.delete(['foo', 'baz']), true); - - assert.strictEqual(headers.has('foo'), false); - assert.strictEqual(headers.has('bar'), true); - assert.strictEqual(headers.has('baa'), false); - }); - - describe('filter', function () { - it('should support RegExp', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo'), true); - - headers.delete('foo', /baz=/); - - assert.strictEqual(headers.has('foo'), true); - - headers.delete('foo', /bar=/); - - assert.strictEqual(headers.has('foo'), false); - }); - - it('should support function', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - headers.delete('foo', (value, header)=> { - assert.strictEqual(value, 'bar=value1'); - assert.strictEqual(header, 'foo'); - return false; - }); - - assert.strictEqual(headers.has('foo'), true); - - assert.strictEqual(headers.delete('foo', ()=> true), true); - - assert.strictEqual(headers.has('foo'), false); - }); - - it('should support string pattern', function () { - const headers = new AxiosHeaders(); - - headers.set('foo', 'bar=value1'); - - assert.strictEqual(headers.has('foo'), true); - - headers.delete('foo', 'baz'); - - assert.strictEqual(headers.has('foo'), true); - - headers.delete('foo', 'bar'); - - assert.strictEqual(headers.has('foo'), false); - }); - }); - }); - - describe('toJSON', function () { - it('should return headers object with original headers case', function () { - const headers = new AxiosHeaders({ - Foo: 'x', - bAr: 'y' - }); - - assert.deepStrictEqual({...headers.toJSON()}, { - Foo: 'x', - bAr: 'y' - }); - }); - }); - - describe('accessors', function () { - it('should support get accessor', function () { - const headers = new AxiosHeaders({ - foo: 1 - }); - - headers.accessor(); - - assert.strictEqual(typeof headers.getFoo, 'function'); - assert.strictEqual(headers.getFoo(), '1'); - }); - - it('should support set accessor', function () { - const headers = new AxiosHeaders({ - foo: 1 - }); - - headers.accessor(); - - assert.strictEqual(typeof headers.setFoo, 'function'); - headers.setFoo(2); - assert.strictEqual(headers.getFoo(), '2'); - }); - }); - - it('should be caseless', function () { - const headers = new AxiosHeaders({ - fOo: 1 - }); - - headers.accessor(); - - assert.strictEqual(headers.get('Foo'), '1'); - assert.strictEqual(headers.get('foo'), '1'); - - headers.set('foo', 2); - - assert.strictEqual(headers.get('foO'), '2'); - assert.strictEqual(headers.get('fOo'), '2'); - - assert.strictEqual(headers.has('fOo'), true); - - headers.delete('FOO'); - - assert.strictEqual(headers.has('fOo'), false); - - }); - - describe('normalize()', function () { - it('should support auto-formatting', function () { - const headers = new AxiosHeaders({ - fOo: 1, - 'x-foo': 2, - 'y-bar-bAz': 3 - }); - - assert.deepStrictEqual({...headers.normalize().toJSON()}, { - Foo: '1', - 'X-Foo': '2', - 'Y-Bar-Baz': '3' - }); - }); - }); - - describe('AxiosHeader.parse', function () { - it('should parse headers', function () { - var date = new Date(); - var headers = AxiosHeaders.parse( - 'Date: ' + date.toISOString() + '\n' + - 'Content-Type: application/json\n' + - 'Connection: keep-alive\n' + - 'Transfer-Encoding: chunked' - ); - - assert.strictEqual(headers.get('date'), date.toISOString()); - assert.strictEqual(headers.get('content-type'),'application/json'); - assert.strictEqual(headers.get('connection'),'keep-alive'); - assert.strictEqual(headers.get('transfer-encoding'),'chunked'); - }); - - it('should use array for set-cookie', function() { - var parsedZero = AxiosHeaders.parse(''); - var parsedSingle = AxiosHeaders.parse( - 'Set-Cookie: key=val;' - ); - var parsedMulti = AxiosHeaders.parse( - 'Set-Cookie: key=val;\n' + - 'Set-Cookie: key2=val2;\n' - ); - - assert.strictEqual(parsedZero.get('set-cookie'), undefined); - assert.deepStrictEqual(parsedSingle.get('set-cookie'), ['key=val;']); - assert.deepStrictEqual(parsedMulti.get('set-cookie'),['key=val;', 'key2=val2;']); - }); - - it('should handle duplicates', function() { - var parsed = AxiosHeaders.parse( - 'Age: age-a\n' + // age is in ignore duplicates blocklist - 'Age: age-b\n' + - 'Foo: foo-a\n' + - 'Foo: foo-b\n' - ); - - assert.strictEqual(parsed.get('age'),'age-a'); - assert.strictEqual(parsed.get('foo'),'foo-a, foo-b'); - }); - }); - -});