got 14.4.8 → 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`);
@@ -449,10 +460,97 @@ export default class Request extends Duplex {
449
460
  const { options } = this;
450
461
  const { url } = options;
451
462
  this._nativeResponse = response;
452
- if (options.decompress) {
463
+ const statusCode = response.statusCode;
464
+ const { method } = options;
465
+ // Skip decompression for responses that must not have bodies per RFC 9110:
466
+ // - HEAD responses (any status code)
467
+ // - 1xx (Informational): 100, 101, 102, 103, etc.
468
+ // - 204 (No Content)
469
+ // - 205 (Reset Content)
470
+ // - 304 (Not Modified)
471
+ const hasNoBody = method === 'HEAD'
472
+ || (statusCode >= 100 && statusCode < 200)
473
+ || statusCode === 204
474
+ || statusCode === 205
475
+ || statusCode === 304;
476
+ const nativeResponse = response;
477
+ if (options.decompress && !hasNoBody) {
453
478
  response = decompressResponse(response);
454
479
  }
455
- const statusCode = response.statusCode;
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
+ }
456
554
  const typedResponse = response;
457
555
  typedResponse.statusMessage = typedResponse.statusMessage ?? http.STATUS_CODES[statusCode];
458
556
  typedResponse.url = options.url.toString();
@@ -466,10 +564,6 @@ export default class Request extends Duplex {
466
564
  this._isFromCache = typedResponse.isFromCache;
467
565
  this._responseSize = Number(response.headers['content-length']) || undefined;
468
566
  this.response = typedResponse;
469
- response.once('end', () => {
470
- this._responseSize = this._downloadedSize;
471
- this.emit('downloadProgress', this.downloadProgress);
472
- });
473
567
  response.once('error', (error) => {
474
568
  this._aborted = true;
475
569
  // Force clean-up, because some packages don't do this.
@@ -485,7 +579,6 @@ export default class Request extends Duplex {
485
579
  code: 'ECONNRESET',
486
580
  }, this));
487
581
  });
488
- this.emit('downloadProgress', this.downloadProgress);
489
582
  const rawCookies = response.headers['set-cookie'];
490
583
  if (is.object(options.cookieJar) && rawCookies) {
491
584
  let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
@@ -524,6 +617,8 @@ export default class Request extends Duplex {
524
617
  return;
525
618
  }
526
619
  this._request = undefined;
620
+ // Reset download progress for the new request
621
+ this._downloadedSize = 0;
527
622
  const updatedOptions = new Options(undefined, undefined, this.options);
528
623
  const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
529
624
  const canRewrite = statusCode !== 307 && statusCode !== 308;
@@ -589,6 +684,14 @@ export default class Request extends Duplex {
589
684
  this._beforeError(new HTTPError(typedResponse));
590
685
  return;
591
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);
592
695
  response.on('readable', () => {
593
696
  if (this._triggerRead) {
594
697
  this._read();
@@ -600,9 +703,6 @@ export default class Request extends Duplex {
600
703
  this.on('pause', () => {
601
704
  response.pause();
602
705
  });
603
- response.once('end', () => {
604
- this.push(null);
605
- });
606
706
  if (this._noPipe) {
607
707
  const success = await this._setRawBody();
608
708
  if (success) {
@@ -657,11 +757,19 @@ export default class Request extends Duplex {
657
757
  const { options } = this;
658
758
  const { timeout, url } = options;
659
759
  timer(request);
760
+ this._cancelTimeouts = timedOut(request, timeout, url);
660
761
  if (this.options.http2) {
661
762
  // Unset stream timeout, as the `timeout` option was used only for connection timeout.
662
- 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
+ });
663
772
  }
664
- this._cancelTimeouts = timedOut(request, timeout, url);
665
773
  const responseEventName = options.cache ? 'cacheableResponse' : 'response';
666
774
  request.once(responseEventName, (response) => {
667
775
  void this._onResponse(response);
@@ -697,7 +805,7 @@ export default class Request extends Duplex {
697
805
  if (is.nodeStream(body)) {
698
806
  body.pipe(currentRequest);
699
807
  }
700
- else if (is.generator(body) || is.asyncGenerator(body)) {
808
+ else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
701
809
  (async () => {
702
810
  try {
703
811
  for await (const chunk of body) {
@@ -710,11 +818,16 @@ export default class Request extends Duplex {
710
818
  }
711
819
  })();
712
820
  }
713
- else if (!is.undefined(body)) {
714
- this._writeRequest(body, undefined, () => { });
715
- 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
+ }
716
828
  }
717
- else if (this._cannotHaveBody || this._noPipe) {
829
+ else {
830
+ this._writeRequest(body, undefined, () => { });
718
831
  currentRequest.end();
719
832
  }
720
833
  }
@@ -834,7 +947,12 @@ export default class Request extends Duplex {
834
947
  this._requestOptions._request = request;
835
948
  this._requestOptions.cache = options.cache;
836
949
  this._requestOptions.body = options.body;
837
- this._prepareCache(options.cache);
950
+ try {
951
+ this._prepareCache(options.cache);
952
+ }
953
+ catch (error) {
954
+ throw new CacheError(error, this);
955
+ }
838
956
  }
839
957
  // Cache support
840
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,6 +376,10 @@ 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
 
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.
382
+
340
383
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
341
384
  - `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
342
385
  - `ECONNRESET`: Connection was forcibly closed by a peer.
@@ -360,6 +403,7 @@ export type RetryOptions = {
360
403
  backoffLimit: number;
361
404
  noise: number;
362
405
  maxRetryAfter?: number;
406
+ enforceRetryRules?: boolean;
363
407
  };
364
408
  export type CreateConnectionFunction = (options: NativeRequestOptions, oncreate: (error: NodeJS.ErrnoException, socket: Socket) => void) => Socket;
