Repository: sindresorhus/got (ts)

Issue:
Options freezing breaks abort signal
The `Options` objects [freeze the abort signal][1] if it gets passed. As a result setting the `signal` prop ends up breaking the signal under node as the builtin code sets private properties when calling `AbortController.abort()`.

This makes it impossible to pass a default abort signal to a `Got` instance as the runtime will throw when aborting.

- NodeJS `v16.15.1`
- Got `v12.3.0`

[1]: https://github.com/sindresorhus/got/blob/e032b60ff3285403b8f9627dcdd32aae39867d7a/source/core/options.ts#L2524


```javascript
import got from 'got';
const abortCtrl = new AbortController();
const got2 = got.extend({
    signal: abortCtrl.signal,
});
abortCtrl.abort();
```

```console
$ node test.mjs
node:internal/abort_controller:279
  signal[kAborted] = true;
                   ^

TypeError: Cannot assign to read only property 'Symbol(kAborted)' of object '#<AbortSignal>'
    at abortSignal (node:internal/abort_controller:279:20)
    at AbortController.abort (node:internal/abort_controller:315:5)
    at file:///tmp/test.mjs:6:11
    at ModuleJob.run (node:internal/modules/esm/module_job:198:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:385:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:61:12)
```

Pull request:
Don't freeze signal when freezing Options
Freezing the abort signal object breaks its functionality under Node.

Fixes #2099

Patch:
diff --git a/readme.md b/readme.md
index 8718b25..3195ec3 100644
--- a/readme.md
+++ b/readme.md
@@ -139,6 +139,8 @@ For advanced JSON usage, check out the [`parseJson`](documentation/2-options.md#
 
 ## Documentation
 
+By default, Got will retry on failure. To disable this option, set [`options.retry.limit`](documentation/7-retry.md#retry) to 0.
+
 #### Main API
 
 - [x] [Promise API](documentation/1-promise.md)
diff --git a/source/core/options.ts b/source/core/options.ts
index 38191f3..110e763 100644
--- a/source/core/options.ts
+++ b/source/core/options.ts
@@ -2521,6 +2521,5 @@ export default class Options {
 		Object.freeze(options.retry.methods);
 		Object.freeze(options.retry.statusCodes);
 		Object.freeze(options.context);
-		Object.freeze(options.signal);
 	}
 }


Test patch:
diff --git a/test/abort.ts b/test/abort.ts
index 3312b09..fea2c1c 100644
--- a/test/abort.ts
+++ b/test/abort.ts
@@ -271,4 +271,17 @@ if (globalThis.AbortController !== undefined) {
 			message: 'This operation was aborted.',
 		});
 	});
+
+	test('support setting the signal as a default option', async t => {
+		const controller = new AbortController();
+
+		const got2 = got.extend({signal: controller.signal});
+		const p = got2('http://example.com', {signal: controller.signal});
+		controller.abort();
+
+		await t.throwsAsync(p, {
+			code: 'ERR_ABORTED',
+			message: 'This operation was aborted.',
+		});
+	});
 }
diff --git a/test/normalize-arguments.ts b/test/normalize-arguments.ts
index dc0edcc..ca25c67 100644
--- a/test/normalize-arguments.ts
+++ b/test/normalize-arguments.ts
@@ -167,3 +167,18 @@ test('searchParams - multiple values for one key', t => {
 		['100', '200', '300'],
 	);
 });
+
+if (globalThis.AbortSignal !== undefined) {
+	test('signal does not get frozen', t => {
+		const controller = new AbortController();
+		const {signal} = controller;
+
+		const options = new Options({
+			url: new URL('http://localhost'),
+			signal,
+		});
+		options.freeze();
+
+		t.is(Object.isFrozen(options.signal), false);
+	});
+}