Skip to content

Commit

Permalink
Fixed toFormData regression bug (unreleased) with Array-like object…
Browse files Browse the repository at this point in the history
…s serialization; (#4714)

Added `toURLEncodedForm` helper;
Added automatic payload serialization to `application/x-www-form-urlencoded` to have parity with `multipart/form-data`;
Added test of handling `application/x-www-form-urlencoded` body by express.js;
Updated README.md;
Added missed param in JSDoc;
Fixed hrefs in README.md;

Co-authored-by: Jay <jasonsaayman@gmail.com>
  • Loading branch information
DigitalBrainJS and jasonsaayman committed May 16, 2022
1 parent e762cf7 commit c05ad48
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 65 deletions.
104 changes: 78 additions & 26 deletions README.md
Expand Up @@ -39,12 +39,12 @@ Promise based HTTP client for the browser and node.js
- [AbortController](#abortcontroller)
- [CancelToken 👎](#canceltoken-deprecated)
- [Using application/x-www-form-urlencoded format](#using-applicationx-www-form-urlencoded-format)
- [Browser](#browser)
- [Node.js](#nodejs)
- [Query string](#query-string)
- [Form data](#form-data)
- [Automatic serialization](#-automatic-serialization)
- [Manual FormData passing](#manual-formdata-passing)
- [URLSearchParams](#urlsearchparams)
- [Query string](#query-string-older-browsers)
- [🆕 Automatic serialization](#-automatic-serialization-to-urlsearchparams)
- [Using multipart/form-data format](#using-multipartform-data-format)
- [FormData](#formdata)
- [🆕 Automatic serialization](#-automatic-serialization-to-formdata)
- [Semver](#semver)
- [Promises](#promises)
- [TypeScript](#typescript)
Expand All @@ -61,6 +61,7 @@ Promise based HTTP client for the browser and node.js
- Transform request and response data
- Cancel requests
- Automatic transforms for JSON data
- 🆕 Automatic data object serialization to `multipart/form-data` and `x-www-form-urlencoded` body encodings
- Client side support for protecting against [XSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery)

## Browser Support
Expand Down Expand Up @@ -814,7 +815,9 @@ cancel();
> During the transition period, you can use both cancellation APIs, even for the same request:
## Using application/x-www-form-urlencoded format
## Using `application/x-www-form-urlencoded` format
### URLSearchParams
By default, axios serializes JavaScript objects to `JSON`. To send data in the [`application/x-www-form-urlencoded` format](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) instead, you can use the [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) API, which is [supported](http://www.caniuse.com/#feat=urlsearchparams) in the vast majority of browsers, [and Node](https://nodejs.org/api/url.html#url_class_urlsearchparams) starting with v10 (released in 2018).
Expand All @@ -824,7 +827,7 @@ params.append('extraparam', 'value');
axios.post('/foo', params);
```
### Older browsers
### Query string (Older browsers)
For compatibility with very old browsers, there is a [polyfill](https://github.com/WebReflection/url-search-params) available (make sure to polyfill the global environment).
Expand Down Expand Up @@ -863,9 +866,73 @@ You can also use the [`qs`](https://github.com/ljharb/qs) library.
> NOTE:
> The `qs` library is preferable if you need to stringify nested objects, as the `querystring` method has [known issues](https://github.com/nodejs/node-v0.x-archive/issues/1665) with that use case.
#### Form data
### 🆕 Automatic serialization to URLSearchParams
Axios will automatically serialize the data object to urlencoded format if the content-type header is set to "application/x-www-form-urlencoded".
```
const data = {
x: 1,
arr: [1, 2, 3],
arr2: [1, [2], 3],
users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}],
};

await axios.postForm('https://postman-echo.com/post', data,
{headers: {'content-type': 'application/x-www-form-urlencoded'}}
);
```
The server will handle it as
```js
{
x: '1',
'arr[]': [ '1', '2', '3' ],
'arr2[0]': '1',
'arr2[1][0]': '2',
'arr2[2]': '3',
'arr3[]': [ '1', '2', '3' ],
'users[0][name]': 'Peter',
'users[0][surname]': 'griffin',
'users[1][name]': 'Thomas',
'users[1][surname]': 'Anderson'
}
````

If your backend body-parser (like `body-parser` of `express.js`) supports nested objects decoding, you will get the same object on the server-side automatically

```js
var app = express();
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
app.post('/', function (req, res, next) {
// echo body as JSON
res.send(JSON.stringify(req.body));
});
server = app.listen(3000);
```

## Using `multipart/form-data` format

### FormData

In node.js, you can use the [`form-data`](https://github.com/form-data/form-data) library as follows:

```js
const FormData = require('form-data');
const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
axios.post('https://example.com', form)
```

##### 🆕 Automatic serialization
### 🆕 Automatic serialization to FormData

Starting from `v0.27.0`, Axios supports automatic object serialization to a FormData object if the request `Content-Type`
header is set to `multipart/form-data`.
Expand Down Expand Up @@ -904,7 +971,7 @@ Axios FormData serializer supports some special endings to perform the following
- `[]` - unwrap the array-like object as separate fields with the same key
> NOTE:
> unwrap/expand operation will be used by default on array-like objects
> unwrap/expand operation will be used by default on arrays and FileList objects
FormData serializer supports additional options via `config.formSerializer: object` property to handle rare cases:
Expand Down Expand Up @@ -976,21 +1043,6 @@ await axios.postForm('https://httpbin.org/post', document.querySelector('#fileIn
All files will be sent with the same field names: `files[]`;
##### Manual FormData passing
In node.js, you can use the [`form-data`](https://github.com/form-data/form-data) library as follows:
```js
const FormData = require('form-data');

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));

axios.post('https://example.com', form)
```
## Semver
Until axios reaches a `1.0` release, breaking changes will be released with a new minor version. For example `0.5.1`, and `0.5.4` will have the same API, but `0.6.0` will have breaking changes.
Expand Down
29 changes: 19 additions & 10 deletions lib/defaults/index.js
Expand Up @@ -5,6 +5,7 @@ var normalizeHeaderName = require('../helpers/normalizeHeaderName');
var AxiosError = require('../core/AxiosError');
var transitionalDefaults = require('./transitional');
var toFormData = require('../helpers/toFormData');
var toURLEncodedForm = require('../helpers/toURLEncodedForm');

var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
Expand Down Expand Up @@ -71,18 +72,26 @@ var defaults = {
}

var isObjectPayload = utils.isObject(data);
var contentType = headers && headers['Content-Type'];

var contentType = headers && headers['Content-Type'] || '';
var isFileList;

if ((isFileList = utils.isFileList(data)) || (isObjectPayload && contentType === 'multipart/form-data')) {
var _FormData = this.env && this.env.FormData;
return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
} else if (isObjectPayload || contentType === 'application/json') {
if (isObjectPayload) {
if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) {
return toURLEncodedForm(data).toString();
}

if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') !== -1) {
var _FormData = this.env && this.env.FormData;

return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
}
}

if (isObjectPayload || contentType.indexOf('application/json') !== -1) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/buildURL.js
Expand Up @@ -17,6 +17,7 @@ function encode(val) {
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @param {?object} paramsSerializer
* @returns {string} The formatted url
*/
module.exports = function buildURL(url, params, paramsSerializer) {
Expand Down
42 changes: 22 additions & 20 deletions lib/helpers/toFormData.js
Expand Up @@ -20,6 +20,20 @@ function renderKey(path, key, dots) {
}).join(dots ? '.' : '');
}

function convertValue(value) {
if (value === null) return '';

if (utils.isDate(value)) {
return value.toISOString();
}

if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) {
return typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value);
}

return value;
}

function isFlatArray(arr) {
return utils.isArray(arr) && !arr.some(isVisitable);
}
Expand Down Expand Up @@ -64,21 +78,6 @@ function toFormData(obj, formData, options) {
throw new TypeError('visitor must be a function');
}

function convertValue(value) {
if (value === null) return '';

if (utils.isDate(value)) {
return value.toISOString();
}

if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) {
return typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value);
}

return value;
}


/**
*
* @param {*} value
Expand All @@ -88,15 +87,18 @@ function toFormData(obj, formData, options) {
* @returns {boolean} return true to visit the each prop of the value recursively
*/
function defaultVisitor(value, key, path) {
var arr;
var arr = value;

if (value && !path && typeof value === 'object') {
if (utils.endsWith(key, '{}')) {
// eslint-disable-next-line no-param-reassign
key = metaTokens ? key : key.slice(0, -2);
// eslint-disable-next-line no-param-reassign
value = JSON.stringify(value);
} else if (!utils.isPlainObject(value) && (arr = utils.toArray(value)) && isFlatArray(arr)) {
} else if (
(utils.isArray(value) && isFlatArray(value)) ||
(utils.isFileList(value) || utils.endsWith(key, '[]') && (arr = utils.toArray(value))
)) {
// eslint-disable-next-line no-param-reassign
key = removeBrackets(key);

Expand Down Expand Up @@ -138,7 +140,7 @@ function toFormData(obj, formData, options) {
stack.push(value);

utils.forEach(value, function each(el, key) {
var result = !utils.isUndefined(el) && defaultVisitor.call(
var result = !utils.isUndefined(el) && visitor.call(
formData, el, utils.isString(key) ? key.trim() : key, path, exposedHelpers
);

Expand All @@ -150,8 +152,8 @@ function toFormData(obj, formData, options) {
stack.pop();
}

if (!utils.isPlainObject(obj)) {
throw new TypeError('data must be a plain object');
if (!utils.isObject(obj)) {
throw new TypeError('data must be an object');
}

build(obj);
Expand Down
18 changes: 18 additions & 0 deletions lib/helpers/toURLEncodedForm.js
@@ -0,0 +1,18 @@
'use strict';

var utils = require('../utils');
var toFormData = require('./toFormData');
var platform = require('../platform/');

module.exports = function toURLEncodedForm(data) {
return toFormData(data, new platform.classes.URLSearchParams(), {
visitor: function(value, key, path, helpers) {
if (platform.isNode && utils.isBuffer(value)) {
this.append(key, value.toString('base64'));
return false;
}

return helpers.defaultVisitor.apply(this, arguments);
}
});
};
38 changes: 38 additions & 0 deletions lib/platform/browser/classes/URLSearchParams.js
@@ -0,0 +1,38 @@
'use strict';

module.exports = (function getURLSearchParams(nativeURLSearchParams) {
if (typeof nativeURLSearchParams === 'function') return nativeURLSearchParams;

function encode(str) {
var charMap = {
'!': '%21',
"'": '%27',
'(': '%28',
')': '%29',
'~': '%7E',
'%20': '+',
'%00': '\x00'
};
return encodeURIComponent(str).replace(/[!'\(\)~]|%20|%00/g, function replacer(match) {
return charMap[match];
});
}

function URLSearchParams() {
this.pairs = [];
}

var prototype = URLSearchParams.prototype;

prototype.append = function append(name, value) {
this.pairs.push([name, value]);
};

prototype.toString = function toString() {
return this.pairs.map(function each(pair) {
return pair[0] + '=' + encode(pair[1]);
}, '').join('&');
};

return URLSearchParams;
})(URLSearchParams);
8 changes: 8 additions & 0 deletions lib/platform/browser/index.js
@@ -0,0 +1,8 @@
'use strict';

module.exports = {
isBrowser: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
};
3 changes: 3 additions & 0 deletions lib/platform/index.js
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('./node/');
5 changes: 5 additions & 0 deletions lib/platform/node/classes/URLSearchParams.js
@@ -0,0 +1,5 @@
'use strict';

var url = require('url');

module.exports = url.URLSearchParams;
8 changes: 8 additions & 0 deletions lib/platform/node/index.js
@@ -0,0 +1,8 @@
'use strict';

module.exports = {
isNode: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
};

0 comments on commit c05ad48

Please sign in to comment.