got 14.6.0 → 14.6.2
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 +16 -8
- package/dist/source/core/diagnostics-channel.d.ts +89 -0
- package/dist/source/core/diagnostics-channel.js +49 -0
- package/dist/source/core/errors.d.ts +14 -1
- package/dist/source/core/errors.js +27 -19
- package/dist/source/core/index.d.ts +6 -1
- package/dist/source/core/index.js +277 -100
- package/dist/source/core/options.d.ts +153 -4
- package/dist/source/core/options.js +79 -8
- package/dist/source/core/response.d.ts +2 -0
- package/dist/source/core/response.js +2 -2
- package/dist/source/core/timed-out.d.ts +1 -0
- package/dist/source/core/timed-out.js +2 -3
- package/dist/source/core/utils/get-body-size.js +1 -2
- package/dist/source/core/utils/weakable-map.d.ts +0 -1
- package/dist/source/core/utils/weakable-map.js +2 -6
- package/dist/source/index.d.ts +1 -0
- package/dist/source/index.js +1 -0
- package/package.json +3 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { Duplex
|
|
3
|
+
import { Duplex } from 'node:stream';
|
|
4
4
|
import http, { ServerResponse } from 'node:http';
|
|
5
|
+
import { byteLength } from 'byte-counter';
|
|
5
6
|
import timer from '@szmarczak/http-timer';
|
|
6
7
|
import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
|
|
7
8
|
import decompressResponse from 'decompress-response';
|
|
@@ -19,12 +20,16 @@ import { isResponseOk } from './response.js';
|
|
|
19
20
|
import isClientRequest from './utils/is-client-request.js';
|
|
20
21
|
import isUnixSocketURL, { getUnixSocketPath } from './utils/is-unix-socket-url.js';
|
|
21
22
|
import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
|
|
23
|
+
import { generateRequestId, publishRequestCreate, publishRequestStart, publishResponseStart, publishResponseEnd, publishRetry, publishError, publishRedirect, } from './diagnostics-channel.js';
|
|
22
24
|
const supportsBrotli = is.string(process.versions.brotli);
|
|
25
|
+
const supportsZstd = is.string(process.versions.zstd);
|
|
23
26
|
const methodsWithoutBody = new Set(['GET', 'HEAD']);
|
|
24
27
|
// Methods that should auto-end streams when no body is provided
|
|
25
28
|
const methodsWithoutBodyStream = new Set(['OPTIONS', 'DELETE', 'PATCH']);
|
|
26
29
|
const cacheableStore = new WeakableMap();
|
|
27
30
|
const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
|
|
31
|
+
// Track errors that have been processed by beforeError hooks to preserve custom error types
|
|
32
|
+
const errorsProcessedByHooks = new WeakSet();
|
|
28
33
|
const proxiedRequestEvents = [
|
|
29
34
|
'socket',
|
|
30
35
|
'connect',
|
|
@@ -33,17 +38,6 @@ const proxiedRequestEvents = [
|
|
|
33
38
|
'upgrade',
|
|
34
39
|
];
|
|
35
40
|
const noop = () => { };
|
|
36
|
-
/**
|
|
37
|
-
Stream transform that counts bytes passing through.
|
|
38
|
-
Used to track compressed bytes before decompression for content-length validation.
|
|
39
|
-
*/
|
|
40
|
-
class ByteCounter extends Transform {
|
|
41
|
-
count = 0;
|
|
42
|
-
_transform(chunk, _encoding, callback) {
|
|
43
|
-
this.count += chunk.length;
|
|
44
|
-
callback(null, chunk);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
41
|
export default class Request extends Duplex {
|
|
48
42
|
// @ts-expect-error - Ignoring for now.
|
|
49
43
|
['constructor'];
|
|
@@ -52,28 +46,30 @@ export default class Request extends Duplex {
|
|
|
52
46
|
options;
|
|
53
47
|
response;
|
|
54
48
|
requestUrl;
|
|
55
|
-
redirectUrls;
|
|
56
|
-
retryCount;
|
|
57
|
-
_stopReading;
|
|
58
|
-
_stopRetry;
|
|
59
|
-
_downloadedSize;
|
|
60
|
-
_uploadedSize;
|
|
61
|
-
_pipedServerResponses;
|
|
49
|
+
redirectUrls = [];
|
|
50
|
+
retryCount = 0;
|
|
51
|
+
_stopReading = false;
|
|
52
|
+
_stopRetry = noop;
|
|
53
|
+
_downloadedSize = 0;
|
|
54
|
+
_uploadedSize = 0;
|
|
55
|
+
_pipedServerResponses = new Set();
|
|
62
56
|
_request;
|
|
63
57
|
_responseSize;
|
|
64
58
|
_bodySize;
|
|
65
|
-
_unproxyEvents;
|
|
59
|
+
_unproxyEvents = noop;
|
|
66
60
|
_isFromCache;
|
|
67
|
-
_triggerRead;
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
_triggerRead = false;
|
|
62
|
+
_jobs = [];
|
|
63
|
+
_cancelTimeouts = noop;
|
|
64
|
+
_removeListeners = noop;
|
|
70
65
|
_nativeResponse;
|
|
71
|
-
_flushed;
|
|
72
|
-
_aborted;
|
|
66
|
+
_flushed = false;
|
|
67
|
+
_aborted = false;
|
|
73
68
|
_expectedContentLength;
|
|
74
|
-
|
|
69
|
+
_compressedBytesCount;
|
|
70
|
+
_requestId = generateRequestId();
|
|
75
71
|
// We need this because `this._request` if `undefined` when using cache
|
|
76
|
-
_requestInitialized;
|
|
72
|
+
_requestInitialized = false;
|
|
77
73
|
constructor(url, options, defaults) {
|
|
78
74
|
super({
|
|
79
75
|
// Don't destroy immediately, as the error may be emitted on unsuccessful retry
|
|
@@ -81,23 +77,8 @@ export default class Request extends Duplex {
|
|
|
81
77
|
// It needs to be zero because we're just proxying the data to another stream
|
|
82
78
|
highWaterMark: 0,
|
|
83
79
|
});
|
|
84
|
-
this._downloadedSize = 0;
|
|
85
|
-
this._uploadedSize = 0;
|
|
86
|
-
this._stopReading = false;
|
|
87
|
-
this._pipedServerResponses = new Set();
|
|
88
|
-
this._unproxyEvents = noop;
|
|
89
|
-
this._triggerRead = false;
|
|
90
|
-
this._cancelTimeouts = noop;
|
|
91
|
-
this._removeListeners = noop;
|
|
92
|
-
this._jobs = [];
|
|
93
|
-
this._flushed = false;
|
|
94
|
-
this._requestInitialized = false;
|
|
95
|
-
this._aborted = false;
|
|
96
|
-
this.redirectUrls = [];
|
|
97
|
-
this.retryCount = 0;
|
|
98
|
-
this._stopRetry = noop;
|
|
99
80
|
this.on('pipe', (source) => {
|
|
100
|
-
if (source?.headers) {
|
|
81
|
+
if (this.options.copyPipedHeaders && source?.headers) {
|
|
101
82
|
Object.assign(this.options.headers, source.headers);
|
|
102
83
|
}
|
|
103
84
|
});
|
|
@@ -115,6 +96,12 @@ export default class Request extends Duplex {
|
|
|
115
96
|
this.options.url = '';
|
|
116
97
|
}
|
|
117
98
|
this.requestUrl = this.options.url;
|
|
99
|
+
// Publish request creation event
|
|
100
|
+
publishRequestCreate({
|
|
101
|
+
requestId: this._requestId,
|
|
102
|
+
url: this.options.url?.toString() ?? '',
|
|
103
|
+
method: this.options.method,
|
|
104
|
+
});
|
|
118
105
|
}
|
|
119
106
|
catch (error) {
|
|
120
107
|
const { options } = error;
|
|
@@ -285,6 +272,8 @@ export default class Request extends Duplex {
|
|
|
285
272
|
if (this.destroyed) {
|
|
286
273
|
return;
|
|
287
274
|
}
|
|
275
|
+
// Capture body BEFORE hooks run to detect reassignment
|
|
276
|
+
const bodyBeforeHooks = this.options.body;
|
|
288
277
|
try {
|
|
289
278
|
for (const hook of this.options.hooks.beforeRetry) {
|
|
290
279
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -292,14 +281,58 @@ export default class Request extends Duplex {
|
|
|
292
281
|
}
|
|
293
282
|
}
|
|
294
283
|
catch (error_) {
|
|
295
|
-
void this._error(new RequestError(error_.message,
|
|
284
|
+
void this._error(new RequestError(error_.message, error_, this));
|
|
296
285
|
return;
|
|
297
286
|
}
|
|
298
287
|
// Something forced us to abort the retry
|
|
299
288
|
if (this.destroyed) {
|
|
300
289
|
return;
|
|
301
290
|
}
|
|
302
|
-
|
|
291
|
+
// Preserve stream body reassigned in beforeRetry hooks.
|
|
292
|
+
const bodyAfterHooks = this.options.body;
|
|
293
|
+
const bodyWasReassigned = bodyBeforeHooks !== bodyAfterHooks;
|
|
294
|
+
// Resource cleanup and preservation logic for retry with body reassignment.
|
|
295
|
+
// The Promise wrapper (as-promise/index.ts) compares body identity to detect consumed streams,
|
|
296
|
+
// so we must preserve the body reference across destroy(). However, destroy() calls _destroy()
|
|
297
|
+
// which destroys this.options.body, creating a complex dance of clear/restore operations.
|
|
298
|
+
//
|
|
299
|
+
// Key constraints:
|
|
300
|
+
// 1. If body was reassigned, we must NOT destroy the NEW stream (it will be used for retry)
|
|
301
|
+
// 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks
|
|
302
|
+
// 3. We must restore the body reference after destroy() for identity checks in promise wrapper
|
|
303
|
+
// 4. We cannot use the normal setter after destroy() because it validates stream readability
|
|
304
|
+
if (bodyWasReassigned) {
|
|
305
|
+
const oldBody = bodyBeforeHooks;
|
|
306
|
+
// Temporarily clear body to prevent destroy() from destroying the new stream
|
|
307
|
+
this.options.body = undefined;
|
|
308
|
+
this.destroy();
|
|
309
|
+
// Clean up the old stream resource if it's a stream and different from new body
|
|
310
|
+
// (edge case: if old and new are same stream object, don't destroy it)
|
|
311
|
+
if (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {
|
|
312
|
+
oldBody.destroy();
|
|
313
|
+
}
|
|
314
|
+
// Restore new body for promise wrapper's identity check
|
|
315
|
+
// We bypass the setter because it validates stream.readable (which fails for destroyed request)
|
|
316
|
+
// Type assertion is necessary here to access private _internals without exposing internal API
|
|
317
|
+
if (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {
|
|
318
|
+
throw new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');
|
|
319
|
+
}
|
|
320
|
+
this.options._internals.body = bodyAfterHooks;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Body wasn't reassigned - use normal destroy flow which handles body cleanup
|
|
324
|
+
this.destroy();
|
|
325
|
+
// Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()
|
|
326
|
+
// and should not be accessed. The promise wrapper will see that body identity hasn't changed
|
|
327
|
+
// and will detect it's a consumed stream, which is the correct behavior.
|
|
328
|
+
}
|
|
329
|
+
// Publish retry event
|
|
330
|
+
publishRetry({
|
|
331
|
+
requestId: this._requestId,
|
|
332
|
+
retryCount: this.retryCount + 1,
|
|
333
|
+
error: typedError,
|
|
334
|
+
delay: backoff,
|
|
335
|
+
});
|
|
303
336
|
this.emit('retry', this.retryCount + 1, error, (updatedOptions) => {
|
|
304
337
|
const request = new Request(options.url, updatedOptions, options);
|
|
305
338
|
request.retryCount = this.retryCount + 1;
|
|
@@ -403,8 +436,15 @@ export default class Request extends Duplex {
|
|
|
403
436
|
timings.phases.total = timings.end - timings.start;
|
|
404
437
|
}
|
|
405
438
|
}
|
|
406
|
-
|
|
407
|
-
|
|
439
|
+
// Preserve custom errors returned by beforeError hooks.
|
|
440
|
+
// For other errors, wrap non-RequestError instances for consistency.
|
|
441
|
+
if (error !== null && !is.undefined(error)) {
|
|
442
|
+
const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
|
|
443
|
+
if (!processedByHooks && !(error instanceof RequestError)) {
|
|
444
|
+
error = error instanceof Error
|
|
445
|
+
? new RequestError(error.message, error, this)
|
|
446
|
+
: new RequestError(String(error), {}, this);
|
|
447
|
+
}
|
|
408
448
|
}
|
|
409
449
|
callback(error);
|
|
410
450
|
}
|
|
@@ -423,9 +463,9 @@ export default class Request extends Duplex {
|
|
|
423
463
|
}
|
|
424
464
|
_checkContentLengthMismatch() {
|
|
425
465
|
if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
|
|
426
|
-
// Use
|
|
466
|
+
// Use compressed bytes count when available (for compressed responses),
|
|
427
467
|
// otherwise use _downloadedSize (for uncompressed responses)
|
|
428
|
-
const actualSize = this.
|
|
468
|
+
const actualSize = this._compressedBytesCount ?? this._downloadedSize;
|
|
429
469
|
if (actualSize !== this._expectedContentLength) {
|
|
430
470
|
this._beforeError(new ReadError({
|
|
431
471
|
message: `Content-Length mismatch: expected ${this._expectedContentLength} bytes, received ${actualSize} bytes`,
|
|
@@ -528,9 +568,9 @@ export default class Request extends Duplex {
|
|
|
528
568
|
// When strictContentLength is enabled, track compressed bytes by listening to
|
|
529
569
|
// the native response's data events before decompression
|
|
530
570
|
if (options.strictContentLength) {
|
|
531
|
-
this.
|
|
571
|
+
this._compressedBytesCount = 0;
|
|
532
572
|
this._nativeResponse.on('data', (chunk) => {
|
|
533
|
-
this.
|
|
573
|
+
this._compressedBytesCount += byteLength(chunk);
|
|
534
574
|
});
|
|
535
575
|
}
|
|
536
576
|
response = decompressResponse(response);
|
|
@@ -548,6 +588,14 @@ export default class Request extends Duplex {
|
|
|
548
588
|
this._isFromCache = typedResponse.isFromCache;
|
|
549
589
|
this._responseSize = Number(response.headers['content-length']) || undefined;
|
|
550
590
|
this.response = typedResponse;
|
|
591
|
+
// Publish response start event
|
|
592
|
+
publishResponseStart({
|
|
593
|
+
requestId: this._requestId,
|
|
594
|
+
url: typedResponse.url,
|
|
595
|
+
statusCode,
|
|
596
|
+
headers: response.headers,
|
|
597
|
+
isFromCache: typedResponse.isFromCache,
|
|
598
|
+
});
|
|
551
599
|
// Workaround for http-timer bug: when connecting to an IP address (no DNS lookup),
|
|
552
600
|
// http-timer sets lookup = connect instead of lookup = socket, resulting in
|
|
553
601
|
// dns = lookup - socket being a small positive number instead of 0.
|
|
@@ -560,6 +608,15 @@ export default class Request extends Duplex {
|
|
|
560
608
|
// Recalculate TCP time to be the full time from socket to connect
|
|
561
609
|
timings.phases.tcp = timings.connect - timings.socket;
|
|
562
610
|
}
|
|
611
|
+
// Workaround for http-timer limitation with HTTP/2:
|
|
612
|
+
// When using HTTP/2, the socket is a proxy that http-timer discards,
|
|
613
|
+
// so lookup, connect, and secureConnect events are never captured.
|
|
614
|
+
// This results in phases.request being NaN (undefined - undefined).
|
|
615
|
+
// Set it to undefined to be consistent with other unavailable timings.
|
|
616
|
+
// See https://github.com/sindresorhus/got/issues/1958
|
|
617
|
+
if (timings && Number.isNaN(timings.phases.request)) {
|
|
618
|
+
timings.phases.request = undefined;
|
|
619
|
+
}
|
|
563
620
|
response.once('error', (error) => {
|
|
564
621
|
this._aborted = true;
|
|
565
622
|
// Force clean-up, because some packages don't do this.
|
|
@@ -662,12 +719,18 @@ export default class Request extends Duplex {
|
|
|
662
719
|
redirectUrl.password = updatedOptions.password;
|
|
663
720
|
}
|
|
664
721
|
this.redirectUrls.push(redirectUrl);
|
|
665
|
-
updatedOptions.prefixUrl = '';
|
|
666
722
|
updatedOptions.url = redirectUrl;
|
|
667
723
|
for (const hook of updatedOptions.hooks.beforeRedirect) {
|
|
668
724
|
// eslint-disable-next-line no-await-in-loop
|
|
669
725
|
await hook(updatedOptions, typedResponse);
|
|
670
726
|
}
|
|
727
|
+
// Publish redirect event
|
|
728
|
+
publishRedirect({
|
|
729
|
+
requestId: this._requestId,
|
|
730
|
+
fromUrl: url.toString(),
|
|
731
|
+
toUrl: redirectUrl.toString(),
|
|
732
|
+
statusCode,
|
|
733
|
+
});
|
|
671
734
|
this.emit('redirect', updatedOptions, typedResponse);
|
|
672
735
|
this.options = updatedOptions;
|
|
673
736
|
await this._makeRequest();
|
|
@@ -711,6 +774,14 @@ export default class Request extends Duplex {
|
|
|
711
774
|
}
|
|
712
775
|
this._responseSize = this._downloadedSize;
|
|
713
776
|
this.emit('downloadProgress', this.downloadProgress);
|
|
777
|
+
// Publish response end event
|
|
778
|
+
publishResponseEnd({
|
|
779
|
+
requestId: this._requestId,
|
|
780
|
+
url: typedResponse.url,
|
|
781
|
+
statusCode,
|
|
782
|
+
bodySize: this._downloadedSize,
|
|
783
|
+
timings: this.timings,
|
|
784
|
+
});
|
|
714
785
|
this.push(null);
|
|
715
786
|
});
|
|
716
787
|
this.emit('downloadProgress', this.downloadProgress);
|
|
@@ -788,6 +859,13 @@ export default class Request extends Duplex {
|
|
|
788
859
|
_onRequest(request) {
|
|
789
860
|
const { options } = this;
|
|
790
861
|
const { timeout, url } = options;
|
|
862
|
+
// Publish request start event
|
|
863
|
+
publishRequestStart({
|
|
864
|
+
requestId: this._requestId,
|
|
865
|
+
url: url?.toString() ?? '',
|
|
866
|
+
method: options.method,
|
|
867
|
+
headers: options.headers,
|
|
868
|
+
});
|
|
791
869
|
timer(request);
|
|
792
870
|
this._cancelTimeouts = timedOut(request, timeout, url);
|
|
793
871
|
if (this.options.http2) {
|
|
@@ -877,47 +955,111 @@ export default class Request extends Duplex {
|
|
|
877
955
|
}
|
|
878
956
|
}
|
|
879
957
|
_prepareCache(cache) {
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
958
|
+
if (cacheableStore.has(cache)) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const cacheableRequest = new CacheableRequest(((requestOptions, handler) => {
|
|
962
|
+
/**
|
|
963
|
+
Wraps the cacheable-request handler to run beforeCache hooks.
|
|
964
|
+
These hooks control caching behavior by:
|
|
965
|
+
- Directly mutating the response object (changes apply to what gets cached)
|
|
966
|
+
- Returning `false` to prevent caching
|
|
967
|
+
- Returning `void`/`undefined` to use default caching behavior
|
|
968
|
+
|
|
969
|
+
Hooks use direct mutation - they can modify response.headers, response.statusCode, etc.
|
|
970
|
+
Mutations take effect immediately and determine what gets cached.
|
|
971
|
+
*/
|
|
972
|
+
const wrappedHandler = handler ? (response) => {
|
|
973
|
+
const { beforeCacheHooks, gotRequest } = requestOptions;
|
|
974
|
+
// Early return if no hooks - cache the original response
|
|
975
|
+
if (!beforeCacheHooks || beforeCacheHooks.length === 0) {
|
|
976
|
+
handler(response);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
// Call each beforeCache hook with the response
|
|
981
|
+
// Hooks can directly mutate the response - mutations take effect immediately
|
|
982
|
+
for (const hook of beforeCacheHooks) {
|
|
983
|
+
const result = hook(response);
|
|
984
|
+
if (result === false) {
|
|
985
|
+
// Prevent caching by adding no-cache headers
|
|
986
|
+
// Mutate the response directly to add headers
|
|
987
|
+
response.headers['cache-control'] = 'no-cache, no-store, must-revalidate';
|
|
988
|
+
response.headers.pragma = 'no-cache';
|
|
989
|
+
response.headers.expires = '0';
|
|
990
|
+
handler(response);
|
|
991
|
+
// Don't call remaining hooks - we've decided not to cache
|
|
992
|
+
return;
|
|
898
993
|
}
|
|
899
|
-
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
(async () => {
|
|
903
|
-
try {
|
|
904
|
-
const request = (await result);
|
|
905
|
-
request.once(event, handler);
|
|
906
|
-
}
|
|
907
|
-
catch { }
|
|
908
|
-
})();
|
|
994
|
+
if (is.promise(result)) {
|
|
995
|
+
// BeforeCache hooks must be synchronous because cacheable-request's handler is synchronous
|
|
996
|
+
throw new TypeError('beforeCache hooks must be synchronous. The hook returned a Promise, but this hook must return synchronously. If you need async logic, use beforeRequest hook instead.');
|
|
909
997
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
998
|
+
if (result !== undefined) {
|
|
999
|
+
// Hooks should return false or undefined only
|
|
1000
|
+
// Mutations work directly - no need to return the response
|
|
1001
|
+
throw new TypeError('beforeCache hook must return false or undefined. To modify the response, mutate it directly.');
|
|
913
1002
|
}
|
|
914
|
-
|
|
915
|
-
}
|
|
1003
|
+
// Else: void/undefined = continue
|
|
1004
|
+
}
|
|
916
1005
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
// Convert hook errors to RequestError and propagate
|
|
1008
|
+
// This is consistent with how other hooks handle errors
|
|
1009
|
+
if (gotRequest) {
|
|
1010
|
+
gotRequest._beforeError(error instanceof RequestError ? error : new RequestError(error.message, error, gotRequest));
|
|
1011
|
+
// Don't call handler when error was propagated successfully
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
// If gotRequest is missing, log the error to aid debugging
|
|
1015
|
+
// We still call the handler to prevent the request from hanging
|
|
1016
|
+
console.error('Got: beforeCache hook error (request context unavailable):', error);
|
|
1017
|
+
// Call handler with response (potentially partially modified)
|
|
1018
|
+
handler(response);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
// All hooks ran successfully
|
|
1022
|
+
// Cache the response with any mutations applied
|
|
1023
|
+
handler(response);
|
|
1024
|
+
} : handler;
|
|
1025
|
+
const result = requestOptions._request(requestOptions, wrappedHandler);
|
|
1026
|
+
// TODO: remove this when `cacheable-request` supports async request functions.
|
|
1027
|
+
if (is.promise(result)) {
|
|
1028
|
+
// We only need to implement the error handler in order to support HTTP2 caching.
|
|
1029
|
+
// The result will be a promise anyway.
|
|
1030
|
+
// @ts-expect-error ignore
|
|
1031
|
+
result.once = (event, handler) => {
|
|
1032
|
+
if (event === 'error') {
|
|
1033
|
+
(async () => {
|
|
1034
|
+
try {
|
|
1035
|
+
await result;
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
handler(error);
|
|
1039
|
+
}
|
|
1040
|
+
})();
|
|
1041
|
+
}
|
|
1042
|
+
else if (event === 'abort' || event === 'destroy') {
|
|
1043
|
+
// The empty catch is needed here in case when
|
|
1044
|
+
// it rejects before it's `await`ed in `_makeRequest`.
|
|
1045
|
+
(async () => {
|
|
1046
|
+
try {
|
|
1047
|
+
const request = (await result);
|
|
1048
|
+
request.once(event, handler);
|
|
1049
|
+
}
|
|
1050
|
+
catch { }
|
|
1051
|
+
})();
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
/* istanbul ignore next: safety check */
|
|
1055
|
+
throw new Error(`Unknown HTTP2 promise event: ${event}`);
|
|
1056
|
+
}
|
|
1057
|
+
return result;
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return result;
|
|
1061
|
+
}), cache);
|
|
1062
|
+
cacheableStore.set(cache, cacheableRequest.request());
|
|
921
1063
|
}
|
|
922
1064
|
async _createCacheableRequest(url, options) {
|
|
923
1065
|
return new Promise((resolve, reject) => {
|
|
@@ -966,7 +1108,14 @@ export default class Request extends Duplex {
|
|
|
966
1108
|
}
|
|
967
1109
|
}
|
|
968
1110
|
if (options.decompress && is.undefined(headers['accept-encoding'])) {
|
|
969
|
-
|
|
1111
|
+
const encodings = ['gzip', 'deflate'];
|
|
1112
|
+
if (supportsBrotli) {
|
|
1113
|
+
encodings.push('br');
|
|
1114
|
+
}
|
|
1115
|
+
if (supportsZstd) {
|
|
1116
|
+
encodings.push('zstd');
|
|
1117
|
+
}
|
|
1118
|
+
headers['accept-encoding'] = encodings.join(', ');
|
|
970
1119
|
}
|
|
971
1120
|
if (username || password) {
|
|
972
1121
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
@@ -979,8 +1128,6 @@ export default class Request extends Duplex {
|
|
|
979
1128
|
headers.cookie = cookieString;
|
|
980
1129
|
}
|
|
981
1130
|
}
|
|
982
|
-
// Reset `prefixUrl`
|
|
983
|
-
options.prefixUrl = '';
|
|
984
1131
|
let request;
|
|
985
1132
|
for (const hook of options.hooks.beforeRequest) {
|
|
986
1133
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -998,6 +1145,8 @@ export default class Request extends Duplex {
|
|
|
998
1145
|
this._requestOptions._request = request;
|
|
999
1146
|
this._requestOptions.cache = options.cache;
|
|
1000
1147
|
this._requestOptions.body = options.body;
|
|
1148
|
+
this._requestOptions.beforeCacheHooks = options.hooks.beforeCache;
|
|
1149
|
+
this._requestOptions.gotRequest = this;
|
|
1001
1150
|
try {
|
|
1002
1151
|
this._prepareCache(options.cache);
|
|
1003
1152
|
}
|
|
@@ -1024,15 +1173,15 @@ export default class Request extends Duplex {
|
|
|
1024
1173
|
if (isClientRequest(requestOrResponse)) {
|
|
1025
1174
|
this._onRequest(requestOrResponse);
|
|
1026
1175
|
}
|
|
1027
|
-
else if (this.
|
|
1176
|
+
else if (this.writableEnded) {
|
|
1177
|
+
void this._onResponse(requestOrResponse);
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1028
1180
|
this.once('finish', () => {
|
|
1029
1181
|
void this._onResponse(requestOrResponse);
|
|
1030
1182
|
});
|
|
1031
1183
|
this._sendBody();
|
|
1032
1184
|
}
|
|
1033
|
-
else {
|
|
1034
|
-
void this._onResponse(requestOrResponse);
|
|
1035
|
-
}
|
|
1036
1185
|
}
|
|
1037
1186
|
catch (error) {
|
|
1038
1187
|
if (error instanceof CacheableCacheError) {
|
|
@@ -1049,15 +1198,35 @@ export default class Request extends Duplex {
|
|
|
1049
1198
|
// See https://github.com/sindresorhus/got/issues/2103
|
|
1050
1199
|
}
|
|
1051
1200
|
else if (this.options) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1201
|
+
const hooks = this.options.hooks.beforeError;
|
|
1202
|
+
if (hooks.length > 0) {
|
|
1203
|
+
for (const hook of hooks) {
|
|
1204
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1205
|
+
error = await hook(error);
|
|
1206
|
+
// Validate hook return value
|
|
1207
|
+
if (!(error instanceof Error)) {
|
|
1208
|
+
throw new TypeError(`The \`beforeError\` hook must return an Error instance. Received ${is.string(error) ? 'string' : String(typeof error)}.`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Mark this error as processed by hooks so _destroy preserves custom error types.
|
|
1212
|
+
// Only mark non-RequestError errors, since RequestErrors are already preserved
|
|
1213
|
+
// by the instanceof check in _destroy (line 642).
|
|
1214
|
+
if (!(error instanceof RequestError)) {
|
|
1215
|
+
errorsProcessedByHooks.add(error);
|
|
1216
|
+
}
|
|
1055
1217
|
}
|
|
1056
1218
|
}
|
|
1057
1219
|
}
|
|
1058
1220
|
catch (error_) {
|
|
1059
1221
|
error = new RequestError(error_.message, error_, this);
|
|
1060
1222
|
}
|
|
1223
|
+
// Publish error event
|
|
1224
|
+
publishError({
|
|
1225
|
+
requestId: this._requestId,
|
|
1226
|
+
url: this.options?.url?.toString() ?? '',
|
|
1227
|
+
error,
|
|
1228
|
+
timings: this.timings,
|
|
1229
|
+
});
|
|
1061
1230
|
this.destroy(error);
|
|
1062
1231
|
// Manually emit error for Promise API to ensure it receives it.
|
|
1063
1232
|
// Node.js streams may not re-emit if an error was already emitted during retry attempts.
|
|
@@ -1080,7 +1249,9 @@ export default class Request extends Duplex {
|
|
|
1080
1249
|
this._request.write(chunk, encoding, (error) => {
|
|
1081
1250
|
// The `!destroyed` check is required to prevent `uploadProgress` being emitted after the stream was destroyed
|
|
1082
1251
|
if (!error && !this._request.destroyed) {
|
|
1083
|
-
|
|
1252
|
+
// For strings, encode them first to measure the actual bytes that will be sent
|
|
1253
|
+
const bytes = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk;
|
|
1254
|
+
this._uploadedSize += byteLength(bytes);
|
|
1084
1255
|
const progress = this.uploadProgress;
|
|
1085
1256
|
if (progress.percent < 1) {
|
|
1086
1257
|
this.emit('uploadProgress', progress);
|
|
@@ -1183,4 +1354,10 @@ export default class Request extends Duplex {
|
|
|
1183
1354
|
get reusedSocket() {
|
|
1184
1355
|
return this._request?.reusedSocket;
|
|
1185
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
Whether the stream is read-only. Returns `true` when `body`, `json`, or `form` options are provided.
|
|
1359
|
+
*/
|
|
1360
|
+
get isReadonly() {
|
|
1361
|
+
return !is.undefined(this.options?.body) || !is.undefined(this.options?.json) || !is.undefined(this.options?.form);
|
|
1362
|
+
}
|
|
1186
1363
|
}
|