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

Improved FormData support; #4448

Merged
merged 12 commits into from Mar 14, 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
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
DigitalBrainJS marked this conversation as resolved.
Show resolved Hide resolved

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;