Skip to content

Commit 670b228

Browse files
committed
Use native FormData global instead of form-data-encoder
Fixes #2077
1 parent b933476 commit 670b228

13 files changed

Lines changed: 50 additions & 216 deletions

File tree

documentation/2-options.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ stream.on('data', console.log);
296296

297297
### `body`
298298

299-
**Type: `string | Buffer | TypedArray | stream.Readable | Generator | AsyncGenerator | Iterable | AsyncIterable | FormData` or [`form-data` instance](https://github.com/form-data/form-data)**
299+
**Type: `string | Buffer | TypedArray | stream.Readable | Generator | AsyncGenerator | Iterable | AsyncIterable | FormData`**
300300

301301
The payload to send.
302302

@@ -365,12 +365,10 @@ await got.post('https://httpbin.org/anything', {
365365
});
366366
```
367367

368-
Since Got 12, you can use spec-compliant [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) objects as request body, such as [`formdata-node`](https://github.com/octet-stream/form-data) or [`formdata-polyfill`](https://github.com/jimmywarting/FormData):
368+
You can use [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) objects as request body:
369369

370370
```js
371371
import got from 'got';
372-
import {FormData} from 'formdata-node'; // or:
373-
// import {FormData} from 'formdata-polyfill/esm.min.js';
374372

375373
const form = new FormData();
376374
form.set('greeting', 'Hello, world!');

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"cacheable-request": "^13.0.18",
5858
"chunk-data": "^0.1.0",
5959
"decompress-response": "^10.0.0",
60-
"form-data-encoder": "^4.1.0",
6160
"http2-wrapper": "^2.2.1",
6261
"keyv": "^5.6.0",
6362
"lowercase-keys": "^4.0.1",
@@ -89,8 +88,6 @@
8988
"delay": "^7.0.0",
9089
"expect-type": "^1.3.0",
9190
"express": "^5.2.1",
92-
"form-data": "^4.0.5",
93-
"formdata-node": "^6.0.3",
9491
"get-stream": "^9.0.1",
9592
"nock": "^14.0.11",
9693
"node-fetch": "^3.3.2",

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ By default, Got will retry on failure. To disable this option, set [`options.ret
359359

360360
[Click here][InstallSizeOfTheDependencies] to see the install size of the Got dependencies.
361361

362-
[InstallSizeOfTheDependencies]: https://packagephobia.com/result?p=@sindresorhus/is@7.0.0,@szmarczak/http-timer@5.0.1,cacheable-lookup@7.0.0,cacheable-request@12.0.1,decompress-response@6.0.0,form-data-encoder@4.0.2,http2-wrapper@2.2.1,lowercase-keys@3.0.0,p-cancelable@4.0.1,responselike@3.0.0,type-fest@4.19.0
362+
[InstallSizeOfTheDependencies]: https://packagephobia.com/result?p=@sindresorhus/is@7.0.0,@szmarczak/http-timer@5.0.1,cacheable-lookup@7.0.0,cacheable-request@12.0.1,decompress-response@6.0.0,http2-wrapper@2.2.1,lowercase-keys@3.0.0,p-cancelable@4.0.1,responselike@3.0.0,type-fest@4.19.0
363363

364364
## Maintainers
365365

source/as-promise/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export default function asPromise<T>(firstRequest?: Request): CancelableRequest<
214214

215215
const newBody = request.options.body;
216216

217-
if (previousBody === newBody && is.nodeStream(newBody)) {
217+
if (previousBody === newBody && (is.nodeStream(newBody) || newBody instanceof ReadableStream)) {
218218
error.message = 'Cannot retry with consumed body stream';
219219

220220
onError(error);

source/core/index.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ import decompressResponse from 'decompress-response';
1515
import type {KeyvStoreAdapter} from 'keyv';
1616
import type KeyvType from 'keyv';
1717
import is, {isBuffer} from '@sindresorhus/is';
18-
import {FormDataEncoder, isFormData as isFormDataLike} from 'form-data-encoder';
1918
import type ResponseLike from 'responselike';
2019
import timer, {type ClientRequestWithTimings, type Timings, type IncomingMessageWithTimings} from './utils/timer.js';
2120
import getBodySize from './utils/get-body-size.js';
22-
import isFormData from './utils/is-form-data.js';
2321
import proxyEvents from './utils/proxy-events.js';
2422
import timedOut, {TimeoutError as TimedOutTimeoutError} from './timed-out.js';
2523
import urlToOptions from './utils/url-to-options.js';
@@ -772,24 +770,17 @@ export default class Request extends Duplex implements RequestEvents<Request> {
772770
const noContentType = !is.string(headers['content-type']);
773771

774772
if (isBody) {
775-
// Body is spec-compliant FormData
776-
if (isFormDataLike(options.body)) {
777-
const encoder = new FormDataEncoder(options.body);
773+
// Native FormData
774+
if (options.body instanceof FormData) {
775+
const response = new Response(options.body);
778776

779777
if (noContentType) {
780-
headers['content-type'] = encoder.headers['Content-Type'];
778+
headers['content-type'] = response.headers.get('content-type') ?? 'multipart/form-data';
781779
}
782780

783-
if ('Content-Length' in encoder.headers) {
784-
headers['content-length'] = encoder.headers['Content-Length'];
785-
}
786-
787-
options.body = encoder.encode();
788-
}
789-
790-
// Special case for https://github.com/form-data/form-data
791-
if (isFormData(options.body) && noContentType) {
792-
headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
781+
options.body = response.body!;
782+
} else if (Object.prototype.toString.call(options.body) === '[object FormData]') {
783+
throw new TypeError('Non-native FormData is not supported. Use globalThis.FormData instead.');
793784
}
794785
} else if (isForm) {
795786
if (noContentType) {
@@ -811,7 +802,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
811802
options.body = options.stringifyJson(json);
812803
}
813804

814-
const uploadBodySize = await getBodySize(options.body, options.headers);
805+
const uploadBodySize = getBodySize(options.body, options.headers);
815806

816807
// See https://tools.ietf.org/html/rfc7230#section-3.3.2
817808
// A user agent SHOULD send a Content-Length in a request message when

source/core/options.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import is, {assert} from '@sindresorhus/is';
1616
import lowercaseKeys from 'lowercase-keys';
1717
import CacheableLookup from 'cacheable-lookup';
1818
import http2wrapper, {type ClientHttp2Session} from 'http2-wrapper';
19-
import {isFormData, type FormDataLike} from 'form-data-encoder';
2019
import type {KeyvStoreAdapter} from 'keyv';
2120
import type KeyvType from 'keyv';
2221
import type ResponseLike from 'responselike';
@@ -1634,7 +1633,7 @@ export default class Options {
16341633
16351634
__Note #4__: This option is not enumerable and will not be merged with the instance defaults.
16361635
1637-
The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / typed array ([`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), etc.) / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
1636+
The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / typed array ([`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), etc.), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
16381637
16391638
Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
16401639
@@ -1655,12 +1654,12 @@ export default class Options {
16551654
});
16561655
```
16571656
*/
1658-
get body(): string | Uint8Array | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined {
1657+
get body(): string | Uint8Array | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormData | ArrayBufferView | undefined {
16591658
return this._internals.body;
16601659
}
16611660

1662-
set body(value: string | Uint8Array | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined) {
1663-
assertAny('body', [is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.typedArray, is.undefined], value);
1661+
set body(value: string | Uint8Array | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormData | ArrayBufferView | undefined) {
1662+
assertAny('body', [is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, is.typedArray, is.undefined], value);
16641663

16651664
if (is.nodeStream(value)) {
16661665
assert.truthy(value.readable);

source/core/utils/get-body-size.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import {promisify} from 'node:util';
21
import type {ClientRequestArgs} from 'node:http';
32
import is from '@sindresorhus/is';
4-
import isFormData from './is-form-data.js';
53

6-
export default async function getBodySize(body: unknown, headers: ClientRequestArgs['headers']): Promise<number | undefined> {
4+
export default function getBodySize(body: unknown, headers: ClientRequestArgs['headers']): number | undefined {
75
if (headers && 'content-length' in headers) {
86
return Number(headers['content-length']);
97
}
@@ -24,21 +22,5 @@ export default async function getBodySize(body: unknown, headers: ClientRequestA
2422
return (body as ArrayBufferView).byteLength;
2523
}
2624

27-
if (isFormData(body)) {
28-
try {
29-
return await promisify(body.getLength.bind(body))();
30-
} catch (error: unknown) {
31-
const typedError = error as Error;
32-
throw new Error('Cannot determine content-length for form-data with stream(s) of unknown length. '
33-
+ 'This is a limitation of the `form-data` package. '
34-
+ 'To fix this, either:\n'
35-
+ '1. Use the `knownLength` option when appending streams:\n'
36-
+ ' form.append(\'file\', stream, {knownLength: 12345});\n'
37-
+ '2. Switch to spec-compliant FormData (formdata-node package)\n'
38-
+ 'See: https://github.com/form-data/form-data#alternative-submission-methods\n'
39-
+ `Original error: ${typedError.message}`);
40-
}
41-
}
42-
4325
return undefined;
4426
}

source/core/utils/is-form-data.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

test/headers.ts

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import fs from 'node:fs';
44
import path from 'node:path';
55
import test from 'ava';
66
import type {Handler} from 'express';
7-
import FormData from 'form-data';
8-
import {FormDataEncoder} from 'form-data-encoder';
9-
import {FormData as FormDataNode} from 'formdata-node';
107
import got, {type Headers} from '../source/index.js';
118
import withServer from './helpers/with-server.js';
129

@@ -159,68 +156,31 @@ test('form manual `content-type` header', withServer, async (t, server, got) =>
159156
t.is(headers['content-type'], 'custom');
160157
});
161158

162-
test('form-data manual `content-type` header', withServer, async (t, server, got) => {
159+
test('sets `content-type` header for native FormData', withServer, async (t, server, got) => {
163160
server.post('/', echoHeaders);
164161

165-
const form = new FormData();
166-
form.append('a', 'b');
167-
const {body} = await got.post({
168-
headers: {
169-
'content-type': 'custom',
170-
},
171-
body: form,
172-
});
173-
const headers = JSON.parse(body);
174-
t.is(headers['content-type'], 'custom');
175-
});
176-
177-
test('form-data automatic `content-type` header', withServer, async (t, server, got) => {
178-
server.post('/', echoHeaders);
179-
180-
const form = new FormData();
181-
form.append('a', 'b');
182-
const {body} = await got.post({
183-
body: form,
184-
});
185-
const headers = JSON.parse(body);
186-
t.is(headers['content-type'], `multipart/form-data; boundary=${form.getBoundary()}`);
187-
});
188-
189-
test('form-data sets `content-length` header', withServer, async (t, server, got) => {
190-
server.post('/', echoHeaders);
191-
192-
const form = new FormData();
193-
form.append('a', 'b');
194-
const {body} = await got.post({body: form});
195-
const headers = JSON.parse(body);
196-
t.is(headers['content-length'], '157');
197-
});
198-
199-
test('sets `content-type` header for spec-compliant FormData', withServer, async (t, server, got) => {
200-
server.post('/', echoHeaders);
201-
202-
const form = new FormDataNode();
162+
const form = new globalThis.FormData();
203163
form.set('a', 'b');
204164
const {body} = await got.post({body: form});
205165
const headers = JSON.parse(body);
206166
t.true((headers['content-type'] as string).startsWith('multipart/form-data'));
207167
});
208168

209-
test('sets `content-length` header for spec-compliant FormData', withServer, async (t, server, got) => {
169+
test('native FormData uses chunked transfer-encoding instead of content-length', withServer, async (t, server, got) => {
210170
server.post('/', echoHeaders);
211171

212-
const form = new FormDataNode();
172+
const form = new globalThis.FormData();
213173
form.set('a', 'b');
214-
const encoder = new FormDataEncoder(form);
215174
const {body} = await got.post({body: form});
216175
const headers = JSON.parse(body);
217-
t.is(headers['content-length'], encoder.headers['Content-Length']);
176+
t.is(headers['content-length'], undefined);
177+
t.is(headers['transfer-encoding'], 'chunked');
218178
});
219179

220-
test('manual `content-type` header should be allowed with spec-compliant FormData', withServer, async (t, server, got) => {
180+
test('manual `content-type` header should be allowed with native FormData', withServer, async (t, server, got) => {
221181
server.post('/', echoHeaders);
222182

223-
const form = new FormDataNode();
183+
const form = new globalThis.FormData();
224184
form.set('a', 'b');
225185
const {body} = await got.post({
226186
headers: {

test/hooks.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {Agent as HttpAgent} from 'node:http';
33
import test from 'ava';
44
import nock from 'nock';
55
import getStream from 'get-stream';
6-
import FormData from 'form-data';
76
import sinon from 'sinon';
87
import delay from 'delay';
98
import type {Handler} from 'express';
@@ -452,8 +451,8 @@ test('returning HTTP response from a beforeRequest hook', withServer, async (t,
452451
test('returning HTTP response from a beforeRequest hook with FormData body', withServer, async (t, server, got) => {
453452
server.post('/', echoBody);
454453

455-
const form = new FormData();
456-
form.append('field', 'value');
454+
const form = new globalThis.FormData();
455+
form.set('field', 'value');
457456

458457
const data = await got.post({
459458
body: form,
@@ -632,8 +631,8 @@ test('beforeRetry allows stream body if different from original', withServer, as
632631
});
633632

634633
const generateBody = () => {
635-
const form = new FormData();
636-
form.append('A', 'B');
634+
const form = new globalThis.FormData();
635+
form.set('A', 'B');
637636
return form;
638637
};
639638

@@ -645,9 +644,7 @@ test('beforeRetry allows stream body if different from original', withServer, as
645644
hooks: {
646645
beforeRetry: [
647646
({options}) => {
648-
const form = generateBody();
649-
options.body = form;
650-
options.headers['content-type'] = `multipart/form-data; boundary=${form.getBoundary()}`;
647+
options.body = generateBody();
651648
options.headers.foo = 'bar';
652649
},
653650
],

0 commit comments

Comments
 (0)