@depup/got 15.1.0-depup.10 → 16.0.0-depup.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/README.md +3 -3
- package/changes.json +2 -2
- package/dist/source/as-promise/index.js +11 -1
- package/dist/source/core/calculate-retry-delay.js +1 -1
- package/dist/source/core/index.d.ts +2 -0
- package/dist/source/core/index.js +186 -74
- package/dist/source/core/options.d.ts +35 -42
- package/dist/source/core/options.js +164 -64
- package/dist/source/core/parse-link-header.js +1 -1
- package/dist/source/core/timed-out.js +1 -1
- package/dist/source/core/utils/dns-cache.d.ts +56 -0
- package/dist/source/core/utils/dns-cache.js +522 -0
- package/dist/source/core/utils/http2-client.d.ts +83 -0
- package/dist/source/core/utils/http2-client.js +1248 -0
- package/dist/source/core/utils/is-unix-socket-url.d.ts +0 -1
- package/dist/source/core/utils/is-unix-socket-url.js +1 -1
- package/dist/source/create.js +25 -15
- package/dist/source/types.d.ts +5 -6
- package/package.json +11 -14
- package/readme.md +4 -5
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ npm install @depup/got
|
|
|
13
13
|
|
|
14
14
|
| Field | Value |
|
|
15
15
|
|-------|-------|
|
|
16
|
-
| Original | [got](https://www.npmjs.com/package/got) @
|
|
17
|
-
| Processed | 2026-
|
|
16
|
+
| Original | [got](https://www.npmjs.com/package/got) @ 16.0.0 |
|
|
17
|
+
| Processed | 2026-09-17 |
|
|
18
18
|
| Smoke test | passed |
|
|
19
19
|
| Deps updated | 3 |
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ npm install @depup/got
|
|
|
24
24
|
|------------|------|-----|
|
|
25
25
|
| @sindresorhus/is | ^8.0.0 | ^8.1.0 |
|
|
26
26
|
| cacheable-request | ^13.0.18 | ^13.0.19 |
|
|
27
|
-
| type-fest | ^5.6.0 | ^5.
|
|
27
|
+
| type-fest | ^5.6.0 | ^5.10.0 |
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
package/changes.json
CHANGED
|
@@ -89,6 +89,16 @@ export default function asPromise(firstRequest) {
|
|
|
89
89
|
&& hasUrlOrPrefixUrlBoundaryChanged(options, currentUrl, previousBoundary)) {
|
|
90
90
|
assertUrlHasSameOriginAsPrefixUrlIfNeeded(options, currentUrl);
|
|
91
91
|
}
|
|
92
|
+
if (!reusesRequestOptions
|
|
93
|
+
&& updatedOptions.url === undefined
|
|
94
|
+
&& previousUrl
|
|
95
|
+
&& currentUrl instanceof URL
|
|
96
|
+
&& !isSameOrigin(previousUrl, currentUrl)) {
|
|
97
|
+
options.stripSensitiveHeaders(previousUrl, currentUrl, updatedOptions);
|
|
98
|
+
if (!hasExplicitBody) {
|
|
99
|
+
options.clearBody();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
if (updatedOptions.url !== undefined) {
|
|
93
103
|
const nextUrl = reusesRequestOptions
|
|
94
104
|
? options.url
|
|
@@ -151,7 +161,7 @@ export default function asPromise(firstRequest) {
|
|
|
151
161
|
if (error instanceof HTTPError && !options.throwHttpErrors) {
|
|
152
162
|
const { response } = error;
|
|
153
163
|
request.destroy();
|
|
154
|
-
resolve(
|
|
164
|
+
resolve(options.resolveBodyOnly ? response.body : response);
|
|
155
165
|
return;
|
|
156
166
|
}
|
|
157
167
|
reject(error);
|
|
@@ -13,7 +13,7 @@ const calculateRetryDelay = ({ attemptCount, retryOptions, error, retryAfter, co
|
|
|
13
13
|
}
|
|
14
14
|
if (error.response) {
|
|
15
15
|
if (retryAfter) {
|
|
16
|
-
// In this case `computedValue` is `options.request
|
|
16
|
+
// In this case `computedValue` is `retryOptions.maxRetryAfter ?? options.timeout.request ?? Infinity`
|
|
17
17
|
return retryAfter > computedValue ? 0 : retryAfter;
|
|
18
18
|
}
|
|
19
19
|
if (error.response.statusCode === 413) {
|
|
@@ -112,6 +112,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
|
|
|
112
112
|
private _skipRequestEndInFinal;
|
|
113
113
|
private _hasWrittenBody;
|
|
114
114
|
private _hasWritableBody;
|
|
115
|
+
private _discardBodyWrites;
|
|
115
116
|
private _incrementalDecode?;
|
|
116
117
|
private readonly _requestId;
|
|
117
118
|
private _requestInitialized;
|
|
@@ -127,6 +128,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
|
|
|
127
128
|
}): T;
|
|
128
129
|
unpipe<T extends NodeJS.WritableStream>(destination: T): this;
|
|
129
130
|
private _attachAbortListener;
|
|
131
|
+
private _destroyInFlightAlpnSocket;
|
|
130
132
|
private _shouldIncrementallyDecodeBody;
|
|
131
133
|
private _checkContentLengthMismatch;
|
|
132
134
|
private _finalizeBody;
|
|
@@ -37,10 +37,15 @@ const transientWriteErrorCodes = new Set(['EPIPE', 'ECONNRESET']);
|
|
|
37
37
|
const omittedPipedHeaders = new Set([
|
|
38
38
|
'host',
|
|
39
39
|
'connection',
|
|
40
|
+
'authorization',
|
|
41
|
+
'cookie',
|
|
42
|
+
'cookie2',
|
|
40
43
|
'keep-alive',
|
|
41
44
|
'proxy-authenticate',
|
|
42
45
|
'proxy-authorization',
|
|
43
46
|
'proxy-connection',
|
|
47
|
+
'set-cookie',
|
|
48
|
+
'set-cookie2',
|
|
44
49
|
'te',
|
|
45
50
|
'trailer',
|
|
46
51
|
'transfer-encoding',
|
|
@@ -56,6 +61,16 @@ const proxiedRequestEvents = [
|
|
|
56
61
|
'upgrade',
|
|
57
62
|
];
|
|
58
63
|
const noop = () => { };
|
|
64
|
+
const createPreRequestErrorTimings = () => {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
return {
|
|
67
|
+
start: now,
|
|
68
|
+
error: now,
|
|
69
|
+
phases: {
|
|
70
|
+
total: 0,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
59
74
|
const serializeNativeFormDataBody = (form) => {
|
|
60
75
|
const response = new globalThis.Response(form);
|
|
61
76
|
return {
|
|
@@ -66,7 +81,7 @@ const serializeNativeFormDataBody = (form) => {
|
|
|
66
81
|
// A body is replayable only if iterating it again restarts from the beginning.
|
|
67
82
|
// Node streams, Web `ReadableStream`s, generators, and self-iterating (one-shot) iterators all yield their data only once, so they cannot be replayed on a redirect.
|
|
68
83
|
const isNonReplayableBody = (body) => is.nodeStream(body)
|
|
69
|
-
||
|
|
84
|
+
|| body instanceof ReadableStream
|
|
70
85
|
|| is.generator(body)
|
|
71
86
|
|| (is.asyncIterable(body) && body[Symbol.asyncIterator]() === body)
|
|
72
87
|
|| (is.iterable(body) && body[Symbol.iterator]() === body);
|
|
@@ -120,12 +135,13 @@ export const normalizeError = (error) => {
|
|
|
120
135
|
const getSanitizedUrl = (options) => options?.url ? stripUrlAuth(options.url) : '';
|
|
121
136
|
const makeProgress = (transferred, total) => {
|
|
122
137
|
let percent = 0;
|
|
123
|
-
if (total) {
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
else if (total === transferred) {
|
|
138
|
+
if (total === transferred) {
|
|
139
|
+
// Known-size complete transfers (including 0/0) should report 100% rather than 0%.
|
|
127
140
|
percent = 1;
|
|
128
141
|
}
|
|
142
|
+
else if (total) {
|
|
143
|
+
percent = transferred / total;
|
|
144
|
+
}
|
|
129
145
|
return { percent, transferred, total };
|
|
130
146
|
};
|
|
131
147
|
export default class Request extends Duplex {
|
|
@@ -159,6 +175,7 @@ export default class Request extends Duplex {
|
|
|
159
175
|
_skipRequestEndInFinal = false;
|
|
160
176
|
_hasWrittenBody = false;
|
|
161
177
|
_hasWritableBody = false;
|
|
178
|
+
_discardBodyWrites = false;
|
|
162
179
|
_incrementalDecode;
|
|
163
180
|
_requestId = generateRequestId();
|
|
164
181
|
// We need this because `this._request` if `undefined` when using cache
|
|
@@ -275,7 +292,10 @@ export default class Request extends Duplex {
|
|
|
275
292
|
const { response, options } = this;
|
|
276
293
|
const attemptCount = this.retryCount + (error.name === 'RetryError' ? 0 : 1);
|
|
277
294
|
this._stopReading = true;
|
|
278
|
-
if (
|
|
295
|
+
if (error instanceof TimedOutTimeoutError) {
|
|
296
|
+
error = new TimeoutError(error, this.timings ?? createPreRequestErrorTimings(), this);
|
|
297
|
+
}
|
|
298
|
+
else if (!(error instanceof RequestError)) {
|
|
279
299
|
error = new RequestError(error.message, error, this);
|
|
280
300
|
}
|
|
281
301
|
const typedError = error;
|
|
@@ -283,15 +303,17 @@ export default class Request extends Duplex {
|
|
|
283
303
|
// Node.js parser is really weird.
|
|
284
304
|
// It emits post-request Parse Errors on the same instance as previous request. WTF.
|
|
285
305
|
// Therefore, we need to check if it has been destroyed as well.
|
|
286
|
-
//
|
|
287
|
-
// Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket,
|
|
288
|
-
// but makes the response unreadable. So we additionally need to check `response.readable`.
|
|
289
306
|
if (response?.readable && !response.rawBody && !this._request?.socket?.destroyed) {
|
|
290
307
|
// @types/node has incorrect typings. `setEncoding` accepts `null` as well.
|
|
291
308
|
response.setEncoding(this.readableEncoding);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
await this._setRawBody(response);
|
|
310
|
+
}
|
|
311
|
+
if (response?.rawBody && response.body === undefined) {
|
|
312
|
+
try {
|
|
313
|
+
response.body = decodeUint8Array(response.rawBody, options.encoding);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Preserve the original request error when decoding its response body also fails.
|
|
295
317
|
}
|
|
296
318
|
}
|
|
297
319
|
if (this.listenerCount('retry') !== 0) {
|
|
@@ -302,13 +324,13 @@ export default class Request extends Duplex {
|
|
|
302
324
|
retryAfter = Number(response.headers['retry-after']);
|
|
303
325
|
if (Number.isNaN(retryAfter)) {
|
|
304
326
|
retryAfter = Date.parse(response.headers['retry-after']) - Date.now();
|
|
305
|
-
if (retryAfter <= 0) {
|
|
306
|
-
retryAfter = 1;
|
|
307
|
-
}
|
|
308
327
|
}
|
|
309
328
|
else {
|
|
310
329
|
retryAfter *= 1000;
|
|
311
330
|
}
|
|
331
|
+
if (retryAfter <= 0) {
|
|
332
|
+
retryAfter = 1;
|
|
333
|
+
}
|
|
312
334
|
}
|
|
313
335
|
const retryOptions = options.retry;
|
|
314
336
|
const computedValue = calculateRetryDelay({
|
|
@@ -471,8 +493,12 @@ export default class Request extends Duplex {
|
|
|
471
493
|
}
|
|
472
494
|
}
|
|
473
495
|
_write(chunk, encoding, callback) {
|
|
474
|
-
this._hasWrittenBody = true;
|
|
475
496
|
const write = () => {
|
|
497
|
+
if (this._discardBodyWrites) {
|
|
498
|
+
callback();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this._hasWrittenBody = true;
|
|
476
502
|
this._writeRequest(chunk, encoding, callback);
|
|
477
503
|
};
|
|
478
504
|
if (this._requestInitialized) {
|
|
@@ -484,6 +510,11 @@ export default class Request extends Duplex {
|
|
|
484
510
|
}
|
|
485
511
|
_final(callback) {
|
|
486
512
|
const endRequest = () => {
|
|
513
|
+
if (this._discardBodyWrites) {
|
|
514
|
+
this._hasWritableBody = false;
|
|
515
|
+
callback();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
487
518
|
if (this._skipRequestEndInFinal) {
|
|
488
519
|
this._skipRequestEndInFinal = false;
|
|
489
520
|
callback();
|
|
@@ -503,11 +534,14 @@ export default class Request extends Duplex {
|
|
|
503
534
|
if (request?._writableState?.errored) {
|
|
504
535
|
return;
|
|
505
536
|
}
|
|
506
|
-
if (!error) {
|
|
507
|
-
this._emitUploadComplete(request);
|
|
508
|
-
}
|
|
509
537
|
this._hasWritableBody = false;
|
|
510
|
-
|
|
538
|
+
if (error) {
|
|
539
|
+
// `ClientRequest.end()` can report the same failure as the request's `error` event. Route it through Got's retry handling without completing `_final`, so this Duplex does not finish a failed upload.
|
|
540
|
+
this._beforeError(error);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
this._emitUploadComplete(request);
|
|
544
|
+
callback();
|
|
511
545
|
});
|
|
512
546
|
};
|
|
513
547
|
if (this._requestInitialized) {
|
|
@@ -524,6 +558,7 @@ export default class Request extends Duplex {
|
|
|
524
558
|
this._stopRetry?.();
|
|
525
559
|
this._cancelTimeouts?.();
|
|
526
560
|
this._abortListenerDisposer?.[Symbol.dispose]();
|
|
561
|
+
this._destroyInFlightAlpnSocket();
|
|
527
562
|
if (this.options) {
|
|
528
563
|
const { body } = this.options;
|
|
529
564
|
if (is.nodeStream(body)) {
|
|
@@ -548,7 +583,7 @@ export default class Request extends Duplex {
|
|
|
548
583
|
}
|
|
549
584
|
// Preserve custom errors returned by beforeError hooks.
|
|
550
585
|
// For other errors, wrap non-RequestError instances for consistency.
|
|
551
|
-
if (error !== null
|
|
586
|
+
if (error !== null) {
|
|
552
587
|
const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
|
|
553
588
|
if (!processedByHooks && !(error instanceof RequestError)) {
|
|
554
589
|
error = error instanceof Error
|
|
@@ -580,9 +615,10 @@ export default class Request extends Duplex {
|
|
|
580
615
|
return;
|
|
581
616
|
}
|
|
582
617
|
const abort = () => {
|
|
618
|
+
this._destroyInFlightAlpnSocket();
|
|
583
619
|
// See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static#return_value
|
|
584
620
|
if (signal.reason?.name === 'TimeoutError') {
|
|
585
|
-
this.destroy(new TimeoutError(signal.reason, this.timings, this));
|
|
621
|
+
this.destroy(new TimeoutError(signal.reason, this.timings ?? createPreRequestErrorTimings(), this));
|
|
586
622
|
}
|
|
587
623
|
else {
|
|
588
624
|
this.destroy(new AbortError(this));
|
|
@@ -595,12 +631,14 @@ export default class Request extends Duplex {
|
|
|
595
631
|
this._abortListenerDisposer = addAbortListener(signal, abort);
|
|
596
632
|
}
|
|
597
633
|
}
|
|
634
|
+
_destroyInFlightAlpnSocket() {
|
|
635
|
+
this._requestOptions?._alpnSocket?.destroy();
|
|
636
|
+
}
|
|
598
637
|
_shouldIncrementallyDecodeBody() {
|
|
599
638
|
const { responseType, encoding } = this.options;
|
|
600
639
|
return Boolean(this._noPipe)
|
|
601
640
|
&& (responseType === 'text' || responseType === 'json')
|
|
602
|
-
&& isUtf8Encoding(encoding)
|
|
603
|
-
&& typeof globalThis.TextDecoder === 'function';
|
|
641
|
+
&& isUtf8Encoding(encoding);
|
|
604
642
|
}
|
|
605
643
|
_checkContentLengthMismatch() {
|
|
606
644
|
if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
|
|
@@ -734,16 +772,15 @@ export default class Request extends Duplex {
|
|
|
734
772
|
// turn an irrelevant redirect body into a client-side failure or decompression DoS.
|
|
735
773
|
const shouldFollowRedirect = isRedirect && (typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect);
|
|
736
774
|
if (options.decompress && !hasNoBody && !shouldFollowRedirect) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
775
|
+
response = decompressResponse(response);
|
|
776
|
+
typedResponse = prepareResponse(response);
|
|
777
|
+
// When strictContentLength is enabled, track the compressed bytes emitted by the native response.
|
|
778
|
+
if (options.strictContentLength && response !== nativeResponse) {
|
|
740
779
|
this._compressedBytesCount = 0;
|
|
741
780
|
nativeResponse.on('data', (chunk) => {
|
|
742
781
|
this._compressedBytesCount += byteLength(chunk);
|
|
743
782
|
});
|
|
744
783
|
}
|
|
745
|
-
response = decompressResponse(response);
|
|
746
|
-
typedResponse = prepareResponse(response);
|
|
747
784
|
}
|
|
748
785
|
// `decompressResponse` wraps the response stream when it decompresses,
|
|
749
786
|
// so `response !== nativeResponse` indicates decompression happened.
|
|
@@ -825,12 +862,13 @@ export default class Request extends Duplex {
|
|
|
825
862
|
// the outward stream until cookie handling has finished.
|
|
826
863
|
response.once('end', handleResponseEnd);
|
|
827
864
|
}
|
|
828
|
-
const
|
|
865
|
+
const rawCookies = response.headers['set-cookie'];
|
|
866
|
+
const responseRawBodyPromise = this._noPipe
|
|
829
867
|
&& is.object(options.cookieJar)
|
|
830
|
-
&&
|
|
868
|
+
&& rawCookies !== undefined
|
|
869
|
+
&& !shouldFollowRedirect
|
|
831
870
|
? this._setRawBody(response)
|
|
832
871
|
: undefined;
|
|
833
|
-
const rawCookies = response.headers['set-cookie'];
|
|
834
872
|
if (is.object(options.cookieJar) && rawCookies) {
|
|
835
873
|
let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
|
|
836
874
|
if (options.ignoreInvalidCookies) {
|
|
@@ -845,6 +883,9 @@ export default class Request extends Duplex {
|
|
|
845
883
|
await Promise.all(promises);
|
|
846
884
|
}
|
|
847
885
|
catch (error) {
|
|
886
|
+
if (responseRawBodyPromise) {
|
|
887
|
+
await responseRawBodyPromise;
|
|
888
|
+
}
|
|
848
889
|
this._beforeError(normalizeError(error));
|
|
849
890
|
return;
|
|
850
891
|
}
|
|
@@ -902,14 +943,15 @@ export default class Request extends Duplex {
|
|
|
902
943
|
}
|
|
903
944
|
else if (isDifferentOrigin
|
|
904
945
|
&& canRewrite
|
|
946
|
+
&& updatedOptions.method !== 'QUERY'
|
|
905
947
|
&& this._hasBodyForRedirect(updatedOptions)) {
|
|
906
948
|
this._dropBody(updatedOptions);
|
|
907
949
|
}
|
|
908
950
|
if (isDifferentOrigin) {
|
|
909
951
|
// On cross-origin redirects, strip sensitive headers and any credentials
|
|
910
952
|
// embedded in the redirect URL itself to prevent a malicious server from
|
|
911
|
-
// leaking them to a third party.
|
|
912
|
-
|
|
953
|
+
// leaking them to a third party. 307/308 redirects preserve the method and replayable body per RFC; QUERY does the same on 301/302.
|
|
954
|
+
updatedOptions.h2session = undefined;
|
|
913
955
|
this._stripCrossOriginState(updatedOptions, redirectUrl);
|
|
914
956
|
}
|
|
915
957
|
else {
|
|
@@ -928,6 +970,7 @@ export default class Request extends Duplex {
|
|
|
928
970
|
this.redirectUrls.push(redirectUrl);
|
|
929
971
|
const boundaryBeforeRedirectHooks = getUrlPrefixBoundary(updatedOptions);
|
|
930
972
|
const bodyBeforeRedirectHooks = updatedOptions.body;
|
|
973
|
+
const h2sessionBeforeRedirectHooks = updatedOptions.h2session;
|
|
931
974
|
const preHookState = isDifferentOrigin
|
|
932
975
|
? undefined
|
|
933
976
|
: {
|
|
@@ -946,7 +989,8 @@ export default class Request extends Duplex {
|
|
|
946
989
|
}
|
|
947
990
|
updatedOptions.clearUnchangedCookieHeader(preHookState, changedState);
|
|
948
991
|
const nativeFormDataBody = this._nativeFormDataBody;
|
|
949
|
-
|
|
992
|
+
const mustReplayBodyOnRedirect = statusCode === 307 || statusCode === 308 || updatedOptions.method === 'QUERY';
|
|
993
|
+
if (mustReplayBodyOnRedirect) {
|
|
950
994
|
const bodyUnchangedByHooks = updatedOptions.body === bodyBeforeRedirectHooks;
|
|
951
995
|
const wasNonReplayable = isNonReplayableBody(bodyBeforeRedirectHooks);
|
|
952
996
|
if (!bodyUnchangedByHooks && wasNonReplayable) {
|
|
@@ -969,7 +1013,7 @@ export default class Request extends Duplex {
|
|
|
969
1013
|
}
|
|
970
1014
|
else if (bodyUnchangedByHooks
|
|
971
1015
|
&& (wasNonReplayable || (is.undefined(updatedOptions.body) && (this._hasWrittenBody || this._hasWritableBody)))) {
|
|
972
|
-
//
|
|
1016
|
+
// Body-preserving redirects must replay the body, so follow the HTTP spec and other clients by failing for unchanged non-replayable bodies. Hooks may supply a fresh body.
|
|
973
1017
|
this._dropBody(updatedOptions);
|
|
974
1018
|
this._beforeError(new RequestError('Cannot follow redirect with a non-replayable body', {}, this));
|
|
975
1019
|
return;
|
|
@@ -989,7 +1033,11 @@ export default class Request extends Duplex {
|
|
|
989
1033
|
this._dropBody(updatedOptions);
|
|
990
1034
|
}
|
|
991
1035
|
if (hookChangedOrigin) {
|
|
1036
|
+
if (updatedOptions.h2session === h2sessionBeforeRedirectHooks) {
|
|
1037
|
+
updatedOptions.h2session = undefined;
|
|
1038
|
+
}
|
|
992
1039
|
if (canRewrite
|
|
1040
|
+
&& updatedOptions.method !== 'QUERY'
|
|
993
1041
|
&& this._hasUnchangedBodyForRedirect(updatedOptions, state, changedState)) {
|
|
994
1042
|
this._dropBody(updatedOptions);
|
|
995
1043
|
}
|
|
@@ -1006,8 +1054,8 @@ export default class Request extends Duplex {
|
|
|
1006
1054
|
// Publish redirect event
|
|
1007
1055
|
publishRedirect({
|
|
1008
1056
|
requestId: this._requestId,
|
|
1009
|
-
fromUrl: url
|
|
1010
|
-
toUrl: (updatedOptions.url)
|
|
1057
|
+
fromUrl: stripUrlAuth(url),
|
|
1058
|
+
toUrl: stripUrlAuth(updatedOptions.url),
|
|
1011
1059
|
statusCode,
|
|
1012
1060
|
});
|
|
1013
1061
|
this.emit('redirect', updatedOptions, typedResponse);
|
|
@@ -1020,8 +1068,6 @@ export default class Request extends Duplex {
|
|
|
1020
1068
|
}
|
|
1021
1069
|
return;
|
|
1022
1070
|
}
|
|
1023
|
-
canFinalizeResponse = true;
|
|
1024
|
-
handleResponseEnd();
|
|
1025
1071
|
// `HTTPError`s always have `error.response.body` defined.
|
|
1026
1072
|
// Therefore, we cannot retry if `options.throwHttpErrors` is false.
|
|
1027
1073
|
// On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
|
|
@@ -1056,11 +1102,16 @@ export default class Request extends Duplex {
|
|
|
1056
1102
|
response.pause();
|
|
1057
1103
|
});
|
|
1058
1104
|
if (this._noPipe) {
|
|
1059
|
-
const captureFromResponse = response.readableEnded ||
|
|
1060
|
-
|
|
1061
|
-
|
|
1105
|
+
const captureFromResponse = response.readableEnded || responseRawBodyPromise !== undefined;
|
|
1106
|
+
if (!captureFromResponse) {
|
|
1107
|
+
canFinalizeResponse = true;
|
|
1108
|
+
handleResponseEnd();
|
|
1109
|
+
}
|
|
1110
|
+
const success = responseRawBodyPromise
|
|
1111
|
+
? await responseRawBodyPromise
|
|
1062
1112
|
: await this._setRawBody(captureFromResponse ? response : this);
|
|
1063
1113
|
if (captureFromResponse) {
|
|
1114
|
+
canFinalizeResponse = true;
|
|
1064
1115
|
handleResponseEnd();
|
|
1065
1116
|
}
|
|
1066
1117
|
if (success) {
|
|
@@ -1089,6 +1140,11 @@ export default class Request extends Duplex {
|
|
|
1089
1140
|
}
|
|
1090
1141
|
destination.statusCode = statusCode;
|
|
1091
1142
|
}
|
|
1143
|
+
if (this._triggerRead) {
|
|
1144
|
+
this._read();
|
|
1145
|
+
}
|
|
1146
|
+
canFinalizeResponse = true;
|
|
1147
|
+
handleResponseEnd();
|
|
1092
1148
|
}
|
|
1093
1149
|
async _setRawBody(from = this) {
|
|
1094
1150
|
try {
|
|
@@ -1146,19 +1202,13 @@ export default class Request extends Duplex {
|
|
|
1146
1202
|
headers: options.headers,
|
|
1147
1203
|
});
|
|
1148
1204
|
timer(request);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
// 2. With HTTP/2 connection reuse, setTimeout(0) accumulates listeners on the socket
|
|
1155
|
-
// 3. removeAllListeners('timeout') properly cleans up without the memory leak
|
|
1156
|
-
request.removeAllListeners('timeout');
|
|
1157
|
-
// For HTTP/2, wait for socket and remove timeout listeners from it
|
|
1158
|
-
request.once('socket', (socket) => {
|
|
1159
|
-
socket.removeAllListeners('timeout');
|
|
1160
|
-
});
|
|
1205
|
+
const { isGotHttp2Request } = request;
|
|
1206
|
+
let timeoutDelays = timeout;
|
|
1207
|
+
if (isGotHttp2Request) {
|
|
1208
|
+
const { socket: _socket, ...http2TimeoutDelays } = timeout;
|
|
1209
|
+
timeoutDelays = http2TimeoutDelays;
|
|
1161
1210
|
}
|
|
1211
|
+
this._cancelTimeouts = timedOut(request, timeoutDelays, url);
|
|
1162
1212
|
let lastRequestError;
|
|
1163
1213
|
const responseEventName = options.cache ? 'cacheableResponse' : 'response';
|
|
1164
1214
|
request.once(responseEventName, (response) => {
|
|
@@ -1168,7 +1218,7 @@ export default class Request extends Duplex {
|
|
|
1168
1218
|
this._aborted = true;
|
|
1169
1219
|
// Force clean-up, because some packages (e.g. nock) don't do this.
|
|
1170
1220
|
request.destroy();
|
|
1171
|
-
const wrappedError = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings, this) : new RequestError(error.message, error, this);
|
|
1221
|
+
const wrappedError = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings ?? createPreRequestErrorTimings(), this) : new RequestError(error.message, error, this);
|
|
1172
1222
|
this._beforeError(wrappedError);
|
|
1173
1223
|
};
|
|
1174
1224
|
request.once('error', (error) => {
|
|
@@ -1238,7 +1288,7 @@ export default class Request extends Duplex {
|
|
|
1238
1288
|
_sendBody() {
|
|
1239
1289
|
// Send body
|
|
1240
1290
|
const { body } = this.options;
|
|
1241
|
-
const currentRequest = this.redirectUrls.length === 0 ? this : this._request ?? this;
|
|
1291
|
+
const currentRequest = this.redirectUrls.length === 0 && !this._discardBodyWrites ? this : this._request ?? this;
|
|
1242
1292
|
if (is.nodeStream(body)) {
|
|
1243
1293
|
body.pipe(currentRequest);
|
|
1244
1294
|
}
|
|
@@ -1577,7 +1627,7 @@ export default class Request extends Duplex {
|
|
|
1577
1627
|
const result = requestOptions._request(requestOptions, wrappedHandler);
|
|
1578
1628
|
// TODO: remove this when `cacheable-request` supports async request functions.
|
|
1579
1629
|
if (is.promise(result)) {
|
|
1580
|
-
// We only need to implement the error handler in order to support
|
|
1630
|
+
// We only need to implement the error handler in order to support HTTP/2 caching.
|
|
1581
1631
|
// The result will be a promise anyway.
|
|
1582
1632
|
// @ts-expect-error ignore
|
|
1583
1633
|
result.once = (event, handler) => {
|
|
@@ -1604,7 +1654,7 @@ export default class Request extends Duplex {
|
|
|
1604
1654
|
}
|
|
1605
1655
|
else {
|
|
1606
1656
|
/* istanbul ignore next: safety check */
|
|
1607
|
-
throw new Error(`Unknown
|
|
1657
|
+
throw new Error(`Unknown HTTP/2 promise event: ${event}`);
|
|
1608
1658
|
}
|
|
1609
1659
|
return result;
|
|
1610
1660
|
};
|
|
@@ -1618,7 +1668,7 @@ export default class Request extends Duplex {
|
|
|
1618
1668
|
Object.assign(options, {
|
|
1619
1669
|
protocol: url.protocol,
|
|
1620
1670
|
hostname: is.string(url.hostname) && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
|
|
1621
|
-
host: url.
|
|
1671
|
+
host: is.string(url.hostname) && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
|
|
1622
1672
|
hash: url.hash === '' ? '' : (url.hash ?? null),
|
|
1623
1673
|
search: url.search === '' ? '' : (url.search ?? null),
|
|
1624
1674
|
pathname: url.pathname,
|
|
@@ -1748,6 +1798,8 @@ export default class Request extends Duplex {
|
|
|
1748
1798
|
let shouldOmitRequestUrlCredentials = false;
|
|
1749
1799
|
const urlBeforeRequestHooks = options.url instanceof URL ? new URL(options.url) : undefined;
|
|
1750
1800
|
const boundaryBeforeRequestHooks = getUrlPrefixBoundary(options);
|
|
1801
|
+
const stateBeforeRequestHooks = urlBeforeRequestHooks ? snapshotCrossOriginState(options) : undefined;
|
|
1802
|
+
const crossOriginHookStrippedHeaders = new Set();
|
|
1751
1803
|
const changedState = await options.trackStateMutations(async (changedState) => {
|
|
1752
1804
|
for (const hook of options.hooks.beforeRequest) {
|
|
1753
1805
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -1765,13 +1817,53 @@ export default class Request extends Duplex {
|
|
|
1765
1817
|
&& hasUrlOrPrefixUrlBoundaryChanged(options, options.url, boundaryBeforeRequestHooks)) {
|
|
1766
1818
|
assertUrlHasSameOriginAsPrefixUrlIfNeeded(options, options.url);
|
|
1767
1819
|
}
|
|
1820
|
+
if (urlBeforeRequestHooks
|
|
1821
|
+
&& options.url instanceof URL
|
|
1822
|
+
&& !isSameOrigin(urlBeforeRequestHooks, options.url)) {
|
|
1823
|
+
const hookChangedState = new Set(changedState);
|
|
1824
|
+
const currentHeaders = options.getInternalHeaders();
|
|
1825
|
+
const changedHeaders = {};
|
|
1826
|
+
for (const header of crossOriginStripHeaders) {
|
|
1827
|
+
if (hookChangedState.has(header)) {
|
|
1828
|
+
changedHeaders[header] = currentHeaders[header];
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
options.deleteInternalHeader(header);
|
|
1832
|
+
crossOriginHookStrippedHeaders.add(header);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const changedOptions = { headers: changedHeaders };
|
|
1836
|
+
if (hookChangedState.has('url')) {
|
|
1837
|
+
changedOptions.url = options.url;
|
|
1838
|
+
}
|
|
1839
|
+
if (hookChangedState.has('prefixUrl')) {
|
|
1840
|
+
changedOptions.prefixUrl = options.prefixUrl;
|
|
1841
|
+
}
|
|
1842
|
+
if (hookChangedState.has('username')) {
|
|
1843
|
+
changedOptions.username = options.username;
|
|
1844
|
+
}
|
|
1845
|
+
if (hookChangedState.has('password')) {
|
|
1846
|
+
changedOptions.password = options.password;
|
|
1847
|
+
}
|
|
1848
|
+
options.stripSensitiveHeaders(urlBeforeRequestHooks, options.url, changedOptions);
|
|
1849
|
+
this._discardBodyWrites = true;
|
|
1850
|
+
this._hasWrittenBody = false;
|
|
1851
|
+
this._hasWritableBody = false;
|
|
1852
|
+
if (!hookChangedState.has('body')
|
|
1853
|
+
&& !hookChangedState.has('json')
|
|
1854
|
+
&& !hookChangedState.has('form')
|
|
1855
|
+
&& isBodyUnchanged(options, stateBeforeRequestHooks)) {
|
|
1856
|
+
options.clearBody();
|
|
1857
|
+
this._bodySize = undefined;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1768
1860
|
if (request === undefined) {
|
|
1769
1861
|
const currentHeaders = options.getInternalHeaders();
|
|
1770
1862
|
// `headers.authorization = undefined` / `headers.cookie = undefined` is an
|
|
1771
1863
|
// explicit opt-out. Respect that instead of regenerating values from URL
|
|
1772
1864
|
// credentials or the cookie jar later in request setup.
|
|
1773
1865
|
const isHeaderExplicitlyOmitted = (header) => options.isHeaderExplicitlySet(header)
|
|
1774
|
-
&& Object.hasOwn(currentHeaders, header)
|
|
1866
|
+
&& (Object.hasOwn(currentHeaders, header) || changedState.has(header))
|
|
1775
1867
|
&& is.undefined(currentHeaders[header]);
|
|
1776
1868
|
const currentAuthorizationHeader = currentHeaders.authorization;
|
|
1777
1869
|
const currentCookieHeader = currentHeaders.cookie;
|
|
@@ -1782,7 +1874,9 @@ export default class Request extends Duplex {
|
|
|
1782
1874
|
// - Otherwise, if the request did not start with explicit Authorization, Got may
|
|
1783
1875
|
// generate Basic auth from the current username/password.
|
|
1784
1876
|
const authorizationWasExplicitlyOmitted = isHeaderExplicitlyOmitted('authorization')
|
|
1785
|
-
|| (authorizationWasInitiallyExplicit
|
|
1877
|
+
|| (authorizationWasInitiallyExplicit
|
|
1878
|
+
&& !crossOriginHookStrippedHeaders.has('authorization')
|
|
1879
|
+
&& is.undefined(currentAuthorizationHeader));
|
|
1786
1880
|
const cookieWasExplicitlyOmitted = is.undefined(currentCookieHeader)
|
|
1787
1881
|
&& (cookieWasInitiallyOmitted || isHeaderExplicitlyOmitted('cookie'));
|
|
1788
1882
|
sanitizeHeaders();
|
|
@@ -1806,7 +1900,7 @@ export default class Request extends Duplex {
|
|
|
1806
1900
|
// A beforeRequest hook intentionally set the outgoing Authorization header.
|
|
1807
1901
|
}
|
|
1808
1902
|
else {
|
|
1809
|
-
const restorableAuthorizationHeader = changedState.has('authorization') && is.undefined(currentAuthorizationHeader)
|
|
1903
|
+
const restorableAuthorizationHeader = crossOriginHookStrippedHeaders.has('authorization') || (changedState.has('authorization') && is.undefined(currentAuthorizationHeader))
|
|
1810
1904
|
? undefined
|
|
1811
1905
|
: explicitAuthorizationHeader;
|
|
1812
1906
|
syncGeneratedHeader('authorization', {
|
|
@@ -1829,9 +1923,12 @@ export default class Request extends Duplex {
|
|
|
1829
1923
|
const cookieHeader = !cookieWasInitiallyOmitted && !cookieWasExplicitlyOmitted
|
|
1830
1924
|
? await getCookieHeader(cookieJar)
|
|
1831
1925
|
: undefined;
|
|
1926
|
+
const restorableCookieHeader = crossOriginHookStrippedHeaders.has('cookie')
|
|
1927
|
+
? undefined
|
|
1928
|
+
: explicitCookieHeader;
|
|
1832
1929
|
syncGeneratedHeader('cookie', {
|
|
1833
1930
|
currentHeader: currentCookieHeader,
|
|
1834
|
-
explicitHeader:
|
|
1931
|
+
explicitHeader: restorableCookieHeader,
|
|
1835
1932
|
nextHeader: cookieHeader,
|
|
1836
1933
|
staleGeneratedHeader: generatedCookieHeader,
|
|
1837
1934
|
});
|
|
@@ -1863,21 +1960,36 @@ export default class Request extends Duplex {
|
|
|
1863
1960
|
try {
|
|
1864
1961
|
// We can't do `await fn(...)`,
|
|
1865
1962
|
// because stream `error` event can be emitted before `Promise.resolve()`.
|
|
1963
|
+
const requestFunctionStartedAt = Date.now();
|
|
1964
|
+
const originalRequestTimeout = options.timeout.request;
|
|
1965
|
+
let shouldRestoreRequestTimeout = false;
|
|
1866
1966
|
let requestOrResponse = function_(url, this._requestOptions);
|
|
1867
1967
|
if (is.promise(requestOrResponse)) {
|
|
1868
1968
|
requestOrResponse = await requestOrResponse;
|
|
1969
|
+
if (options.timeout.request !== undefined) {
|
|
1970
|
+
const remainingRequestTimeout = options.timeout.request - (Date.now() - requestFunctionStartedAt);
|
|
1971
|
+
options.timeout.request = Math.max(0, remainingRequestTimeout);
|
|
1972
|
+
shouldRestoreRequestTimeout = true;
|
|
1973
|
+
}
|
|
1869
1974
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
this.once('finish', () => {
|
|
1975
|
+
try {
|
|
1976
|
+
if (isClientRequest(requestOrResponse)) {
|
|
1977
|
+
this._onRequest(requestOrResponse);
|
|
1978
|
+
}
|
|
1979
|
+
else if (this.writableEnded) {
|
|
1878
1980
|
void this._onResponse(requestOrResponse);
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
this.once('finish', () => {
|
|
1984
|
+
void this._onResponse(requestOrResponse);
|
|
1985
|
+
});
|
|
1986
|
+
this._sendBody();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
finally {
|
|
1990
|
+
if (shouldRestoreRequestTimeout) {
|
|
1991
|
+
options.timeout.request = originalRequestTimeout;
|
|
1992
|
+
}
|
|
1881
1993
|
}
|
|
1882
1994
|
}
|
|
1883
1995
|
catch (error) {
|