got 14.6.0 → 14.6.1
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 +5 -0
- package/dist/source/core/index.js +267 -82
- package/dist/source/core/options.d.ts +151 -2
- 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 +2 -1
|
@@ -19,12 +19,16 @@ import { isResponseOk } from './response.js';
|
|
|
19
19
|
import isClientRequest from './utils/is-client-request.js';
|
|
20
20
|
import isUnixSocketURL, { getUnixSocketPath } from './utils/is-unix-socket-url.js';
|
|
21
21
|
import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
|
|
22
|
+
import { generateRequestId, publishRequestCreate, publishRequestStart, publishResponseStart, publishResponseEnd, publishRetry, publishError, publishRedirect, } from './diagnostics-channel.js';
|
|
22
23
|
const supportsBrotli = is.string(process.versions.brotli);
|
|
24
|
+
const supportsZstd = is.string(process.versions.zstd);
|
|
23
25
|
const methodsWithoutBody = new Set(['GET', 'HEAD']);
|
|
24
26
|
// Methods that should auto-end streams when no body is provided
|
|
25
27
|
const methodsWithoutBodyStream = new Set(['OPTIONS', 'DELETE', 'PATCH']);
|
|
26
28
|
const cacheableStore = new WeakableMap();
|
|
27
29
|
const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
|
|
30
|
+
// Track errors that have been processed by beforeError hooks to preserve custom error types
|
|
31
|
+
const errorsProcessedByHooks = new WeakSet();
|
|
28
32
|
const proxiedRequestEvents = [
|
|
29
33
|
'socket',
|
|
30
34
|
'connect',
|
|
@@ -52,28 +56,30 @@ export default class Request extends Duplex {
|
|
|
52
56
|
options;
|
|
53
57
|
response;
|
|
54
58
|
requestUrl;
|
|
55
|
-
redirectUrls;
|
|
56
|
-
retryCount;
|
|
57
|
-
_stopReading;
|
|
58
|
-
_stopRetry;
|
|
59
|
-
_downloadedSize;
|
|
60
|
-
_uploadedSize;
|
|
61
|
-
_pipedServerResponses;
|
|
59
|
+
redirectUrls = [];
|
|
60
|
+
retryCount = 0;
|
|
61
|
+
_stopReading = false;
|
|
62
|
+
_stopRetry = noop;
|
|
63
|
+
_downloadedSize = 0;
|
|
64
|
+
_uploadedSize = 0;
|
|
65
|
+
_pipedServerResponses = new Set();
|
|
62
66
|
_request;
|
|
63
67
|
_responseSize;
|
|
64
68
|
_bodySize;
|
|
65
|
-
_unproxyEvents;
|
|
69
|
+
_unproxyEvents = noop;
|
|
66
70
|
_isFromCache;
|
|
67
|
-
_triggerRead;
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
_triggerRead = false;
|
|
72
|
+
_jobs = [];
|
|
73
|
+
_cancelTimeouts = noop;
|
|
74
|
+
_removeListeners = noop;
|
|
70
75
|
_nativeResponse;
|
|
71
|
-
_flushed;
|
|
72
|
-
_aborted;
|
|
76
|
+
_flushed = false;
|
|
77
|
+
_aborted = false;
|
|
73
78
|
_expectedContentLength;
|
|
74
79
|
_byteCounter;
|
|
80
|
+
_requestId = generateRequestId();
|
|
75
81
|
// We need this because `this._request` if `undefined` when using cache
|
|
76
|
-
_requestInitialized;
|
|
82
|
+
_requestInitialized = false;
|
|
77
83
|
constructor(url, options, defaults) {
|
|
78
84
|
super({
|
|
79
85
|
// Don't destroy immediately, as the error may be emitted on unsuccessful retry
|
|
@@ -81,23 +87,8 @@ export default class Request extends Duplex {
|
|
|
81
87
|
// It needs to be zero because we're just proxying the data to another stream
|
|
82
88
|
highWaterMark: 0,
|
|
83
89
|
});
|
|
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
90
|
this.on('pipe', (source) => {
|
|
100
|
-
if (source?.headers) {
|
|
91
|
+
if (this.options.copyPipedHeaders && source?.headers) {
|
|
101
92
|
Object.assign(this.options.headers, source.headers);
|
|
102
93
|
}
|
|
103
94
|
});
|
|
@@ -115,6 +106,12 @@ export default class Request extends Duplex {
|
|
|
115
106
|
this.options.url = '';
|
|
116
107
|
}
|
|
117
108
|
this.requestUrl = this.options.url;
|
|
109
|
+
// Publish request creation event
|
|
110
|
+
publishRequestCreate({
|
|
111
|
+
requestId: this._requestId,
|
|
112
|
+
url: this.options.url?.toString() ?? '',
|
|
113
|
+
method: this.options.method,
|
|
114
|
+
});
|
|
118
115
|
}
|
|
119
116
|
catch (error) {
|
|
120
117
|
const { options } = error;
|
|
@@ -285,6 +282,8 @@ export default class Request extends Duplex {
|
|
|
285
282
|
if (this.destroyed) {
|
|
286
283
|
return;
|
|
287
284
|
}
|
|
285
|
+
// Capture body BEFORE hooks run to detect reassignment
|
|
286
|
+
const bodyBeforeHooks = this.options.body;
|
|
288
287
|
try {
|
|
289
288
|
for (const hook of this.options.hooks.beforeRetry) {
|
|
290
289
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -292,14 +291,58 @@ export default class Request extends Duplex {
|
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
293
|
catch (error_) {
|
|
295
|
-
void this._error(new RequestError(error_.message,
|
|
294
|
+
void this._error(new RequestError(error_.message, error_, this));
|
|
296
295
|
return;
|
|
297
296
|
}
|
|
298
297
|
// Something forced us to abort the retry
|
|
299
298
|
if (this.destroyed) {
|
|
300
299
|
return;
|
|
301
300
|
}
|
|
302
|
-
|
|
301
|
+
// Preserve stream body reassigned in beforeRetry hooks.
|
|
302
|
+
const bodyAfterHooks = this.options.body;
|
|
303
|
+
const bodyWasReassigned = bodyBeforeHooks !== bodyAfterHooks;
|
|
304
|
+
// Resource cleanup and preservation logic for retry with body reassignment.
|
|
305
|
+
// The Promise wrapper (as-promise/index.ts) compares body identity to detect consumed streams,
|
|
306
|
+
// so we must preserve the body reference across destroy(). However, destroy() calls _destroy()
|
|
307
|
+
// which destroys this.options.body, creating a complex dance of clear/restore operations.
|
|
308
|
+
//
|
|
309
|
+
// Key constraints:
|
|
310
|
+
// 1. If body was reassigned, we must NOT destroy the NEW stream (it will be used for retry)
|
|
311
|
+
// 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks
|
|
312
|
+
// 3. We must restore the body reference after destroy() for identity checks in promise wrapper
|
|
313
|
+
// 4. We cannot use the normal setter after destroy() because it validates stream readability
|
|
314
|
+
if (bodyWasReassigned) {
|
|
315
|
+
const oldBody = bodyBeforeHooks;
|
|
316
|
+
// Temporarily clear body to prevent destroy() from destroying the new stream
|
|
317
|
+
this.options.body = undefined;
|
|
318
|
+
this.destroy();
|
|
319
|
+
// Clean up the old stream resource if it's a stream and different from new body
|
|
320
|
+
// (edge case: if old and new are same stream object, don't destroy it)
|
|
321
|
+
if (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {
|
|
322
|
+
oldBody.destroy();
|
|
323
|
+
}
|
|
324
|
+
// Restore new body for promise wrapper's identity check
|
|
325
|
+
// We bypass the setter because it validates stream.readable (which fails for destroyed request)
|
|
326
|
+
// Type assertion is necessary here to access private _internals without exposing internal API
|
|
327
|
+
if (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {
|
|
328
|
+
throw new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');
|
|
329
|
+
}
|
|
330
|
+
this.options._internals.body = bodyAfterHooks;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Body wasn't reassigned - use normal destroy flow which handles body cleanup
|
|
334
|
+
this.destroy();
|
|
335
|
+
// Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()
|
|
336
|
+
// and should not be accessed. The promise wrapper will see that body identity hasn't changed
|
|
337
|
+
// and will detect it's a consumed stream, which is the correct behavior.
|
|
338
|
+
}
|
|
339
|
+
// Publish retry event
|
|
340
|
+
publishRetry({
|
|
341
|
+
requestId: this._requestId,
|
|
342
|
+
retryCount: this.retryCount + 1,
|
|
343
|
+
error: typedError,
|
|
344
|
+
delay: backoff,
|
|
345
|
+
});
|
|
303
346
|
this.emit('retry', this.retryCount + 1, error, (updatedOptions) => {
|
|
304
347
|
const request = new Request(options.url, updatedOptions, options);
|
|
305
348
|
request.retryCount = this.retryCount + 1;
|
|
@@ -403,8 +446,15 @@ export default class Request extends Duplex {
|
|
|
403
446
|
timings.phases.total = timings.end - timings.start;
|
|
404
447
|
}
|
|
405
448
|
}
|
|
406
|
-
|
|
407
|
-
|
|
449
|
+
// Preserve custom errors returned by beforeError hooks.
|
|
450
|
+
// For other errors, wrap non-RequestError instances for consistency.
|
|
451
|
+
if (error !== null && !is.undefined(error)) {
|
|
452
|
+
const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
|
|
453
|
+
if (!processedByHooks && !(error instanceof RequestError)) {
|
|
454
|
+
error = error instanceof Error
|
|
455
|
+
? new RequestError(error.message, error, this)
|
|
456
|
+
: new RequestError(String(error), {}, this);
|
|
457
|
+
}
|
|
408
458
|
}
|
|
409
459
|
callback(error);
|
|
410
460
|
}
|
|
@@ -548,6 +598,14 @@ export default class Request extends Duplex {
|
|
|
548
598
|
this._isFromCache = typedResponse.isFromCache;
|
|
549
599
|
this._responseSize = Number(response.headers['content-length']) || undefined;
|
|
550
600
|
this.response = typedResponse;
|
|
601
|
+
// Publish response start event
|
|
602
|
+
publishResponseStart({
|
|
603
|
+
requestId: this._requestId,
|
|
604
|
+
url: typedResponse.url,
|
|
605
|
+
statusCode,
|
|
606
|
+
headers: response.headers,
|
|
607
|
+
isFromCache: typedResponse.isFromCache,
|
|
608
|
+
});
|
|
551
609
|
// Workaround for http-timer bug: when connecting to an IP address (no DNS lookup),
|
|
552
610
|
// http-timer sets lookup = connect instead of lookup = socket, resulting in
|
|
553
611
|
// dns = lookup - socket being a small positive number instead of 0.
|
|
@@ -560,6 +618,15 @@ export default class Request extends Duplex {
|
|
|
560
618
|
// Recalculate TCP time to be the full time from socket to connect
|
|
561
619
|
timings.phases.tcp = timings.connect - timings.socket;
|
|
562
620
|
}
|
|
621
|
+
// Workaround for http-timer limitation with HTTP/2:
|
|
622
|
+
// When using HTTP/2, the socket is a proxy that http-timer discards,
|
|
623
|
+
// so lookup, connect, and secureConnect events are never captured.
|
|
624
|
+
// This results in phases.request being NaN (undefined - undefined).
|
|
625
|
+
// Set it to undefined to be consistent with other unavailable timings.
|
|
626
|
+
// See https://github.com/sindresorhus/got/issues/1958
|
|
627
|
+
if (timings && Number.isNaN(timings.phases.request)) {
|
|
628
|
+
timings.phases.request = undefined;
|
|
629
|
+
}
|
|
563
630
|
response.once('error', (error) => {
|
|
564
631
|
this._aborted = true;
|
|
565
632
|
// Force clean-up, because some packages don't do this.
|
|
@@ -662,12 +729,18 @@ export default class Request extends Duplex {
|
|
|
662
729
|
redirectUrl.password = updatedOptions.password;
|
|
663
730
|
}
|
|
664
731
|
this.redirectUrls.push(redirectUrl);
|
|
665
|
-
updatedOptions.prefixUrl = '';
|
|
666
732
|
updatedOptions.url = redirectUrl;
|
|
667
733
|
for (const hook of updatedOptions.hooks.beforeRedirect) {
|
|
668
734
|
// eslint-disable-next-line no-await-in-loop
|
|
669
735
|
await hook(updatedOptions, typedResponse);
|
|
670
736
|
}
|
|
737
|
+
// Publish redirect event
|
|
738
|
+
publishRedirect({
|
|
739
|
+
requestId: this._requestId,
|
|
740
|
+
fromUrl: url.toString(),
|
|
741
|
+
toUrl: redirectUrl.toString(),
|
|
742
|
+
statusCode,
|
|
743
|
+
});
|
|
671
744
|
this.emit('redirect', updatedOptions, typedResponse);
|
|
672
745
|
this.options = updatedOptions;
|
|
673
746
|
await this._makeRequest();
|
|
@@ -711,6 +784,14 @@ export default class Request extends Duplex {
|
|
|
711
784
|
}
|
|
712
785
|
this._responseSize = this._downloadedSize;
|
|
713
786
|
this.emit('downloadProgress', this.downloadProgress);
|
|
787
|
+
// Publish response end event
|
|
788
|
+
publishResponseEnd({
|
|
789
|
+
requestId: this._requestId,
|
|
790
|
+
url: typedResponse.url,
|
|
791
|
+
statusCode,
|
|
792
|
+
bodySize: this._downloadedSize,
|
|
793
|
+
timings: this.timings,
|
|
794
|
+
});
|
|
714
795
|
this.push(null);
|
|
715
796
|
});
|
|
716
797
|
this.emit('downloadProgress', this.downloadProgress);
|
|
@@ -788,6 +869,13 @@ export default class Request extends Duplex {
|
|
|
788
869
|
_onRequest(request) {
|
|
789
870
|
const { options } = this;
|
|
790
871
|
const { timeout, url } = options;
|
|
872
|
+
// Publish request start event
|
|
873
|
+
publishRequestStart({
|
|
874
|
+
requestId: this._requestId,
|
|
875
|
+
url: url?.toString() ?? '',
|
|
876
|
+
method: options.method,
|
|
877
|
+
headers: options.headers,
|
|
878
|
+
});
|
|
791
879
|
timer(request);
|
|
792
880
|
this._cancelTimeouts = timedOut(request, timeout, url);
|
|
793
881
|
if (this.options.http2) {
|
|
@@ -877,47 +965,111 @@ export default class Request extends Duplex {
|
|
|
877
965
|
}
|
|
878
966
|
}
|
|
879
967
|
_prepareCache(cache) {
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
968
|
+
if (cacheableStore.has(cache)) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const cacheableRequest = new CacheableRequest(((requestOptions, handler) => {
|
|
972
|
+
/**
|
|
973
|
+
Wraps the cacheable-request handler to run beforeCache hooks.
|
|
974
|
+
These hooks control caching behavior by:
|
|
975
|
+
- Directly mutating the response object (changes apply to what gets cached)
|
|
976
|
+
- Returning `false` to prevent caching
|
|
977
|
+
- Returning `void`/`undefined` to use default caching behavior
|
|
978
|
+
|
|
979
|
+
Hooks use direct mutation - they can modify response.headers, response.statusCode, etc.
|
|
980
|
+
Mutations take effect immediately and determine what gets cached.
|
|
981
|
+
*/
|
|
982
|
+
const wrappedHandler = handler ? (response) => {
|
|
983
|
+
const { beforeCacheHooks, gotRequest } = requestOptions;
|
|
984
|
+
// Early return if no hooks - cache the original response
|
|
985
|
+
if (!beforeCacheHooks || beforeCacheHooks.length === 0) {
|
|
986
|
+
handler(response);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
// Call each beforeCache hook with the response
|
|
991
|
+
// Hooks can directly mutate the response - mutations take effect immediately
|
|
992
|
+
for (const hook of beforeCacheHooks) {
|
|
993
|
+
const result = hook(response);
|
|
994
|
+
if (result === false) {
|
|
995
|
+
// Prevent caching by adding no-cache headers
|
|
996
|
+
// Mutate the response directly to add headers
|
|
997
|
+
response.headers['cache-control'] = 'no-cache, no-store, must-revalidate';
|
|
998
|
+
response.headers.pragma = 'no-cache';
|
|
999
|
+
response.headers.expires = '0';
|
|
1000
|
+
handler(response);
|
|
1001
|
+
// Don't call remaining hooks - we've decided not to cache
|
|
1002
|
+
return;
|
|
898
1003
|
}
|
|
899
|
-
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
(async () => {
|
|
903
|
-
try {
|
|
904
|
-
const request = (await result);
|
|
905
|
-
request.once(event, handler);
|
|
906
|
-
}
|
|
907
|
-
catch { }
|
|
908
|
-
})();
|
|
1004
|
+
if (is.promise(result)) {
|
|
1005
|
+
// BeforeCache hooks must be synchronous because cacheable-request's handler is synchronous
|
|
1006
|
+
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
1007
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1008
|
+
if (result !== undefined) {
|
|
1009
|
+
// Hooks should return false or undefined only
|
|
1010
|
+
// Mutations work directly - no need to return the response
|
|
1011
|
+
throw new TypeError('beforeCache hook must return false or undefined. To modify the response, mutate it directly.');
|
|
913
1012
|
}
|
|
914
|
-
|
|
915
|
-
}
|
|
1013
|
+
// Else: void/undefined = continue
|
|
1014
|
+
}
|
|
916
1015
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
// Convert hook errors to RequestError and propagate
|
|
1018
|
+
// This is consistent with how other hooks handle errors
|
|
1019
|
+
if (gotRequest) {
|
|
1020
|
+
gotRequest._beforeError(error instanceof RequestError ? error : new RequestError(error.message, error, gotRequest));
|
|
1021
|
+
// Don't call handler when error was propagated successfully
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
// If gotRequest is missing, log the error to aid debugging
|
|
1025
|
+
// We still call the handler to prevent the request from hanging
|
|
1026
|
+
console.error('Got: beforeCache hook error (request context unavailable):', error);
|
|
1027
|
+
// Call handler with response (potentially partially modified)
|
|
1028
|
+
handler(response);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// All hooks ran successfully
|
|
1032
|
+
// Cache the response with any mutations applied
|
|
1033
|
+
handler(response);
|
|
1034
|
+
} : handler;
|
|
1035
|
+
const result = requestOptions._request(requestOptions, wrappedHandler);
|
|
1036
|
+
// TODO: remove this when `cacheable-request` supports async request functions.
|
|
1037
|
+
if (is.promise(result)) {
|
|
1038
|
+
// We only need to implement the error handler in order to support HTTP2 caching.
|
|
1039
|
+
// The result will be a promise anyway.
|
|
1040
|
+
// @ts-expect-error ignore
|
|
1041
|
+
result.once = (event, handler) => {
|
|
1042
|
+
if (event === 'error') {
|
|
1043
|
+
(async () => {
|
|
1044
|
+
try {
|
|
1045
|
+
await result;
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
handler(error);
|
|
1049
|
+
}
|
|
1050
|
+
})();
|
|
1051
|
+
}
|
|
1052
|
+
else if (event === 'abort' || event === 'destroy') {
|
|
1053
|
+
// The empty catch is needed here in case when
|
|
1054
|
+
// it rejects before it's `await`ed in `_makeRequest`.
|
|
1055
|
+
(async () => {
|
|
1056
|
+
try {
|
|
1057
|
+
const request = (await result);
|
|
1058
|
+
request.once(event, handler);
|
|
1059
|
+
}
|
|
1060
|
+
catch { }
|
|
1061
|
+
})();
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
/* istanbul ignore next: safety check */
|
|
1065
|
+
throw new Error(`Unknown HTTP2 promise event: ${event}`);
|
|
1066
|
+
}
|
|
1067
|
+
return result;
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return result;
|
|
1071
|
+
}), cache);
|
|
1072
|
+
cacheableStore.set(cache, cacheableRequest.request());
|
|
921
1073
|
}
|
|
922
1074
|
async _createCacheableRequest(url, options) {
|
|
923
1075
|
return new Promise((resolve, reject) => {
|
|
@@ -966,7 +1118,14 @@ export default class Request extends Duplex {
|
|
|
966
1118
|
}
|
|
967
1119
|
}
|
|
968
1120
|
if (options.decompress && is.undefined(headers['accept-encoding'])) {
|
|
969
|
-
|
|
1121
|
+
const encodings = ['gzip', 'deflate'];
|
|
1122
|
+
if (supportsBrotli) {
|
|
1123
|
+
encodings.push('br');
|
|
1124
|
+
}
|
|
1125
|
+
if (supportsZstd) {
|
|
1126
|
+
encodings.push('zstd');
|
|
1127
|
+
}
|
|
1128
|
+
headers['accept-encoding'] = encodings.join(', ');
|
|
970
1129
|
}
|
|
971
1130
|
if (username || password) {
|
|
972
1131
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
@@ -979,8 +1138,6 @@ export default class Request extends Duplex {
|
|
|
979
1138
|
headers.cookie = cookieString;
|
|
980
1139
|
}
|
|
981
1140
|
}
|
|
982
|
-
// Reset `prefixUrl`
|
|
983
|
-
options.prefixUrl = '';
|
|
984
1141
|
let request;
|
|
985
1142
|
for (const hook of options.hooks.beforeRequest) {
|
|
986
1143
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -998,6 +1155,8 @@ export default class Request extends Duplex {
|
|
|
998
1155
|
this._requestOptions._request = request;
|
|
999
1156
|
this._requestOptions.cache = options.cache;
|
|
1000
1157
|
this._requestOptions.body = options.body;
|
|
1158
|
+
this._requestOptions.beforeCacheHooks = options.hooks.beforeCache;
|
|
1159
|
+
this._requestOptions.gotRequest = this;
|
|
1001
1160
|
try {
|
|
1002
1161
|
this._prepareCache(options.cache);
|
|
1003
1162
|
}
|
|
@@ -1024,15 +1183,15 @@ export default class Request extends Duplex {
|
|
|
1024
1183
|
if (isClientRequest(requestOrResponse)) {
|
|
1025
1184
|
this._onRequest(requestOrResponse);
|
|
1026
1185
|
}
|
|
1027
|
-
else if (this.
|
|
1186
|
+
else if (this.writableEnded) {
|
|
1187
|
+
void this._onResponse(requestOrResponse);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1028
1190
|
this.once('finish', () => {
|
|
1029
1191
|
void this._onResponse(requestOrResponse);
|
|
1030
1192
|
});
|
|
1031
1193
|
this._sendBody();
|
|
1032
1194
|
}
|
|
1033
|
-
else {
|
|
1034
|
-
void this._onResponse(requestOrResponse);
|
|
1035
|
-
}
|
|
1036
1195
|
}
|
|
1037
1196
|
catch (error) {
|
|
1038
1197
|
if (error instanceof CacheableCacheError) {
|
|
@@ -1049,15 +1208,35 @@ export default class Request extends Duplex {
|
|
|
1049
1208
|
// See https://github.com/sindresorhus/got/issues/2103
|
|
1050
1209
|
}
|
|
1051
1210
|
else if (this.options) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1211
|
+
const hooks = this.options.hooks.beforeError;
|
|
1212
|
+
if (hooks.length > 0) {
|
|
1213
|
+
for (const hook of hooks) {
|
|
1214
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1215
|
+
error = await hook(error);
|
|
1216
|
+
// Validate hook return value
|
|
1217
|
+
if (!(error instanceof Error)) {
|
|
1218
|
+
throw new TypeError(`The \`beforeError\` hook must return an Error instance. Received ${is.string(error) ? 'string' : String(typeof error)}.`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// Mark this error as processed by hooks so _destroy preserves custom error types.
|
|
1222
|
+
// Only mark non-RequestError errors, since RequestErrors are already preserved
|
|
1223
|
+
// by the instanceof check in _destroy (line 642).
|
|
1224
|
+
if (!(error instanceof RequestError)) {
|
|
1225
|
+
errorsProcessedByHooks.add(error);
|
|
1226
|
+
}
|
|
1055
1227
|
}
|
|
1056
1228
|
}
|
|
1057
1229
|
}
|
|
1058
1230
|
catch (error_) {
|
|
1059
1231
|
error = new RequestError(error_.message, error_, this);
|
|
1060
1232
|
}
|
|
1233
|
+
// Publish error event
|
|
1234
|
+
publishError({
|
|
1235
|
+
requestId: this._requestId,
|
|
1236
|
+
url: this.options?.url?.toString() ?? '',
|
|
1237
|
+
error,
|
|
1238
|
+
timings: this.timings,
|
|
1239
|
+
});
|
|
1061
1240
|
this.destroy(error);
|
|
1062
1241
|
// Manually emit error for Promise API to ensure it receives it.
|
|
1063
1242
|
// Node.js streams may not re-emit if an error was already emitted during retry attempts.
|
|
@@ -1183,4 +1362,10 @@ export default class Request extends Duplex {
|
|
|
1183
1362
|
get reusedSocket() {
|
|
1184
1363
|
return this._request?.reusedSocket;
|
|
1185
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
Whether the stream is read-only. Returns `true` when `body`, `json`, or `form` options are provided.
|
|
1367
|
+
*/
|
|
1368
|
+
get isReadonly() {
|
|
1369
|
+
return !is.undefined(this.options?.body) || !is.undefined(this.options?.json) || !is.undefined(this.options?.form);
|
|
1370
|
+
}
|
|
1186
1371
|
}
|