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.
- package/dist/source/as-promise/index.js +5 -1
- package/dist/source/core/index.d.ts +2 -3
- package/dist/source/core/index.js +130 -24
- package/dist/source/core/options.d.ts +93 -9
- package/dist/source/core/options.js +77 -5
- package/dist/source/core/utils/get-body-size.js +14 -1
- package/package.json +5 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
226
|
+
const computedValue = calculateRetryDelay({
|
|
225
227
|
attemptCount,
|
|
226
228
|
retryOptions,
|
|
227
229
|
error: typedError,
|
|
228
230
|
retryAfter,
|
|
229
|
-
computedValue:
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
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",
|