got 14.4.9 → 14.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,6 +68,7 @@ export default function asPromise(firstRequest) {
68
68
  // @ts-expect-error TS doesn't notice that CancelableRequest is a Promise
69
69
  // eslint-disable-next-line no-await-in-loop
70
70
  response = await hook(response, async (updatedOptions) => {
71
+ const preserveHooks = updatedOptions.preserveHooks ?? false;
71
72
  options.merge(updatedOptions);
72
73
  options.prefixUrl = '';
73
74
  if (updatedOptions.url) {
@@ -75,7 +76,10 @@ export default function asPromise(firstRequest) {
75
76
  }
76
77
  // Remove any further hooks for that request, because we'll call them anyway.
77
78
  // The loop continues. We don't want duplicates (asPromise recursion).
78
- options.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);
79
+ // Unless preserveHooks is true, in which case we keep the remaining hooks.
80
+ if (!preserveHooks) {
81
+ options.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);
82
+ }
79
83
  throw new RetryError(request);
80
84
  });
81
85
  if (!(is.object(response) && is.number(response.statusCode) && !is.nullOrUndefined(response.body))) {
@@ -2,7 +2,7 @@ import { Duplex } from 'node:stream';
2
2
  import { type ClientRequest } from 'node:http';
3
3
  import type { Socket } from 'node:net';
4
4
  import { type Timings } from '@szmarczak/http-timer';
5
- import Options from './options.js';
5
+ import Options, { type OptionsInit } from './options.js';
6
6
  import { type PlainResponse, type Response } from './response.js';
7
7
  import { RequestError } from './errors.js';
8
8
  type Error = NodeJS.ErrnoException;
@@ -71,7 +71,7 @@ When this event is emitted, you should reset the stream you were writing to and
71
71
 
72
72
  See `got.options.retry` for more information.
73
73
  */
74
- & ((name: 'retry', listener: (retryCount: number, error: RequestError) => void) => T);
74
+ & ((name: 'retry', listener: (retryCount: number, error: RequestError, createRetryStream: (options?: OptionsInit) => Request) => void) => T);
75
75
  export type RequestEvents<T> = {
76
76
  on: GotEventFunction<T>;
77
77
  once: GotEventFunction<T>;
@@ -99,7 +99,6 @@ export default class Request extends Duplex implements RequestEvents<Request> {
99
99
  private _bodySize?;
100
100
  private _unproxyEvents;
101
101
  private _isFromCache?;
102
- private _cannotHaveBody;
103
102
  private _triggerRead;
104
103
  private readonly _jobs;
105
104
  private _cancelTimeouts;
@@ -1,6 +1,8 @@
1
1
  import process from 'node:process';
2
2
  import { Buffer } from 'node:buffer';
3
3
  import { Duplex } from 'node:stream';
4
+ import { gunzip, inflate, brotliDecompress } from 'node:zlib';
5
+ import { promisify } from 'node:util';
4
6
  import http, { ServerResponse } from 'node:http';
5
7
  import timer from '@szmarczak/http-timer';
6
8
  import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
@@ -21,6 +23,8 @@ import isUnixSocketURL from './utils/is-unix-socket-url.js';
21
23
  import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
22
24
  const supportsBrotli = is.string(process.versions.brotli);
23
25
  const methodsWithoutBody = new Set(['GET', 'HEAD']);
26
+ // Methods that should auto-end streams when no body is provided
27
+ const methodsWithoutBodyStream = new Set(['OPTIONS', 'DELETE', 'PATCH']);
24
28
  const cacheableStore = new WeakableMap();
25
29
  const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
26
30
  const proxiedRequestEvents = [
@@ -51,7 +55,6 @@ export default class Request extends Duplex {
51
55
  _bodySize;
52
56
  _unproxyEvents;
53
57
  _isFromCache;
54
- _cannotHaveBody;
55
58
  _triggerRead;
56
59
  _cancelTimeouts;
57
60
  _removeListeners;
@@ -71,7 +74,6 @@ export default class Request extends Duplex {
71
74
  this._uploadedSize = 0;
72
75
  this._stopReading = false;
73
76
  this._pipedServerResponses = new Set();
74
- this._cannotHaveBody = false;
75
77
  this._unproxyEvents = noop;
76
78
  this._triggerRead = false;
77
79
  this._cancelTimeouts = noop;
@@ -221,19 +223,29 @@ export default class Request extends Duplex {
221
223
  }
222
224
  }
223
225
  const retryOptions = options.retry;
224
- backoff = await retryOptions.calculateDelay({
226
+ const computedValue = calculateRetryDelay({
225
227
  attemptCount,
226
228
  retryOptions,
227
229
  error: typedError,
228
230
  retryAfter,
229
- computedValue: calculateRetryDelay({
231
+ computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY,
232
+ });
233
+ // When enforceRetryRules is true, respect the retry rules (limit, methods, statusCodes, errorCodes)
234
+ // before calling the user's calculateDelay function. If computedValue is 0 (meaning retry is not allowed
235
+ // based on these rules), skip calling calculateDelay entirely.
236
+ // When false (default), always call calculateDelay, allowing it to override retry decisions.
237
+ if (retryOptions.enforceRetryRules && computedValue === 0) {
238
+ backoff = 0;
239
+ }
240
+ else {
241
+ backoff = await retryOptions.calculateDelay({
230
242
  attemptCount,
231
243
  retryOptions,
232
244
  error: typedError,
233
245
  retryAfter,
234
- computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY,
235
- }),
236
- });
246
+ computedValue,
247
+ });
248
+ }
237
249
  }
238
250
  catch (error_) {
239
251
  void this._error(new RequestError(error_.message, error_, this));
@@ -382,7 +394,6 @@ export default class Request extends Duplex {
382
394
  const isJSON = !is.undefined(options.json);
383
395
  const isBody = !is.undefined(options.body);
384
396
  const cannotHaveBody = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
385
- this._cannotHaveBody = cannotHaveBody;
386
397
  if (isForm || isJSON || isBody) {
387
398
  if (cannotHaveBody) {
388
399
  throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
@@ -462,9 +473,84 @@ export default class Request extends Duplex {
462
473
  || statusCode === 204
463
474
  || statusCode === 205
464
475
  || statusCode === 304;
476
+ const nativeResponse = response;
465
477
  if (options.decompress && !hasNoBody) {
466
478
  response = decompressResponse(response);
467
479
  }
480
+ // For revalidated cached responses (304 Not Modified), cacheable-request may return
481
+ // a ResponseLike object with the body stored in a .body property but not pushed into
482
+ // the stream. The stream is created and may be ended without data, and the 'end' event is
483
+ // never emitted, causing Got to hang waiting for it. Detect and fix this.
484
+ const nativeAsAny = nativeResponse;
485
+ if (nativeAsAny.body) {
486
+ // Mark the response as complete immediately (ResponseLike doesn't have req.res.complete)
487
+ nativeResponse.complete = true;
488
+ response.complete = true;
489
+ // Use setImmediate to check if this is a stuck revalidated response
490
+ setImmediate(() => {
491
+ // Check if the stream ended with no data (revalidated response)
492
+ if (nativeResponse.readableEnded && !nativeResponse.readableLength) {
493
+ // The body is in nativeAsAny.body but was never pushed to the stream.
494
+ // We need to push it to the native response so it flows through decompression if needed.
495
+ try {
496
+ // Push the body to the native stream
497
+ nativeResponse.push(nativeAsAny.body);
498
+ // eslint-disable-next-line unicorn/no-array-push-push
499
+ nativeResponse.push(null);
500
+ // Update download size with the cached body length
501
+ let bodyLength = 0;
502
+ if (Buffer.isBuffer(nativeAsAny.body)) {
503
+ bodyLength = nativeAsAny.body.length;
504
+ }
505
+ else if (typeof nativeAsAny.body === 'string') {
506
+ bodyLength = Buffer.byteLength(nativeAsAny.body);
507
+ }
508
+ this._downloadedSize += bodyLength;
509
+ }
510
+ catch {
511
+ // If push fails (stream already ended), we need to decompress manually for compressed responses
512
+ const encoding = nativeResponse.headers['content-encoding'];
513
+ if (encoding && Buffer.isBuffer(nativeAsAny.body)) {
514
+ // Decompress the body based on the encoding
515
+ let decompressAsync;
516
+ switch (encoding) {
517
+ case 'gzip': {
518
+ decompressAsync = promisify(gunzip);
519
+ break;
520
+ }
521
+ case 'deflate': {
522
+ decompressAsync = promisify(inflate);
523
+ break;
524
+ }
525
+ case 'br': {
526
+ decompressAsync = promisify(brotliDecompress);
527
+ break;
528
+ }
529
+ default: {
530
+ break;
531
+ }
532
+ }
533
+ if (decompressAsync) {
534
+ // Decompress asynchronously and set rawBody
535
+ void decompressAsync(nativeAsAny.body).then((decompressed) => {
536
+ response.rawBody = decompressed;
537
+ this._downloadedSize += nativeAsAny.body.length;
538
+ response.emit('end');
539
+ }).catch(() => {
540
+ // Decompression failed, use compressed body as-is
541
+ response.rawBody = nativeAsAny.body;
542
+ response.emit('end');
543
+ });
544
+ return;
545
+ }
546
+ }
547
+ // Not compressed or decompression not needed, set rawBody directly
548
+ response.rawBody = nativeAsAny.body;
549
+ response.emit('end');
550
+ }
551
+ }
552
+ });
553
+ }
468
554
  const typedResponse = response;
469
555
  typedResponse.statusMessage = typedResponse.statusMessage ?? http.STATUS_CODES[statusCode];
470
556
  typedResponse.url = options.url.toString();
@@ -478,10 +564,6 @@ export default class Request extends Duplex {
478
564
  this._isFromCache = typedResponse.isFromCache;
479
565
  this._responseSize = Number(response.headers['content-length']) || undefined;
480
566
  this.response = typedResponse;
481
- response.once('end', () => {
482
- this._responseSize = this._downloadedSize;
483
- this.emit('downloadProgress', this.downloadProgress);
484
- });
485
567
  response.once('error', (error) => {
486
568
  this._aborted = true;
487
569
  // Force clean-up, because some packages don't do this.
@@ -497,7 +579,6 @@ export default class Request extends Duplex {
497
579
  code: 'ECONNRESET',
498
580
  }, this));
499
581
  });
500
- this.emit('downloadProgress', this.downloadProgress);
501
582
  const rawCookies = response.headers['set-cookie'];
502
583
  if (is.object(options.cookieJar) && rawCookies) {
503
584
  let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
@@ -536,6 +617,8 @@ export default class Request extends Duplex {
536
617
  return;
537
618
  }
538
619
  this._request = undefined;
620
+ // Reset download progress for the new request
621
+ this._downloadedSize = 0;
539
622
  const updatedOptions = new Options(undefined, undefined, this.options);
540
623
  const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
541
624
  const canRewrite = statusCode !== 307 && statusCode !== 308;
@@ -601,6 +684,14 @@ export default class Request extends Duplex {
601
684
  this._beforeError(new HTTPError(typedResponse));
602
685
  return;
603
686
  }
687
+ // Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
688
+ const endStream = () => {
689
+ this._responseSize = this._downloadedSize;
690
+ this.emit('downloadProgress', this.downloadProgress);
691
+ this.push(null);
692
+ };
693
+ response.once('end', endStream);
694
+ this.emit('downloadProgress', this.downloadProgress);
604
695
  response.on('readable', () => {
605
696
  if (this._triggerRead) {
606
697
  this._read();
@@ -612,9 +703,6 @@ export default class Request extends Duplex {
612
703
  this.on('pause', () => {
613
704
  response.pause();
614
705
  });
615
- response.once('end', () => {
616
- this.push(null);
617
- });
618
706
  if (this._noPipe) {
619
707
  const success = await this._setRawBody();
620
708
  if (success) {
@@ -669,11 +757,19 @@ export default class Request extends Duplex {
669
757
  const { options } = this;
670
758
  const { timeout, url } = options;
671
759
  timer(request);
760
+ this._cancelTimeouts = timedOut(request, timeout, url);
672
761
  if (this.options.http2) {
673
762
  // Unset stream timeout, as the `timeout` option was used only for connection timeout.
674
- request.setTimeout(0);
763
+ // We remove all 'timeout' listeners instead of calling setTimeout(0) because:
764
+ // 1. setTimeout(0) causes a memory leak (see https://github.com/sindresorhus/got/issues/690)
765
+ // 2. With HTTP/2 connection reuse, setTimeout(0) accumulates listeners on the socket
766
+ // 3. removeAllListeners('timeout') properly cleans up without the memory leak
767
+ request.removeAllListeners('timeout');
768
+ // For HTTP/2, wait for socket and remove timeout listeners from it
769
+ request.once('socket', (socket) => {
770
+ socket.removeAllListeners('timeout');
771
+ });
675
772
  }
676
- this._cancelTimeouts = timedOut(request, timeout, url);
677
773
  const responseEventName = options.cache ? 'cacheableResponse' : 'response';
678
774
  request.once(responseEventName, (response) => {
679
775
  void this._onResponse(response);
@@ -709,7 +805,7 @@ export default class Request extends Duplex {
709
805
  if (is.nodeStream(body)) {
710
806
  body.pipe(currentRequest);
711
807
  }
712
- else if (is.generator(body) || is.asyncGenerator(body)) {
808
+ else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
713
809
  (async () => {
714
810
  try {
715
811
  for await (const chunk of body) {
@@ -722,11 +818,16 @@ export default class Request extends Duplex {
722
818
  }
723
819
  })();
724
820
  }
725
- else if (!is.undefined(body)) {
726
- this._writeRequest(body, undefined, () => { });
727
- currentRequest.end();
821
+ else if (is.undefined(body)) {
822
+ // No body to send, end the request
823
+ const cannotHaveBody = methodsWithoutBody.has(this.options.method) && !(this.options.method === 'GET' && this.options.allowGetBody);
824
+ const shouldAutoEndStream = methodsWithoutBodyStream.has(this.options.method);
825
+ if ((this._noPipe ?? false) || cannotHaveBody || currentRequest !== this || shouldAutoEndStream) {
826
+ currentRequest.end();
827
+ }
728
828
  }
729
- else if (this._cannotHaveBody || this._noPipe) {
829
+ else {
830
+ this._writeRequest(body, undefined, () => { });
730
831
  currentRequest.end();
731
832
  }
732
833
  }
@@ -846,7 +947,12 @@ export default class Request extends Duplex {
846
947
  this._requestOptions._request = request;
847
948
  this._requestOptions.cache = options.cache;
848
949
  this._requestOptions.body = options.body;
849
- this._prepareCache(options.cache);
950
+ try {
951
+ this._prepareCache(options.cache);
952
+ }
953
+ catch (error) {
954
+ throw new CacheError(error, this);
955
+ }
850
956
  }
851
957
  // Cache support
852
958
  const function_ = options.cache ? this._createCacheableRequest : request;
@@ -7,13 +7,15 @@ import type { Socket } from 'node:net';
7
7
  import CacheableLookup from 'cacheable-lookup';
8
8
  import http2wrapper, { type ClientHttp2Session } from 'http2-wrapper';
9
9
  import { type FormDataLike } from 'form-data-encoder';
10
- import type { StorageAdapter } from 'cacheable-request';
10
+ import type { KeyvStoreAdapter } from 'keyv';
11
+ import type KeyvType from 'keyv';
11
12
  import type ResponseLike from 'responselike';
12
13
  import type { IncomingMessageWithTimings } from '@szmarczak/http-timer';
13
14
  import type { CancelableRequest } from '../as-promise/types.js';
14
15
  import type { PlainResponse, Response } from './response.js';
15
16
  import type { RequestError } from './errors.js';
16
17
  import type { Delays } from './timed-out.js';
18
+ type StorageAdapter = KeyvStoreAdapter | KeyvType | Map<any, any>;
17
19
  type Promisable<T> = T | Promise<T>;
18
20
  export type DnsLookupIpVersion = undefined | 4 | 6;
19
21
  type Except<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;
@@ -272,9 +274,15 @@ export type Hooks = {
272
274
  > - When using the Stream API, this hook is ignored.
273
275
 
274
276
  **Note:**
275
- > - Calling the `retryWithMergedOptions` function will trigger `beforeRetry` hooks. If the retry is successful, all remaining `afterResponse` hooks will be called. In case of an error, `beforeRetry` hooks will be called instead.
277
+ > - Calling the `retryWithMergedOptions` function will trigger `beforeRetry` hooks. By default, remaining `afterResponse` hooks are removed to prevent duplicate execution. To preserve remaining hooks on retry, set `preserveHooks: true` in the options passed to `retryWithMergedOptions`. In case of an error, `beforeRetry` hooks will be called instead.
276
278
  Meanwhile the `init`, `beforeRequest` , `beforeRedirect` as well as already executed `afterResponse` hooks will be skipped.
277
279
 
280
+ **Note:**
281
+ > - To preserve remaining `afterResponse` hooks after calling `retryWithMergedOptions`, set `preserveHooks: true` in the options passed to `retryWithMergedOptions`. This is useful when you want hooks to run on retried requests.
282
+
283
+ **Warning:**
284
+ > - Be cautious when using `preserveHooks: true`. If a hook unconditionally calls `retryWithMergedOptions` with `preserveHooks: true`, it will create an infinite retry loop. Always ensure hooks have proper conditional logic to avoid infinite retries.
285
+
278
286
  @example
279
287
  ```
280
288
  import got from 'got';
@@ -312,6 +320,37 @@ export type Hooks = {
312
320
  mutableDefaults: true
313
321
  });
314
322
  ```
323
+
324
+ @example
325
+ ```
326
+ // Example with preserveHooks
327
+ import got from 'got';
328
+
329
+ const instance = got.extend({
330
+ hooks: {
331
+ afterResponse: [
332
+ (response, retryWithMergedOptions) => {
333
+ if (response.statusCode === 401) {
334
+ return retryWithMergedOptions({
335
+ headers: {
336
+ authorization: getNewToken()
337
+ },
338
+ preserveHooks: true // Keep remaining hooks
339
+ });
340
+ }
341
+
342
+ return response;
343
+ },
344
+ (response) => {
345
+ // This hook will run on the retried request
346
+ // (the original request is interrupted when the first hook triggers a retry)
347
+ console.log('Response received:', response.statusCode);
348
+ return response;
349
+ }
350
+ ]
351
+ }
352
+ });
353
+ ```
315
354
  */
316
355
  afterResponse: AfterResponseHook[];
317
356
  };
@@ -337,7 +376,9 @@ Delays between retries counts with function `1000 * Math.pow(2, retry) + Math.ra
337
376
  The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value.
338
377
  The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).
339
378
 
340
- __Note:__ When you provide `calculateDelay`, you take full control of retry decisions. The `limit` option is not automatically enforced - you must check `attemptCount` yourself or return `0` when `computedValue` is `0` to respect the default retry logic.
379
+ The `enforceRetryRules` property is a `boolean` that, when set to `true`, enforces the `limit`, `methods`, `statusCodes`, and `errorCodes` options before calling `calculateDelay`. Your `calculateDelay` function is only invoked when a retry is allowed based on these criteria. When `false` (default), `calculateDelay` receives the computed value but can override all retry logic.
380
+
381
+ __Note:__ When `enforceRetryRules` is `false`, you must check `computedValue` in your `calculateDelay` function to respect the default retry logic. When `true`, the retry rules are enforced automatically.
341
382
 
342
383
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
343
384
  - `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
@@ -362,6 +403,7 @@ export type RetryOptions = {
362
403
  backoffLimit: number;
363
404
  noise: number;
364
405
  maxRetryAfter?: number;
406
+ enforceRetryRules?: boolean;
365
407
  };
366
408
  export type CreateConnectionFunction = (options: NativeRequestOptions, oncreate: (error: NodeJS.ErrnoException, socket: Socket) => void) => Socket;
367
409
  export type CheckServerIdentityFunction = (hostname: string, certificate: DetailedPeerCertificate) => NodeJS.ErrnoException | void;
@@ -381,6 +423,24 @@ export type HttpsOptions = {
381
423
  rejectUnauthorized?: NativeRequestOptions['rejectUnauthorized'];
382
424
  checkServerIdentity?: CheckServerIdentityFunction;
383
425
  /**
426
+ Server name for the [Server Name Indication (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) TLS extension.
427
+
428
+ This is useful when requesting to servers that don't have a proper domain name but use a certificate with a known CN/SAN.
429
+
430
+ @example
431
+ ```
432
+ import got from 'got';
433
+
434
+ // Request to IP address with specific servername for TLS
435
+ await got('https://192.168.1.100', {
436
+ https: {
437
+ serverName: 'example.com'
438
+ }
439
+ });
440
+ ```
441
+ */
442
+ serverName?: string;
443
+ /**
384
444
  Override the default Certificate Authorities ([from Mozilla](https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport)).
385
445
 
386
446
  @example
@@ -553,6 +613,7 @@ export type OptionsError = NodeJS.ErrnoException & {
553
613
  export type OptionsInit = Except<Partial<InternalsType>, 'hooks' | 'retry'> & {
554
614
  hooks?: Partial<Hooks>;
555
615
  retry?: Partial<RetryOptions>;
616
+ preserveHooks?: boolean;
556
617
  };
557
618
  export default class Options {
558
619
  private _unixOptions?;
@@ -677,9 +738,26 @@ export default class Options {
677
738
  The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / [`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`.
678
739
 
679
740
  Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
741
+
742
+ You can use `Iterable` and `AsyncIterable` objects as request body, including Web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
743
+
744
+ @example
745
+ ```
746
+ import got from 'got';
747
+
748
+ // Using an async generator
749
+ async function* generateData() {
750
+ yield 'Hello, ';
751
+ yield 'world!';
752
+ }
753
+
754
+ await got.post('https://httpbin.org/anything', {
755
+ body: generateData()
756
+ });
757
+ ```
680
758
  */
681
- get body(): string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined;
682
- set body(value: string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined);
759
+ get body(): string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined;
760
+ set body(value: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined);
683
761
  /**
684
762
  The form body is converted to a query string using [`(new URLSearchParams(object)).toString()`](https://nodejs.org/api/url.html#url_constructor_new_urlsearchparams_obj).
685
763
 
@@ -1007,7 +1085,9 @@ export default class Options {
1007
1085
  The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value.
1008
1086
  The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).
1009
1087
 
1010
- __Note:__ When you provide `calculateDelay`, you take full control of retry decisions. The `limit` option is not automatically enforced - you must check `attemptCount` yourself or return `0` when `computedValue` is `0` to respect the default retry logic.
1088
+ The `enforceRetryRules` property is a `boolean` that, when set to `true`, enforces the `limit`, `methods`, `statusCodes`, and `errorCodes` options before calling `calculateDelay`. Your `calculateDelay` function is only invoked when a retry is allowed based on these criteria. When `false` (default), `calculateDelay` receives the computed value but can override all retry logic.
1089
+
1090
+ __Note:__ When `enforceRetryRules` is `false`, you must check `computedValue` in your `calculateDelay` function to respect the default retry logic. When `true`, the retry rules are enforced automatically.
1011
1091
 
1012
1092
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
1013
1093
 
@@ -1135,7 +1215,7 @@ export default class Options {
1135
1215
  h2session: ClientHttp2Session | undefined;
1136
1216
  decompress: boolean;
1137
1217
  prefixUrl: string | URL;
1138
- body: string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined;
1218
+ body: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined;
1139
1219
  form: Record<string, any> | undefined;
1140
1220
  url: string | URL | undefined;
1141
1221
  cookieJar: PromiseCookieJar | ToughCookieJar | undefined;
@@ -1178,6 +1258,7 @@ export default class Options {
1178
1258
  pfx: PfxType;
1179
1259
  rejectUnauthorized: boolean | undefined;
1180
1260
  checkServerIdentity: CheckServerIdentityFunction | typeof checkServerIdentity;
1261
+ servername: string | undefined;
1181
1262
  ciphers: string | undefined;
1182
1263
  honorCipherOrder: boolean | undefined;
1183
1264
  minVersion: import("tls").SecureVersion | undefined;
@@ -1196,7 +1277,11 @@ export default class Options {
1196
1277
  (hostname: string, options: import("cacheable-lookup").LookupOptions, callback: (error: NodeJS.ErrnoException | null, address: string, family: import("cacheable-lookup").IPFamily) => void): void;
1197
1278
  } | undefined;
1198
1279
  family: DnsLookupIpVersion;
1199
- agent: false | http.Agent | Agents | undefined;
1280
+ agent: false | http.Agent | {
1281
+ http2: {};
1282
+ http?: HttpAgent | false;
1283
+ https?: HttpsAgent | false;
1284
+ } | undefined;
1200
1285
  setHost: boolean;
1201
1286
  method: Method;
1202
1287
  maxHeaderSize: number | undefined;
@@ -1233,7 +1318,6 @@ export default class Options {
1233
1318
  secureProtocol?: string | undefined;
1234
1319
  sessionIdContext?: string | undefined;
1235
1320
  ticketKeys?: Buffer | undefined;
1236
- servername?: string | undefined;
1237
1321
  shared?: boolean;
1238
1322
  cacheHeuristic?: number;
1239
1323
  immutableMinTimeToLive?: number;
@@ -27,6 +27,39 @@ const getGlobalDnsCache = () => {
27
27
  globalDnsCache = new CacheableLookup();
28
28
  return globalDnsCache;
29
29
  };
30
+ // Detects and wraps QuickLRU v7+ instances to make them compatible with the StorageAdapter interface
31
+ const wrapQuickLruIfNeeded = (value) => {
32
+ // Check if this is QuickLRU v7+ using Symbol.toStringTag and the evict method (added in v7)
33
+ if (value?.[Symbol.toStringTag] === 'QuickLRU' && typeof value.evict === 'function') {
34
+ // QuickLRU v7+ uses set(key, value, {maxAge: number}) but StorageAdapter expects set(key, value, ttl)
35
+ // Wrap it to translate the interface
36
+ return {
37
+ get(key) {
38
+ return value.get(key);
39
+ },
40
+ set(key, cacheValue, ttl) {
41
+ if (ttl === undefined) {
42
+ value.set(key, cacheValue);
43
+ }
44
+ else {
45
+ value.set(key, cacheValue, { maxAge: ttl });
46
+ }
47
+ return true;
48
+ },
49
+ delete(key) {
50
+ return value.delete(key);
51
+ },
52
+ clear() {
53
+ return value.clear();
54
+ },
55
+ has(key) {
56
+ return value.has(key);
57
+ },
58
+ };
59
+ }
60
+ // QuickLRU v5 and other caches work as-is
61
+ return value;
62
+ };
30
63
  const defaultInternals = {
31
64
  request: undefined,
32
65
  agent: {
@@ -115,6 +148,8 @@ const defaultInternals = {
115
148
  calculateDelay: ({ computedValue }) => computedValue,
116
149
  backoffLimit: Number.POSITIVE_INFINITY,
117
150
  noise: 100,
151
+ // TODO: Change default to `true` in the next major version to fix https://github.com/sindresorhus/got/issues/2243
152
+ enforceRetryRules: false,
118
153
  },
119
154
  localAddress: undefined,
120
155
  method: 'GET',
@@ -129,6 +164,7 @@ const defaultInternals = {
129
164
  alpnProtocols: undefined,
130
165
  rejectUnauthorized: undefined,
131
166
  checkServerIdentity: undefined,
167
+ serverName: undefined,
132
168
  certificateAuthority: undefined,
133
169
  key: undefined,
134
170
  certificate: undefined,
@@ -386,6 +422,10 @@ export default class Options {
386
422
  if (key === 'url') {
387
423
  continue;
388
424
  }
425
+ // Never merge `preserveHooks` - it's a control flag, not a persistent option
426
+ if (key === 'preserveHooks') {
427
+ continue;
428
+ }
389
429
  if (!(key in this)) {
390
430
  throw new Error(`Unexpected option: ${key}`);
391
431
  }
@@ -452,7 +492,7 @@ export default class Options {
452
492
  throw new TypeError(`Unexpected agent option: ${key}`);
453
493
  }
454
494
  // @ts-expect-error - No idea why `value[key]` doesn't work here.
455
- assert.any([is.object, is.undefined], value[key]);
495
+ assert.any([is.object, is.undefined, (v) => v === false], value[key]);
456
496
  }
457
497
  if (this._merging) {
458
498
  Object.assign(this._internals.agent, value);
@@ -593,12 +633,29 @@ export default class Options {
593
633
  The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / [`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`.
594
634
 
595
635
  Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
636
+
637
+ You can use `Iterable` and `AsyncIterable` objects as request body, including Web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
638
+
639
+ @example
640
+ ```
641
+ import got from 'got';
642
+
643
+ // Using an async generator
644
+ async function* generateData() {
645
+ yield 'Hello, ';
646
+ yield 'world!';
647
+ }
648
+
649
+ await got.post('https://httpbin.org/anything', {
650
+ body: generateData()
651
+ });
652
+ ```
596
653
  */
597
654
  get body() {
598
655
  return this._internals.body;
599
656
  }
600
657
  set body(value) {
601
- assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, isFormData, is.undefined], value);
658
+ assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.undefined], value);
602
659
  if (is.nodeStream(value)) {
603
660
  assert.truthy(value.readable);
604
661
  }
@@ -1034,7 +1091,7 @@ export default class Options {
1034
1091
  this._internals.cache = undefined;
1035
1092
  }
1036
1093
  else {
1037
- this._internals.cache = value;
1094
+ this._internals.cache = wrapQuickLruIfNeeded(value);
1038
1095
  }
1039
1096
  }
1040
1097
  /**
@@ -1261,7 +1318,9 @@ export default class Options {
1261
1318
  The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value.
1262
1319
  The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).
1263
1320
 
1264
- __Note:__ When you provide `calculateDelay`, you take full control of retry decisions. The `limit` option is not automatically enforced - you must check `attemptCount` yourself or return `0` when `computedValue` is `0` to respect the default retry logic.
1321
+ The `enforceRetryRules` property is a `boolean` that, when set to `true`, enforces the `limit`, `methods`, `statusCodes`, and `errorCodes` options before calling `calculateDelay`. Your `calculateDelay` function is only invoked when a retry is allowed based on these criteria. When `false` (default), `calculateDelay` receives the computed value but can override all retry logic.
1322
+
1323
+ __Note:__ When `enforceRetryRules` is `false`, you must check `computedValue` in your `calculateDelay` function to respect the default retry logic. When `true`, the retry rules are enforced automatically.
1265
1324
 
1266
1325
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
1267
1326
 
@@ -1289,6 +1348,7 @@ export default class Options {
1289
1348
  assert.any([is.array, is.undefined], value.statusCodes);
1290
1349
  assert.any([is.array, is.undefined], value.errorCodes);
1291
1350
  assert.any([is.number, is.undefined], value.noise);
1351
+ assert.any([is.boolean, is.undefined], value.enforceRetryRules);
1292
1352
  if (value.noise && Math.abs(value.noise) > 100) {
1293
1353
  throw new Error(`The maximum acceptable retry noise is +/- 100ms, got ${value.noise}`);
1294
1354
  }
@@ -1375,6 +1435,7 @@ export default class Options {
1375
1435
  assert.plainObject(value);
1376
1436
  assert.any([is.boolean, is.undefined], value.rejectUnauthorized);
1377
1437
  assert.any([is.function, is.undefined], value.checkServerIdentity);
1438
+ assert.any([is.string, is.undefined], value.serverName);
1378
1439
  assert.any([is.string, is.object, is.array, is.undefined], value.certificateAuthority);
1379
1440
  assert.any([is.string, is.object, is.array, is.undefined], value.key);
1380
1441
  assert.any([is.string, is.object, is.array, is.undefined], value.certificate);
@@ -1541,7 +1602,17 @@ export default class Options {
1541
1602
  const url = internals.url;
1542
1603
  let agent;
1543
1604
  if (url.protocol === 'https:') {
1544
- agent = internals.http2 ? internals.agent : internals.agent.https;
1605
+ if (internals.http2) {
1606
+ // Ensure HTTP/2 agent is configured for connection reuse
1607
+ // If no custom agent.http2 is provided, use the global agent for connection pooling
1608
+ agent = {
1609
+ ...internals.agent,
1610
+ http2: internals.agent.http2 ?? http2wrapper.globalAgent,
1611
+ };
1612
+ }
1613
+ else {
1614
+ agent = internals.agent.https;
1615
+ }
1545
1616
  }
1546
1617
  else {
1547
1618
  agent = internals.agent.http;
@@ -1567,6 +1638,7 @@ export default class Options {
1567
1638
  pfx: https.pfx,
1568
1639
  rejectUnauthorized: https.rejectUnauthorized,
1569
1640
  checkServerIdentity: https.checkServerIdentity ?? checkServerIdentity,
1641
+ servername: https.serverName,
1570
1642
  ciphers: https.ciphers,
1571
1643
  honorCipherOrder: https.honorCipherOrder,
1572
1644
  minVersion: https.minVersion,
@@ -16,7 +16,20 @@ export default async function getBodySize(body, headers) {
16
16
  return body.length;
17
17
  }
18
18
  if (isFormData(body)) {
19
- return promisify(body.getLength.bind(body))();
19
+ try {
20
+ return await promisify(body.getLength.bind(body))();
21
+ }
22
+ catch (error) {
23
+ const typedError = error;
24
+ throw new Error('Cannot determine content-length for form-data with stream(s) of unknown length. '
25
+ + 'This is a limitation of the `form-data` package. '
26
+ + 'To fix this, either:\n'
27
+ + '1. Use the `knownLength` option when appending streams:\n'
28
+ + ' form.append(\'file\', stream, {knownLength: 12345});\n'
29
+ + '2. Switch to spec-compliant FormData (formdata-node package)\n'
30
+ + 'See: https://github.com/form-data/form-data#alternative-submission-methods\n'
31
+ + `Original error: ${typedError.message}`);
32
+ }
20
33
  }
21
34
  return undefined;
22
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "14.4.9",
3
+ "version": "14.5.0",
4
4
  "description": "Human-friendly and powerful HTTP request library for Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/got",
@@ -51,13 +51,14 @@
51
51
  "@sindresorhus/is": "^7.0.1",
52
52
  "@szmarczak/http-timer": "^5.0.1",
53
53
  "cacheable-lookup": "^7.0.0",
54
- "cacheable-request": "^12.0.1",
54
+ "cacheable-request": "^13.0.12",
55
55
  "decompress-response": "^6.0.0",
56
56
  "form-data-encoder": "^4.0.2",
57
57
  "http2-wrapper": "^2.2.1",
58
+ "keyv": "^5.5.3",
58
59
  "lowercase-keys": "^3.0.0",
59
60
  "p-cancelable": "^4.0.1",
60
- "responselike": "^3.0.0",
61
+ "responselike": "^4.0.2",
61
62
  "type-fest": "^4.26.1"
62
63
  },
63
64
  "devDependencies": {
@@ -93,6 +94,7 @@
93
94
  "p-event": "^6.0.1",
94
95
  "pem": "^1.14.8",
95
96
  "pify": "^6.1.0",
97
+ "quick-lru": "^7.2.0",
96
98
  "readable-stream": "^4.4.2",
97
99
  "request": "^2.88.2",
98
100
  "sinon": "^19.0.2",