Repository: sindresorhus/got (ts)

Issue:
GOT not able to handle Passthrough streams
**Current Behaviour**
Hi, I am trying to upload the file stream to another server.
Please check the code below
```typescript
const fd = new FormData({
    readable: true
});
const stream = getFileStream();
fd.append('file', stream, {
    filename: 'upload.txt',
});
await got.post('http://localhost:3001/upload', {
    body: fd,
});
```

Hi, I am creating a file stream like the below snippet.

``` typescript
const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    return fileStream.pipe(passThrough);
};
```
I have to use passthrough for some reasons.

If I do something like the above I get an error
**'ERR_GOT_REQUEST_ERROR'**

But if I change **getFileStream** function like below it works basically I am not using Passthrough

``` typescript
const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    return fileStream;
};
```
If I do the same in Axios and node fetch it's working with passthrough.

**Expected Behaviour:**
It should upload the file

**Node Version** 
v14.18.2

Pull request description:
Support FormData without known length
This PR updates fixes support for FormData instances without known length. In detail:

* Updated `form-data-encoder` dependency to version 2.1.0;
* Fixed usage of headers, returned by `FormDataEncoder` instance;
* Added a test for `got.post` requests using `FormData` without known length.

Fixes #2111

#### Checklist

- [x] I have read the documentation.
- [x] I have included a pull request description of my changes.
- [x] I have included some tests.
- [ ] If it's a new feature, I have included documentation updates in both the README and the types.


Patch:
diff --git a/package.json b/package.json
index e3b47b9..8e02603 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
 		"cacheable-lookup": "^6.0.4",
 		"cacheable-request": "^7.0.2",
 		"decompress-response": "^6.0.0",
-		"form-data-encoder": "^2.0.1",
+		"form-data-encoder": "^2.1.0",
 		"get-stream": "^6.0.1",
 		"http2-wrapper": "^2.1.10",
 		"lowercase-keys": "^3.0.0",
diff --git a/source/core/index.ts b/source/core/index.ts
index 4ead97a..69dcb15 100644
--- a/source/core/index.ts
+++ b/source/core/index.ts
@@ -574,7 +574,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
 						headers['content-type'] = encoder.headers['Content-Type'];
 					}
 
-					headers['content-length'] = encoder.headers['Content-Length'];
+					if ('Content-Length' in encoder.headers) {
+						headers['content-length'] = encoder.headers['Content-Length'];
+					}
 
 					options.body = encoder.encode();
 				}


Test patch:
diff --git a/test/helpers/create-https-test-server.ts b/test/helpers/create-https-test-server.ts
index 05b5c35..3ea6a66 100644
--- a/test/helpers/create-https-test-server.ts
+++ b/test/helpers/create-https-test-server.ts
@@ -5,6 +5,7 @@ import type {SecureContextOptions} from 'tls';
 import express from 'express';
 import pify from 'pify';
 import pem from 'pem';
+import type {CreateCsr, CreateCertificate} from '../types/pem.js';
 
 export type HttpsServerOptions = {
 	commonName?: string;
@@ -25,8 +26,8 @@ export type ExtendedHttpsTestServer = {
 } & express.Express;
 
 const createHttpsTestServer = async (options: HttpsServerOptions = {}): Promise<ExtendedHttpsTestServer> => {
-	const createCsr = pify(pem.createCSR);
-	const createCertificate = pify(pem.createCertificate);
+	const createCsr = pify(pem.createCSR as CreateCsr);
+	const createCertificate = pify(pem.createCertificate as CreateCertificate);
 
 	const caCsrResult = await createCsr({commonName: 'authority'});
 	const caResult = await createCertificate({
@@ -68,7 +69,7 @@ const createHttpsTestServer = async (options: HttpsServerOptions = {}): Promise<
 
 	await pify(server.https.listen.bind(server.https))();
 
-	server.caKey = caKey;
+	server.caKey = caKey as any;
 	server.caCert = caCert;
 	server.port = (server.https.address() as net.AddressInfo).port;
 	server.url = `https://localhost:${(server.port)}`;
diff --git a/test/https.ts b/test/https.ts
index e1b80ec..8ddd2c6 100644
--- a/test/https.ts
+++ b/test/https.ts
@@ -6,10 +6,11 @@ import pify from 'pify';
 import pem from 'pem';
 import got from '../source/index.js';
 import {withHttpsServer} from './helpers/with-server.js';
+import type {CreatePrivateKey, CreateCsr, CreateCertificate} from './types/pem.js';
 
-const createPrivateKey = pify(pem.createPrivateKey);
-const createCsr = pify(pem.createCSR);
-const createCertificate = pify(pem.createCertificate);
+const createPrivateKey = pify(pem.createPrivateKey as CreatePrivateKey);
+const createCsr = pify(pem.createCSR as CreateCsr);
+const createCertificate = pify(pem.createCertificate as CreateCertificate);
 const createPkcs12 = pify(pem.createPkcs12);
 
 test('https request without ca', withHttpsServer(), async (t, server, got) => {
diff --git a/test/post.ts b/test/post.ts
index a19614c..96a8960 100644
--- a/test/post.ts
+++ b/test/post.ts
@@ -361,6 +361,36 @@ test('body - sends files with spec-compliant FormData', withServer, async (t, se
 	t.deepEqual(body, expected);
 });
 
+test('body - sends form-data with without known length', withServer, async (t, server, got) => {
+	server.post('/', echoMultipartBody);
+	const fullPath = path.resolve('test/fixtures/ok');
+
+	function getFileStream() {
+		const fileStream = fs.createReadStream(fullPath);
+		const passThrough = new stream.PassThrough();
+		fileStream.pipe(passThrough);
+		return passThrough;
+	}
+
+	const expected = {
+		file: await fsPromises.readFile(fullPath, 'utf8'),
+	};
+
+	const form = new FormDataNode();
+	form.set('file', {
+		[Symbol.toStringTag]: 'File',
+		type: 'text/plain',
+		name: 'file.txt',
+		stream() {
+			return getFileStream();
+		},
+	});
+
+	const body = await got.post({body: form}).json<typeof expected>();
+
+	t.deepEqual(body, expected);
+});
+
 test('throws on upload error', withServer, async (t, server, got) => {
 	server.post('/', defaultEndpoint);
 
diff --git a/test/types/pem.ts b/test/types/pem.ts
new file mode 100644
index 0000000..58fa3e6
--- /dev/null
+++ b/test/types/pem.ts
@@ -0,0 +1,7 @@
+import type {CertificateCreationOptions, CertificateCreationResult, PrivateKeyCreationOptions, CSRCreationOptions, Callback} from 'pem';
+
+export type CreateCertificate = (options: CertificateCreationOptions, callback: Callback<CertificateCreationResult>) => void;
+
+export type CreateCsr = (options: CSRCreationOptions, callback: Callback<{csr: string; clientKey: string}>) => void;
+
+export type CreatePrivateKey = (keyBitsize: number, options: PrivateKeyCreationOptions, callback: Callback<{key: string}>) => void;