365
409
  export type CheckServerIdentityFunction = (hostname: string, certificate: DetailedPeerCertificate) => NodeJS.ErrnoException | void;
@@ -379,6 +423,24 @@ export type HttpsOptions = {
379
423
  rejectUnauthorized?: NativeRequestOptions['rejectUnauthorized'];
380
424
  checkServerIdentity?: CheckServerIdentityFunction;
381
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
+ /**
382
444
  Override the default Certificate Authorities ([from Mozilla](https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport)).
383
445
 
384
446
  @example
@@ -551,6 +613,7 @@ export type OptionsError = NodeJS.ErrnoException & {
551
613
  export type OptionsInit = Except<Partial<InternalsType>, 'hooks' | 'retry'> & {
552
614
  hooks?: Partial<Hooks>;
553
615
  retry?: Partial<RetryOptions>;
616
+ preserveHooks?: boolean;
554
617
  };
555
618
  export default class Options {
556
619
  private _unixOptions?;
@@ -675,9 +738,26 @@ export default class Options {
675
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`.
676
739
 
677
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
+ ```
678
758
  */
679
- get body(): string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined;
680
- 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);
681
761
  /**
682
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).
683
763
 
@@ -1005,6 +1085,10 @@ export default class Options {
1005
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.
1006
1086
  The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).
1007
1087
 
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.
1091
+
1008
1092
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
1009
1093
 
1010
1094
  - `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
@@ -1131,7 +1215,7 @@ export default class Options {
1131
1215
  h2session: ClientHttp2Session | undefined;
1132
1216
  decompress: boolean;
1133
1217
  prefixUrl: string | URL;
1134
- body: string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined;
1218
+ body: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined;
1135
1219
  form: Record<string, any> | undefined;
1136
1220
  url: string | URL | undefined;
1137
1221
  cookieJar: PromiseCookieJar | ToughCookieJar | undefined;
@@ -1174,6 +1258,7 @@ export default class Options {
1174
1258
  pfx: PfxType;
1175
1259
  rejectUnauthorized: boolean | undefined;
1176
1260
  checkServerIdentity: CheckServerIdentityFunction | typeof checkServerIdentity;
1261
+ servername: string | undefined;
1177
1262
  ciphers: string | undefined;
1178
1263
  honorCipherOrder: boolean | undefined;
1179
1264
  minVersion: import("tls").SecureVersion | undefined;
@@ -1192,7 +1277,11 @@ export default class Options {
1192
1277
  (hostname: string, options: import("cacheable-lookup").LookupOptions, callback: (error: NodeJS.ErrnoException | null, address: string, family: import("cacheable-lookup").IPFamily) => void): void;
1193
1278
  } | undefined;
1194
1279
  family: DnsLookupIpVersion;
1195
- agent: false | http.Agent | Agents | undefined;
1280
+ agent: false | http.Agent | {
1281
+ http2: {};
1282
+ http?: HttpAgent | false;
1283
+ https?: HttpsAgent | false;
1284
+ } | undefined;
1196
1285
  setHost: boolean;
1197
1286
  method: Method;
1198
1287
  maxHeaderSize: number | undefined;
@@ -1229,7 +1318,6 @@ export default class Options {
1229
1318
  secureProtocol?: string | undefined;
1230
1319
  sessionIdContext?: string | undefined;
1231
1320
  ticketKeys?: Buffer | undefined;
1232
- servername?: string | undefined;
1233
1321
  shared?: boolean;
1234
1322
  cacheHeuristic?: number;
1235
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,6 +1318,10 @@ 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
 
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.
1324
+
1264
1325
  By default, it retries *only* on the specified methods, status codes, and on these network errors:
1265
1326
 
1266
1327
  - `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
@@ -1287,6 +1348,7 @@ export default class Options {
1287
1348
  assert.any([is.array, is.undefined], value.statusCodes);
1288
1349
  assert.any([is.array, is.undefined], value.errorCodes);
1289
1350
  assert.any([is.number, is.undefined], value.noise);
1351
+ assert.any([is.boolean, is.undefined], value.enforceRetryRules);
1290
1352
  if (value.noise && Math.abs(value.noise) > 100) {
1291
1353
  throw new Error(`The maximum acceptable retry noise is +/- 100ms, got ${value.noise}`);
1292
1354
  }
@@ -1373,6 +1435,7 @@ export default class Options {
1373
1435
  assert.plainObject(value);
1374
1436
  assert.any([is.boolean, is.undefined], value.rejectUnauthorized);
1375
1437
  assert.any([is.function, is.undefined], value.checkServerIdentity);
1438
+ assert.any([is.string, is.undefined], value.serverName);
1376
1439
  assert.any([is.string, is.object, is.array, is.undefined], value.certificateAuthority);
1377
1440
  assert.any([is.string, is.object, is.array, is.undefined], value.key);
1378
1441
  assert.any([is.string, is.object, is.array, is.undefined], value.certificate);
@@ -1539,7 +1602,17 @@ export default class Options {
1539
1602
  const url = internals.url;
1540
1603
  let agent;
1541
1604
  if (url.protocol === 'https:') {
1542
- 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
+ }
1543
1616
  }
1544
1617
  else {
1545
1618
  agent = internals.agent.http;
@@ -1565,6 +1638,7 @@ export default class Options {
1565
1638
  pfx: https.pfx,
1566
1639
  rejectUnauthorized: https.rejectUnauthorized,
1567
1640
  checkServerIdentity: https.checkServerIdentity ?? checkServerIdentity,
1641
+ servername: https.serverName,
1568
1642
  ciphers: https.ciphers,
1569
1643
  honorCipherOrder: https.honorCipherOrder,
1570
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.8",
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",