Repository: node-fetch/node-fetch
Pull request #1523 (2022-03-16 20:50:30)

test: fix flaky tests
<!-- Thanks for contributing! -->

## Purpose

By relying upon a 100ms delay before aborting, it is possible for the request to succeed before being canceled.

Since these 2 tests merely ensures that cancellation via AbortController works, adding a delay isn't needed regardless.

## Changes

Removes delay between request and canceling.

## Additional information

Undici runs node-fetch's test suite (more or less) and this test is flaky (https://github.com/nodejs/undici/runs/5153562802?check_suite_focus=true for example).

Example code with the issue fixed:
```js
import fetch from 'node-fetch';
import { createServer } from 'http';
import { once } from 'events';

const server = createServer((req, res) => res.end());
server.listen(3000, () => console.log('done'));
await once(server, 'listening');

let i = 1;
while (i++) {
	try {
		const controller = new AbortController();

		const promise = fetch(`http://localhost:3000/`, {
			method: 'POST',
			signal: controller.signal,
			headers: {
				'Content-Type': 'application/json',
				body: '{"hello": "world"}'
			}
		});

		controller.abort()

		const test = await promise;
		console.log('did not fail', test);
		break;
	} catch {
		if (i % 1e3 === 0) {
			console.log('did not fail after ' + i + ' tries');
		}
	}
}
```


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4e20528..6d231e8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
 
 Thank you for considering to contribute to `node-fetch` 💖
 
-Please note that this project is released with a [Contributor Code of Conduct][./CODE_OF_CONDUCT.md].
+Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
 By participating you agree to abide by its terms.
 
 ## Setup
diff --git a/README.md b/README.md
index 6b27b2a..fce3d8f 100644
--- a/README.md
+++ b/README.md
@@ -428,7 +428,7 @@ import fetch, { FormData, File, fileFrom } from 'node-fetch'
 const httpbin = 'https://httpbin.org/post'
 const formData = new FormData()
 const binary = new Uint8Array([ 97, 98, 99 ])
-const abc = new File([binary], 'abc.txt'), { type: 'text/plain' })
+const abc = new File([binary], 'abc.txt', { type: 'text/plain' })
 
 formData.set('greeting', 'Hello, world!')
 formData.set('file-upload', abc, 'new name.txt')
@@ -537,7 +537,7 @@ If no values are set, the following request headers will be sent automatically:
 
 | Header              | Value                                                  |
 | ------------------- | ------------------------------------------------------ |
-| `Accept-Encoding`   | `gzip,deflate,br` _(when `options.compress === true`)_ |
+| `Accept-Encoding`   | `gzip, deflate, br` (when `options.compress === true`) |
 | `Accept`            | `*/*`                                                  |
 | `Connection`        | `close` _(when no `options.agent` is present)_         |
 | `Content-Length`    | _(automatically calculated, if possible)_              |
@@ -594,7 +594,7 @@ The recommended way to fix this problem is to resolve cloned response in paralle
 import fetch from 'node-fetch';
 
 const response = await fetch('https://example.com');
-const r1 = await response.clone();
+const r1 = response.clone();
 
 const results = await Promise.all([response.json(), r1.text()]);
 
@@ -669,7 +669,7 @@ See [options](#fetch-options) for exact meaning of these extensions.
 <small>_(spec-compliant)_</small>
 
 - `input` A string representing a URL, or another `Request` (which will be cloned)
-- `options` [Options][#fetch-options] for the HTTP(S) request
+- `options` [Options](#fetch-options) for the HTTP(S) request
 
 Constructs a new `Request` object. The constructor is identical to that in the [browser](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request).
 
diff --git a/src/index.js b/src/index.js
index f5f3984..82efc98 100644
--- a/src/index.js
+++ b/src/index.js
@@ -110,7 +110,9 @@ export default async function fetch(url, options_) {
 		});
 
 		fixResponseChunkedTransferBadEnding(request_, error => {
-			response.body.destroy(error);
+			if (response && response.body) {
+				response.body.destroy(error);
+			}
 		});
 
 		/* c8 ignore next 18 */
@@ -387,13 +389,7 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
 			}
 		};
 
-		socket.prependListener('close', onSocketClose);
-
-		request.on('abort', () => {
-			socket.removeListener('close', onSocketClose);
-		});
-
-		socket.on('data', buf => {
+		const onData = buf => {
 			properLastChunkReceived = Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0;
 
 			// Sometimes final 0-length chunk and end of message code are in separate packets
@@ -405,6 +401,14 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
 			}
 
 			previousChunk = buf;
+		};
+
+		socket.prependListener('close', onSocketClose);
+		socket.on('data', onData);
+
+		request.on('close', () => {
+			socket.removeListener('close', onSocketClose);
+			socket.removeListener('data', onData);
 		});
 	});
 }
diff --git a/src/request.js b/src/request.js
index a4d1b22..7a1a954 100644
--- a/src/request.js
+++ b/src/request.js
@@ -61,9 +61,11 @@ export default class Request extends Body {
 		}
 
 		let method = init.method || input.method || 'GET';
-		method = method.toUpperCase();
+		if (/^(delete|get|head|options|post|put)$/i.test(method)) {
+			method = method.toUpperCase();
+		}
 
-		if ('data' in init) {
+		if (!isRequest(init) && 'data' in init) {
 			doBadDataWarn();
 		}
 
@@ -278,7 +280,7 @@ export const getNodeRequestOptions = request => {
 
 	// HTTP-network-or-cache fetch step 2.15
 	if (request.compress && !headers.has('Accept-Encoding')) {
-		headers.set('Accept-Encoding', 'gzip,deflate,br');
+		headers.set('Accept-Encoding', 'gzip, deflate, br');
 	}
 
 	let {agent} = request;


Test patch:
diff --git a/test/main.js b/test/main.js
index 5df12c4..bc323eb 100644
--- a/test/main.js
+++ b/test/main.js
@@ -1003,9 +1003,7 @@ describe('node-fetch', () => {
 					}
 				});
 
-				setTimeout(() => {
-					controller.abort();
-				}, 100);
+				controller.abort();
 
 				return expect(promise)
 					.to.eventually.be.rejected
@@ -1029,9 +1027,7 @@ describe('node-fetch', () => {
 					})
 				];
 
-				setTimeout(() => {
-					controller.abort();
-				}, 100);
+				controller.abort();
 
 				return Promise.all(fetches.map(fetched => expect(fetched)
 					.to.eventually.be.rejected
diff --git a/test/request.js b/test/request.js
index b8ba107..03cbe3c 100644
--- a/test/request.js
+++ b/test/request.js
@@ -151,6 +151,24 @@ describe('Request', () => {
 		expect(request.headers.get('a')).to.equal('1');
 	});
 
+	it('should uppercase DELETE, GET, HEAD, OPTIONS, POST and PUT methods', () => {
+		const url = base;
+		for (const method of ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) {
+			const request = new Request(url, {
+				method: method.toLowerCase()
+			});
+			expect(request.method).to.equal(method);
+		}
+	});
+
+	it('should not uppercase unknown methods and patch', () => {
+		const url = base;
+		for (const method of ['patch', 'chicken']) {
+			const request = new Request(url, {method});
+			expect(request.method).to.equal(method);
+		}
+	});
+
 	it('should support arrayBuffer() method', () => {
 		const url = base;
 		const request = new Request(url, {