Repository: node-fetch/node-fetch (js)

Issue:
Drop support for Form-Data?
[form-data](https://www.npmjs.com/package/form-data) isn't really spec compatible and it dose some abnormal stuff. I know many other libraries support it. But it increases the complexity to anyone who is building a http library if they decide to support it. They (we) need to learn how it works internally of how it can be read & streamed

I would like to emphasize that it's better to follow the standards instead. Any spec'ed FormData impl shouldn't need any documentation.

There are two other spec compatible FormData packages on npm that works grate on NodeJS
1. There is my own [formdata-polyfill](https://www.npmjs.com/package/formdata-polyfill)
2. And also [formdata-node](https://www.npmjs.com/package/formdata-node)

Both of wish can support appending Blob/Files backed up by the FileSystem (and also blob-like items). Unlike form-data that can only append a one-time readable streams. A FormData should be re-useable if you want to clone the body for whatever reason. (our tests cases could use the same FormData instances - but this dose not work with form-data)

This means that all other http libraries out there needs to serialize the body themself, But fear not, there are packages out there also that can convert them

- My formdata-polyfill have a utility function to convert any spec compatible FormData into a Blob so that can be posted instead `formDataToBlob(formdata)`
- octet-stream have built a [FormData Encoder](https://github.com/octet-stream/form-data-encoder) 
- And we also have a encoder built in now into node-fetch itself to convert any spec compatible FormData.

form-data have had a long time to address the read/modify parts but never got around to implement any those methods. That's b/c it's based on appending all entries directly into a readable stream, so they can't iterated over or modified them.

So my proposal really is to:
1. Stop supporting form-data
2. Throw a error or warning message that form-data is no longer supported and also mention alternative packages
3. Remove all hacks where we utilize  `form.getHeaders(), getBoundary(), getLength(), hasKnownLength()` - A FormData size should always be known/calculated. 

---

So what do you think? I would rather want this to be moved into a discussion but this repo haven't enabled it (and i can't do that)


Pull request description:
chore: completely remove support for form-data package
<!-- Thanks for contributing! -->

## Purpose
Pull the plug on deprecated form-data package

___

<!-- Mark the ones you have done and remove unnecessary ones. Add new tasks that fit (like TODOs). -->
- [ ] I updated readme
- [x] I removed some unit test(s)

___

<!-- Add `- fix #_NUMBER_` line for every PR/Issue this PR solves. Do not comma separate them. -->
- fix #1167 


Patch:
diff --git a/package.json b/package.json
index 7d86a62..f72d337 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,6 @@
     "chai-iterator": "^3.0.2",
     "chai-string": "^1.5.0",
     "coveralls": "^3.1.0",
-    "form-data": "^4.0.0",
     "formdata-node": "^4.2.4",
     "mocha": "^9.1.3",
     "p-timeout": "^5.0.0",
diff --git a/src/body.js b/src/body.js
index b0fe16b..cb79a66 100644
--- a/src/body.js
+++ b/src/body.js
@@ -158,19 +158,8 @@ export default class Body {
 		const buffer = await consumeBody(this);
 		return buffer.toString();
 	}
-
-	/**
-	 * Decode response as buffer (non-spec api)
-	 *
-	 * @return  Promise
-	 */
-	buffer() {
-		return consumeBody(this);
-	}
 }
 
-Body.prototype.buffer = deprecate(Body.prototype.buffer, 'Please use \'response.arrayBuffer()\' instead of \'response.buffer()\'', 'node-fetch#buffer');
-
 // In browsers, all properties are enumerable.
 Object.defineProperties(Body.prototype, {
 	body: {enumerable: true},
@@ -267,9 +256,7 @@ export const clone = (instance, highWaterMark) => {
 		throw new Error('cannot clone body after it is used');
 	}
 
-	// Check that body is a stream and not form-data object
-	// note: we can't clone the form-data object without having it as a dependency
-	if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
+	if (body instanceof Stream) {
 		// Tee instance body
 		p1 = new PassThrough({highWaterMark});
 		p2 = new PassThrough({highWaterMark});
@@ -283,12 +270,6 @@ export const clone = (instance, highWaterMark) => {
 	return body;
 };
 
-const getNonSpecFormDataBoundary = deprecate(
-	body => body.getBoundary(),
-	'form-data doesn\'t follow the spec and requires special treatment. Use alternative package',
-	'https://github.com/node-fetch/node-fetch/issues/1167'
-);
-
 /**
  * Performs the operation "extract a `Content-Type` value from |object|" as
  * specified in the specification:
@@ -329,11 +310,6 @@ export const extractContentType = (body, request) => {
 		return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
 	}
 
-	// Detect form data input from form-data module
-	if (body && typeof body.getBoundary === 'function') {
-		return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`;
-	}
-
 	// Body is stream - can't really do much about this
 	if (body instanceof Stream) {
 		return null;
@@ -370,11 +346,6 @@ export const getTotalBytes = request => {
 		return body.length;
 	}
 
-	// Detect form data input from form-data module
-	if (body && typeof body.getLengthSync === 'function') {
-		return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
-	}
-
 	// Body is stream
 	return null;
 };
diff --git a/src/utils/is.js b/src/utils/is.js
index 00eb89b..0b5ee6f 100644
--- a/src/utils/is.js
+++ b/src/utils/is.js
@@ -49,12 +49,7 @@ export const isBlob = object => {
  * @return {boolean}
  */
 export const isAbortSignal = object => {
-	return (
-		typeof object === 'object' && (
-			object[NAME] === 'AbortSignal' ||
-			object[NAME] === 'EventTarget'
-		)
-	);
+	return object?.[NAME] === 'AbortSignal';
 };
 
 /**


Test patch:
diff --git a/@types/index.test-d.ts b/@types/index.test-d.ts
index 4b24dcb..75e28b6 100644
--- a/@types/index.test-d.ts
+++ b/@types/index.test-d.ts
@@ -21,9 +21,6 @@ async function run() {
 		}
 	}
 
-	// Test Buffer
-	expectType<Buffer>(await getResponse.buffer());
-
 	// Test arrayBuffer
 	expectType<ArrayBuffer>(await getResponse.arrayBuffer());
 
diff --git a/test/main.js b/test/main.js
index 5df12c4..f190d88 100644
--- a/test/main.js
+++ b/test/main.js
@@ -1,7 +1,6 @@
 // Test tools
 import {lookup} from 'node:dns';
 import crypto from 'node:crypto';
-import fs from 'node:fs';
 import http from 'node:http';
 import path from 'node:path';
 import stream from 'node:stream';
@@ -15,7 +14,6 @@ import chai from 'chai';
 import chaiIterator from 'chai-iterator';
 import chaiPromised from 'chai-as-promised';
 import chaiString from 'chai-string';
-import FormData from 'form-data';
 
 import fetch, {
 	Blob,
@@ -30,7 +28,7 @@ import {FetchError as FetchErrorOrig} from '../src/errors/fetch-error.js';
 import HeadersOrig, {fromRawHeaders} from '../src/headers.js';
 import RequestOrig from '../src/request.js';
 import ResponseOrig from '../src/response.js';
-import Body, {getTotalBytes, extractContentType} from '../src/body.js';
+import Body from '../src/body.js';
 import TestServer from './utils/server.js';
 import chaiTimeout from './utils/chai-timeout.js';
 import {isDomainOrSubdomain} from '../src/utils/is.js';
@@ -1206,11 +1204,11 @@ describe('node-fetch', () => {
 
 	testAbortController('mysticatea', () => new AbortControllerMysticatea());
 
-	if (process.version > 'v15') {
-		testAbortController('native', () => new AbortController());
+	if (globalThis.AbortController) {
+		testAbortController('native', () => new globalThis.AbortController());
 	}
 
-	it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
+	it('should throw a TypeError if a signal is not of type AbortSignal', () => {
 		return Promise.all([
 			expect(fetch(`${base}inspect`, {signal: {}}))
 				.to.be.eventually.rejected
@@ -1456,63 +1454,6 @@ describe('node-fetch', () => {
 			.to.be.rejectedWith(Error, errorMessage);
 	});
 
-	it('should allow POST request with form-data as body', async () => {
-		const form = new FormData();
-		form.append('a', '1');
-
-		const url = `${base}multipart`;
-		const options = {
-			method: 'POST',
-			body: form
-		};
-		const res = await fetch(url, options);
-		const json = await res.json();
-		expect(json.method).to.equal('POST');
-		expect(json.headers['content-type']).to.startWith('multipart/form-data;boundary=');
-		expect(json.headers['content-length']).to.be.a('string');
-		expect(json.body).to.equal('a=1');
-	});
-
-	it('should allow POST request with form-data using stream as body', async () => {
-		const form = new FormData();
-		form.append('my_field', fs.createReadStream('test/utils/dummy.txt'));
-
-		const url = `${base}multipart`;
-		const options = {
-			method: 'POST',
-			body: form
-		};
-
-		const res = await fetch(url, options);
-		const json = await res.json();
-		expect(json.method).to.equal('POST');
-		expect(json.headers['content-type']).to.startWith('multipart/form-data;boundary=');
-		expect(json.headers['content-length']).to.be.undefined;
-		expect(json.body).to.contain('my_field=');
-	});
-
-	it('should allow POST request with form-data as body and custom headers', async () => {
-		const form = new FormData();
-		form.append('a', '1');
-
-		const headers = form.getHeaders();
-		headers.b = '2';
-
-		const url = `${base}multipart`;
-		const options = {
-			method: 'POST',
-			body: form,
-			headers
-		};
-		const res = await fetch(url, options);
-		const json = await res.json();
-		expect(json.method).to.equal('POST');
-		expect(json.headers['content-type']).to.startWith('multipart/form-data; boundary=');
-		expect(json.headers['content-length']).to.be.a('string');
-		expect(json.headers.b).to.equal('2');
-		expect(json.body).to.equal('a=1');
-	});
-
 	it('should support spec-compliant form-data as POST body', async () => {
 		const form = new FormDataNode();
 
@@ -1857,7 +1798,7 @@ describe('node-fetch', () => {
 			res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize));
 		});
 		return expect(
-			fetch(url).then(res => res.clone().buffer())
+			fetch(url).then(res => res.clone().arrayBuffer())
 		).to.timeout;
 	});
 
@@ -1869,7 +1810,7 @@ describe('node-fetch', () => {
 			res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize));
 		});
 		return expect(
-			fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer())
+			fetch(url, {highWaterMark: 10}).then(res => res.clone().arrayBuffer())
 		).to.timeout;
 	});
 
@@ -1886,7 +1827,7 @@ describe('node-fetch', () => {
 			res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1));
 		});
 		return expect(
-			fetch(url).then(res => res.clone().buffer())
+			fetch(url).then(res => res.clone().arrayBuffer())
 		).not.to.timeout;
 	});
 
@@ -1903,7 +1844,7 @@ describe('node-fetch', () => {
 			res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1));
 		});
 		return expect(
-			fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer())
+			fetch(url, {highWaterMark: 10}).then(res => res.clone().arrayBuffer())
 		).not.to.timeout;
 	});
 
@@ -1918,7 +1859,7 @@ describe('node-fetch', () => {
 			res.end(crypto.randomBytes((2 * 512 * 1024) - 1));
 		});
 		return expect(
-			fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer())
+			fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().arrayBuffer())
 		).not.to.timeout;
 	});
 
@@ -2079,13 +2020,13 @@ describe('node-fetch', () => {
 		expect(headers.a).to.equal('2');
 	});
 
-	it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => {
+	it('should support arrayBuffer(), blob(), text(), and json() method in Body constructor', () => {
 		const body = new Body('a=1');
 		expect(body).to.have.property('arrayBuffer');
 		expect(body).to.have.property('blob');
 		expect(body).to.have.property('text');
 		expect(body).to.have.property('json');
-		expect(body).to.have.property('buffer');
+		expect(body).to.not.have.property('buffer');
 	});
 
 	/* eslint-disable-next-line func-names */
@@ -2187,66 +2128,6 @@ describe('node-fetch', () => {
 		expect(json.headers.connection).to.equal('keep-alive');
 	});
 
-	it('should calculate content length and extract content type for each body type', () => {
-		const url = `${base}hello`;
-		const bodyContent = 'a=1';
-
-		const streamBody = stream.Readable.from(bodyContent);
-		const streamRequest = new Request(url, {
-			method: 'POST',
-			body: streamBody,
-			size: 1024
-		});
-
-		const blobBody = new Blob([bodyContent], {type: 'text/plain'});
-		const blobRequest = new Request(url, {
-			method: 'POST',
-			body: blobBody,
-			size: 1024
-		});
-
-		const formBody = new FormData();
-		formBody.append('a', '1');
-		const formRequest = new Request(url, {
-			method: 'POST',
-			body: formBody,
-			size: 1024
-		});
-
-		const bufferBody = encoder.encode(bodyContent);
-		const bufferRequest = new Request(url, {
-			method: 'POST',
-			body: bufferBody,
-			size: 1024
-		});
-
-		const stringRequest = new Request(url, {
-			method: 'POST',
-			body: bodyContent,
-			size: 1024
-		});
-
-		const nullRequest = new Request(url, {
-			method: 'GET',
-			body: null,
-			size: 1024
-		});
-
-		expect(getTotalBytes(streamRequest)).to.be.null;
-		expect(getTotalBytes(blobRequest)).to.equal(blobBody.size);
-		expect(getTotalBytes(formRequest)).to.not.be.null;
-		expect(getTotalBytes(bufferRequest)).to.equal(bufferBody.length);
-		expect(getTotalBytes(stringRequest)).to.equal(bodyContent.length);
-		expect(getTotalBytes(nullRequest)).to.equal(0);
-
-		expect(extractContentType(streamBody)).to.be.null;
-		expect(extractContentType(blobBody)).to.equal('text/plain');
-		expect(extractContentType(formBody)).to.startWith('multipart/form-data');
-		expect(extractContentType(bufferBody)).to.be.null;
-		expect(extractContentType(bodyContent)).to.equal('text/plain;charset=UTF-8');
-		expect(extractContentType(null)).to.be.null;
-	});
-
 	it('should encode URLs as UTF-8', async () => {
 		const url = `${base}möbius`;
 		const res = await fetch(url);
diff --git a/test/request.js b/test/request.js
index b8ba107..761dc2a 100644
--- a/test/request.js
+++ b/test/request.js
@@ -3,10 +3,8 @@ import http from 'node:http';
 
 import AbortController from 'abort-controller';
 import chai from 'chai';
-import FormData from 'form-data';
-import Blob from 'fetch-blob';
 
-import {Request} from '../src/index.js';
+import {Request, FormData, Blob} from '../src/index.js';
 import TestServer from './utils/server.js';
 
 const {expect} = chai;
@@ -78,7 +76,7 @@ describe('Request', () => {
 		expect(r2.method).to.equal('POST');
 		expect(r2.signal).to.equal(signal);
 		// Note that we didn't clone the body
-		expect(r2.body).to.equal(form);
+		// expect(r2.body).to.equal(form);
 		expect(r1.follow).to.equal(1);
 		expect(r2.follow).to.equal(2);
 		expect(r1.counter).to.equal(0);
@@ -189,18 +187,6 @@ describe('Request', () => {
 		});
 	});
 
-	it('should support buffer() method', () => {
-		const url = base;
-		const request = new Request(url, {
-			method: 'POST',
-			body: 'a=1'
-		});
-		expect(request.url).to.equal(url);
-		return request.buffer().then(result => {
-			expect(result.toString()).to.equal('a=1');
-		});
-	});
-
 	it('should support blob() method', async () => {
 		const url = base;
 		const request = new Request(url, {
diff --git a/test/response.js b/test/response.js
index 714cda1..f587948 100644
--- a/test/response.js
+++ b/test/response.js
@@ -91,13 +91,6 @@ describe('Response', () => {
 		});
 	});
 
-	it('should support buffer() method', () => {
-		const res = new Response('a=1');
-		return res.buffer().then(result => {
-			expect(result.toString()).to.equal('a=1');
-		});
-	});
-
 	it('should support blob() method', () => {
 		const res = new Response('a=1', {
 			method: 'POST',