From f20490eb6b7d37478bd9906649ce085e0a269c6c Mon Sep 17 00:00:00 2001 From: Nick Uraltsev Date: Sat, 7 May 2016 12:26:28 -0700 Subject: [PATCH 1/2] Adding support for URLSearchParams --- README.md | 6 +++- lib/defaults.js | 34 +++++++++++-------- lib/helpers/buildURL.js | 3 +- lib/helpers/normalizeHeaderName.js | 12 +++++++ lib/utils.js | 19 +++++++++++ package.json | 3 +- test/specs/helpers/buildURL.spec.js | 9 +++-- .../specs/helpers/normalizeHeaderName.spec.js | 21 ++++++++++++ test/specs/requests.spec.js | 15 ++++++++ test/specs/utils/isX.spec.js | 6 ++++ 10 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 lib/helpers/normalizeHeaderName.js create mode 100644 test/specs/helpers/normalizeHeaderName.spec.js diff --git a/README.md b/README.md index 3ea7108..36868ef 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ These are the available config options for making requests. Only the `url` is re headers: {'X-Requested-With': 'XMLHttpRequest'}, // `params` are the URL parameters to be sent with the request + // Must be a plain object or a URLSearchParams object params: { ID: 12345 }, @@ -228,7 +229,10 @@ 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, a hash, or a Stream + // When no `transformRequest` is set, must be of one of the following types: + // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams + // - Browser only: FormData, File, Blob + // - Node only: Stream data: { firstName: 'Fred' }, diff --git a/lib/defaults.js b/lib/defaults.js index 1942ce0..162ffa7 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,33 +1,39 @@ 'use strict'; var utils = require('./utils'); +var normalizeHeaderName = require('./helpers/normalizeHeaderName'); var PROTECTION_PREFIX = /^\)\]\}',?\n/; var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' }; +function setContentTypeIfUnset(headers, value) { + if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) { + headers['Content-Type'] = value; + } +} + module.exports = { transformRequest: [function transformRequest(data, headers) { - if (utils.isFormData(data) || utils.isArrayBuffer(data) || utils.isStream(data)) { + normalizeHeaderName(headers, 'Content-Type'); + if (utils.isFormData(data) || + utils.isArrayBuffer(data) || + utils.isStream(data) || + utils.isFile(data) || + utils.isBlob(data) + ) { return data; } if (utils.isArrayBufferView(data)) { return data.buffer; } - if (utils.isObject(data) && !utils.isFile(data) && !utils.isBlob(data)) { - // Set application/json if no Content-Type has been specified - if (!utils.isUndefined(headers)) { - utils.forEach(headers, function processContentTypeHeader(val, key) { - if (key.toLowerCase() === 'content-type') { - headers['Content-Type'] = val; - } - }); - - if (utils.isUndefined(headers['Content-Type'])) { - headers['Content-Type'] = 'application/json;charset=utf-8'; - } - } + if (utils.isURLSearchParams(data)) { + setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8'); + return data.toString(); + } + if (utils.isObject(data)) { + setContentTypeIfUnset(headers, 'application/json;charset=utf-8'); return JSON.stringify(data); } return data; diff --git a/lib/helpers/buildURL.js b/lib/helpers/buildURL.js index c8365f4..1e66456 100644 --- a/lib/helpers/buildURL.js +++ b/lib/helpers/buildURL.js @@ -29,6 +29,8 @@ module.exports = function buildURL(url, params, paramsSerializer) { var serializedParams; if (paramsSerializer) { serializedParams = paramsSerializer(params); + } else if (utils.isURLSearchParams(params)) { + serializedParams = params.toString(); } else { var parts = []; @@ -64,4 +66,3 @@ module.exports = function buildURL(url, params, paramsSerializer) { return url; }; - diff --git a/lib/helpers/normalizeHeaderName.js b/lib/helpers/normalizeHeaderName.js new file mode 100644 index 0000000..738c9fe --- /dev/null +++ b/lib/helpers/normalizeHeaderName.js @@ -0,0 +1,12 @@ +'use strict'; + +var utils = require('../utils'); + +module.exports = function normalizeHeaderName(headers, normalizedName) { + utils.forEach(headers, function processHeader(value, name) { + if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { + headers[normalizedName] = value; + delete headers[name]; + } + }); +}; diff --git a/lib/utils.js b/lib/utils.js index 33f2d4d..a88ec95 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -142,6 +142,24 @@ function isStream(val) { return isObject(val) && isFunction(val.pipe); } +/** + * Determine if a value is a URLSearchParams object + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a URLSearchParams object, otherwise false + */ +function isURLSearchParams(val) { + // Object.prototype.toString will return [object Object] for a polyfill + // Hence, we have to use duck typing + return toString.call(val) === '[object URLSearchParams]' || ( + isObject(val) && + isFunction(val.append) && + isFunction(val.delete) && + isFunction(val.get) && + isFunction(val.set) + ); +} + /** * Trim excess whitespace off the beginning and end of a string * @@ -259,6 +277,7 @@ module.exports = { isBlob: isBlob, isFunction: isFunction, isStream: isStream, + isURLSearchParams: isURLSearchParams, isStandardBrowserEnv: isStandardBrowserEnv, forEach: forEach, merge: merge, diff --git a/package.json b/package.json index 72908c3..5af2e3b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "phantomjs-prebuilt": "2.1.6", "sinon": "1.17.3", "webpack": "1.12.14", - "webpack-dev-server": "1.14.1" + "webpack-dev-server": "1.14.1", + "url-search-params": "0.5.0" }, "browser": { "./lib/adapters/http.js": "./lib/adapters/xhr.js" diff --git a/test/specs/helpers/buildURL.spec.js b/test/specs/helpers/buildURL.spec.js index 453c1f9..83d7ba1 100644 --- a/test/specs/helpers/buildURL.spec.js +++ b/test/specs/helpers/buildURL.spec.js @@ -1,4 +1,5 @@ var buildURL = require('../../../lib/helpers/buildURL'); +var URLSearchParams = require('url-search-params'); describe('helpers::buildURL', function () { it('should support null params', function () { @@ -60,7 +61,9 @@ describe('helpers::buildURL', function () { expect(buildURL('/foo', params, serializer)).toEqual('/foo?foo=bar'); expect(serializer.calledOnce).toBe(true); expect(serializer.calledWith(params)).toBe(true); - }) + }); + + it('should support URLSearchParams', function () { + expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toEqual('/foo?bar=baz'); + }); }); - - diff --git a/test/specs/helpers/normalizeHeaderName.spec.js b/test/specs/helpers/normalizeHeaderName.spec.js new file mode 100644 index 0000000..d8d5e82 --- /dev/null +++ b/test/specs/helpers/normalizeHeaderName.spec.js @@ -0,0 +1,21 @@ +var normalizeHeaderName = require('../../../lib/helpers/normalizeHeaderName'); + +describe('helpers::normalizeHeaderName', function () { + it('should normalize matching header name', function () { + var headers = { + 'conTenT-Type': 'foo/bar', + }; + normalizeHeaderName(headers, 'Content-Type'); + expect(headers['Content-Type']).toBe('foo/bar'); + expect(headers['conTenT-Type']).toBeUndefined(); + }); + + it('should not change non-matching header name', function () { + var headers = { + 'content-type': 'foo/bar', + }; + normalizeHeaderName(headers, 'Content-Length'); + expect(headers['content-type']).toBe('foo/bar'); + expect(headers['Content-Length']).toBeUndefined(); + }); +}); diff --git a/test/specs/requests.spec.js b/test/specs/requests.spec.js index 053de1e..7c47b5e 100644 --- a/test/specs/requests.spec.js +++ b/test/specs/requests.spec.js @@ -1,3 +1,5 @@ +var URLSearchParams = require('url-search-params'); + describe('requests', function () { beforeEach(function () { jasmine.Ajax.install(); @@ -277,4 +279,17 @@ describe('requests', function () { }); }); + it('should support URLSearchParams', function (done) { + var params = new URLSearchParams(); + params.append('param1', 'value1'); + params.append('param2', 'value2'); + + axios.post('/foo', params); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders['Content-Type']).toBe('application/x-www-form-urlencoded;charset=utf-8'); + expect(request.params).toBe('param1=value1¶m2=value2'); + done(); + }); + }); }); diff --git a/test/specs/utils/isX.spec.js b/test/specs/utils/isX.spec.js index d58d113..2365b25 100644 --- a/test/specs/utils/isX.spec.js +++ b/test/specs/utils/isX.spec.js @@ -1,5 +1,6 @@ var utils = require('../../../lib/utils'); var Stream = require('stream'); +var URLSearchParams = require('url-search-params'); describe('utils::isX', function () { it('should validate Array', function () { @@ -78,4 +79,9 @@ describe('utils::isX', function () { expect(utils.isStream(new Stream.Readable())).toEqual(true); expect(utils.isStream({ foo: 'bar' })).toEqual(false); }); + + it('should validate URLSearchParams', function () { + expect(utils.isURLSearchParams(new URLSearchParams())).toEqual(true); + expect(utils.isURLSearchParams('foo=1&bar=2')).toEqual(false); + }); }); From 2b8d89a65e0d3de0bcc643af35bae593a98eaf13 Mon Sep 17 00:00:00 2001 From: Nick Uraltsev Date: Wed, 18 May 2016 18:47:34 -0700 Subject: [PATCH 2/2] Modifying isURLSearchParams function to use instanceof instead of duck typing --- lib/utils.js | 10 +--------- test/specs/__helpers.js | 3 +++ test/specs/requests.spec.js | 2 -- test/specs/utils/isX.spec.js | 1 - 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index a88ec95..26d710e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -149,15 +149,7 @@ function isStream(val) { * @returns {boolean} True if value is a URLSearchParams object, otherwise false */ function isURLSearchParams(val) { - // Object.prototype.toString will return [object Object] for a polyfill - // Hence, we have to use duck typing - return toString.call(val) === '[object URLSearchParams]' || ( - isObject(val) && - isFunction(val.append) && - isFunction(val.delete) && - isFunction(val.get) && - isFunction(val.set) - ); + return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams; } /** diff --git a/test/specs/__helpers.js b/test/specs/__helpers.js index 80ecc74..8a06d0d 100644 --- a/test/specs/__helpers.js +++ b/test/specs/__helpers.js @@ -1,6 +1,9 @@ // Polyfill ES6 Promise require('es6-promise').polyfill(); +// Polyfill URLSearchParams +URLSearchParams = require('url-search-params'); + // Import axios axios = require('../../index'); diff --git a/test/specs/requests.spec.js b/test/specs/requests.spec.js index 7c47b5e..47be0b0 100644 --- a/test/specs/requests.spec.js +++ b/test/specs/requests.spec.js @@ -1,5 +1,3 @@ -var URLSearchParams = require('url-search-params'); - describe('requests', function () { beforeEach(function () { jasmine.Ajax.install(); diff --git a/test/specs/utils/isX.spec.js b/test/specs/utils/isX.spec.js index 2365b25..7620f8c 100644 --- a/test/specs/utils/isX.spec.js +++ b/test/specs/utils/isX.spec.js @@ -1,6 +1,5 @@ var utils = require('../../../lib/utils'); var Stream = require('stream'); -var URLSearchParams = require('url-search-params'); describe('utils::isX', function () { it('should validate Array', function () {