2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-05 16:42:32 +03:00

Fixed toFormData regression bug (unreleased) with Array-like objects 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>
This commit is contained in:
Dmitriy Mozgovoy
2022-05-16 09:30:17 +03:00
committed by GitHub
parent e762cf77b3
commit c05ad48952
13 changed files with 245 additions and 65 deletions
+78 -26
View File
@@ -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)
@@ -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
@@ -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).
@@ -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).
@@ -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
##### 🆕 Automatic serialization
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 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`.
@@ -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:
@@ -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.
+19 -10
View File
@@ -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'
@@ -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);
}
+1
View File
@@ -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) {
+22 -20
View File
@@ -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);
}
@@ -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
@@ -88,7 +87,7 @@ 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, '{}')) {
@@ -96,7 +95,10 @@ function toFormData(obj, formData, options) {
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);
@@ -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
);
@@ -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);
+18
View File
@@ -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);
}
});
};
@@ -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
View File
@@ -0,0 +1,8 @@
'use strict';
module.exports = {
isBrowser: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
};
+3
View File
@@ -0,0 +1,3 @@
'use strict';
module.exports = require('./node/');
@@ -0,0 +1,5 @@
'use strict';
var url = require('url');
module.exports = url.URLSearchParams;
+8
View File
@@ -0,0 +1,8 @@
'use strict';
module.exports = {
isNode: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
};
+5 -5
View File
@@ -10,7 +10,8 @@
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0"
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"devDependencies": {
"@rollup/plugin-babel": "^5.3.0",
@@ -19,6 +20,7 @@
"@rollup/plugin-multi-entry": "^4.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"abortcontroller-polyfill": "^1.7.3",
"body-parser": "^1.20.0",
"coveralls": "^3.1.1",
"dtslint": "^4.2.1",
"es6-promise": "^4.2.8",
@@ -11989,8 +11991,7 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/prr": {
"version": "1.0.1",
@@ -26801,8 +26802,7 @@
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"prr": {
"version": "1.0.1",
+3 -1
View File
@@ -37,6 +37,7 @@
"@rollup/plugin-multi-entry": "^4.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"abortcontroller-polyfill": "^1.7.3",
"body-parser": "^1.20.0",
"coveralls": "^3.1.1",
"dtslint": "^4.2.1",
"es6-promise": "^4.2.8",
@@ -79,7 +80,8 @@
},
"browser": {
"./lib/adapters/http.js": "./lib/adapters/xhr.js",
"./lib/defaults/env/FormData.js": "./lib/helpers/null.js"
"./lib/defaults/env/FormData.js": "./lib/helpers/null.js",
"./lib/platform/node/index.js": "./lib/platform/browser/index.js"
},
"jsdelivr": "dist/axios.min.js",
"unpkg": "dist/axios.min.js",
+37 -3
View File
@@ -12,8 +12,9 @@ var server, proxy;
var AxiosError = require('../../../lib/core/AxiosError');
var FormData = require('form-data');
var formidable = require('formidable');
const express = require('express');
const multer = require('multer');
var express = require('express');
var multer = require('multer');
var bodyParser = require('body-parser');
describe('supports http with nodejs', function () {
@@ -1337,9 +1338,11 @@ describe('supports http with nodejs', function () {
}).catch(done);
});
});
describe('toFormData helper', function () {
it('should properly serialize nested objects for parsing with multer.js (express.js)', function (done) {
const app = express();
var app = express();
var obj = {
arr1: ['1', '2', '3'],
arr2: ['1', ['2'], '3'],
@@ -1374,4 +1377,35 @@ describe('supports http with nodejs', function () {
});
});
});
describe('URLEncoded Form', function () {
it('should post object data as url-encoded form if content-type is application/x-www-form-urlencoded', function (done) {
var app = express();
var obj = {
arr1: ['1', '2', '3'],
arr2: ['1', ['2'], '3'],
obj: {x: '1', y: {z: '1'}},
users: [{name: 'Peter', surname: 'griffin'}, {name: 'Thomas', surname: 'Anderson'}]
};
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/', function (req, res, next) {
res.send(JSON.stringify(req.body));
});
server = app.listen(3001, function () {
return axios.post('http://localhost:3001/', obj, {
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
})
.then(function (res) {
assert.deepStrictEqual(res.data, obj);
done();
}, done);
});
});
});
});