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:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
isBrowser: true,
|
||||
classes: {
|
||||
URLSearchParams: require('./classes/URLSearchParams')
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./node/');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var url = require('url');
|
||||
|
||||
module.exports = url.URLSearchParams;
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
isNode: true,
|
||||
classes: {
|
||||
URLSearchParams: require('./classes/URLSearchParams')
|
||||
}
|
||||
};
|
||||
Generated
+5
-5
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user