Skip to content

Commit

Permalink
Improved FormData support; (#4448)
Browse files Browse the repository at this point in the history
* Fixed isFormData predicate;
Added support for automatic object serialization to FormData if `Content-Type` is `multipart/form-data`;
Added support for FormData to be overloaded using `config.env.FormData` option;
Added support for FormData in node.js environment through `form-data` package;

* Added the `form-data` package as a dependency for the server build;
Added tests for FormData payload;

* Added FormData automatic serialization section;
Refactored cancellation section;

* Reworked toFormData helper;
Expose toFormData helper as a static method;
Refactored transform request;
Added kindOf, kindOfTest, endsWith, isTypedArray util;
Refactored utils.js to use kindOf for tests;

* Fixed isFormData predicate; (#4413)

Added support for automatic object serialization to FormData if `Content-Type` is `multipart/form-data`;
Added support for FormData to be overloaded using `config.env.FormData` option;
Added support for FormData in node.js environment using `form-data` package;

(cherry picked from commit 73e3bdb)

* Added shortcut methods `postForm`, `putForm`, `patchForm` to submit a Form;
Added ability to submit FileList object as a FormData;
Updated README.md;

* Updated README.md;
  • Loading branch information
DigitalBrainJS committed Mar 14, 2022
1 parent c9aca75 commit 6b9b05b
Show file tree
Hide file tree
Showing 22 changed files with 713 additions and 239 deletions.
129 changes: 100 additions & 29 deletions README.md
Expand Up @@ -23,7 +23,7 @@ Promise based HTTP client for the browser and node.js
- [Example](#example)
- [Axios API](#axios-api)
- [Request method aliases](#request-method-aliases)
- [Concurrency (Deprecated)](#concurrency-deprecated)
- [Concurrency 👎](#concurrency-deprecated)
- [Creating an instance](#creating-an-instance)
- [Instance methods](#instance-methods)
- [Request Config](#request-config)
Expand All @@ -36,11 +36,15 @@ Promise based HTTP client for the browser and node.js
- [Multiple Interceptors](#multiple-interceptors)
- [Handling Errors](#handling-errors)
- [Cancellation](#cancellation)
- [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)
- [Semver](#semver)
- [Promises](#promises)
- [TypeScript](#typescript)
Expand Down Expand Up @@ -483,6 +487,11 @@ These are the available config options for making requests. Only the `url` is re

// throw ETIMEDOUT error instead of generic ECONNABORTED on request timeouts
clarifyTimeoutError: false,
},

env: {
// The FormData class to be used to automatically serialize the payload into a FormData object
FormData: window?.FormData || global?.FormData
}
}
```
Expand Down Expand Up @@ -707,10 +716,30 @@ axios.get('/user/12345')
## Cancellation
You can cancel a request using a *cancel token*.
### AbortController
Starting from `v0.22.0` Axios supports AbortController to cancel requests in fetch API way:
```js
const controller = new AbortController();

axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
```
### CancelToken `👎deprecated`
You can also cancel a request using a *CancelToken*.
> The axios cancel token API is based on the withdrawn [cancelable promises proposal](https://github.com/tc39/proposal-cancelable-promises).
> This API is deprecated since v0.22.0 and shouldn't be used in new projects
You can create a cancel token using the `CancelToken.source` factory as shown below:
```js
Expand Down Expand Up @@ -754,22 +783,11 @@ axios.get('/user/12345', {
cancel();
```
Axios supports AbortController to abort requests in [`fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#aborting_a_fetch) way:
```js
const controller = new AbortController();

axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
```

> Note: you can cancel several requests with the same cancel token/abort controller.
> If a cancellation token is already cancelled at the moment of starting an Axios request, then the request is cancelled immediately, without any attempts to make real request.
> During the transition period, you can use both cancellation APIs, even for the same request:
## Using application/x-www-form-urlencoded format
By default, axios serializes JavaScript objects to `JSON`. To send data in the `application/x-www-form-urlencoded` format instead, you can use one of the following options.
Expand Down Expand Up @@ -829,11 +847,75 @@ axios.post('http://something.com/', params.toString());
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 with that use case (https://github.com/nodejs/node-v0.x-archive/issues/1665).
> 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
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`.
The following request will submit the data in a FormData format (Browser & Node.js):
```js
import axios from 'axios';

axios.post('https://httpbin.org/post', {x: 1}, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(({data})=> console.log(data));
```
In the `node.js` build, the ([`form-data`](https://github.com/form-data/form-data)) polyfill is used by default.
You can overload the FormData class by setting the `env.FormData` config variable,
but you probably won't need it in most cases:
```js
const axios= require('axios');
var FormData = require('form-data');

axios.post('https://httpbin.org/post', {x: 1, buf: new Buffer(10)}, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(({data})=> console.log(data));
```
Axios FormData serializer supports some special endings to perform the following operations:
- `{}` - serialize the value with JSON.stringify
- `[]` - unwrap the array like object as separate fields with the same key
```js
const axios= require('axios');

axios.post('https://httpbin.org/post', {
'myObj{}': {x: 1, s: "foo"},
'files[]': document.querySelector('#fileInput').files
}, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(({data})=> console.log(data));
```
Axios supports the following shortcut methods: `postForm`, `putForm`, `patchForm`
which are just the corresponding http methods with a header preset: `Content-Type`: `multipart/form-data`.
FileList object can be passed directly:
```js
await axios.postForm('https://httpbin.org/post', document.querySelector('#fileInput').files)
```
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
Expand All @@ -844,18 +926,7 @@ 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, { headers: form.getHeaders() })
```

Alternatively, use an interceptor:

```js
axios.interceptors.request.use(config => {
if (config.data instanceof FormData) {
Object.assign(config.headers, config.data.getHeaders());
}
return config;
});
axios.post('https://example.com', form)
```
## Semver
Expand Down
6 changes: 6 additions & 0 deletions index.d.ts
Expand Up @@ -107,6 +107,9 @@ export interface AxiosRequestConfig<D = any> {
transitional?: TransitionalOptions;
signal?: AbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
};
}

export interface HeadersDefaults {
Expand Down Expand Up @@ -197,6 +200,9 @@ export class Axios {
post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
}

export interface AxiosInstance extends Axios {
Expand Down
5 changes: 4 additions & 1 deletion lib/adapters/http.js
Expand Up @@ -87,7 +87,10 @@ module.exports = function httpAdapter(config) {
headers['User-Agent'] = 'axios/' + VERSION;
}

if (data && !utils.isStream(data)) {
// support for https://www.npmjs.com/package/form-data api
if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {
Object.assign(headers, data.getHeaders());
} else if (data && !utils.isStream(data)) {
if (Buffer.isBuffer(data)) {
// Nothing to do...
} else if (utils.isArrayBuffer(data)) {
Expand Down
1 change: 1 addition & 0 deletions lib/axios.js
Expand Up @@ -41,6 +41,7 @@ axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;
axios.toFormData = require('./helpers/toFormData');

// Expose all/spread
axios.all = function all(promises) {
Expand Down
24 changes: 17 additions & 7 deletions lib/core/Axios.js
Expand Up @@ -136,13 +136,23 @@ utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};

function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url: url,
data: data
}));
};
}

Axios.prototype[method] = generateHTTPMethod();

Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

module.exports = Axios;
17 changes: 16 additions & 1 deletion lib/defaults.js
Expand Up @@ -3,6 +3,7 @@
var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
var enhanceError = require('./core/enhanceError');
var toFormData = require('./helpers/toFormData');

var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
Expand Down Expand Up @@ -71,10 +72,20 @@ var defaults = {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {

var isObjectPayload = utils.isObject(data);
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());
} else if (isObjectPayload || contentType === 'application/json') {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}

return data;
}],

Expand Down Expand Up @@ -112,6 +123,10 @@ var defaults = {
maxContentLength: -1,
maxBodyLength: -1,

env: {
FormData: require('./defaults/env/FormData')
},

validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
Expand Down
2 changes: 2 additions & 0 deletions lib/defaults/env/FormData.js
@@ -0,0 +1,2 @@
// eslint-disable-next-line strict
module.exports = require('form-data');
2 changes: 2 additions & 0 deletions lib/helpers/null.js
@@ -0,0 +1,2 @@
// eslint-disable-next-line strict
module.exports = null;

0 comments on commit 6b9b05b

Please sign in to comment.