Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added automatic payload serialization to application/x-www-form-urlencoded #4714

Merged
merged 2 commits into from May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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')
}
};