@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 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) @ 15.1.0 |
17
- | Processed | 2026-07-21 |
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.8.0 |
27
+ | type-fest | ^5.6.0 | ^5.10.0 |
28
28
 
29
29
  ---
30
30
 
package/changes.json CHANGED
@@ -10,9 +10,9 @@
10
10
  },
11
11
  "type-fest": {
12
12
  "from": "^5.6.0",
13
- "to": "^5.8.0"
13
+ "to": "^5.10.0"
14
14
  }
15
15
  },
16
- "timestamp": "2026-07-21T16:15:47.761Z",
16
+ "timestamp": "2026-09-17T16:10:47.696Z",
17
17
  "totalUpdated": 3
18
18
  }
@@ -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(request.options.resolveBodyOnly ? response.body : response);
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.timeout`
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
- || (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream)
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
- percent = transferred / total;
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 (!(error instanceof RequestError)) {
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
- const success = await this._setRawBody(response);
293
- if (success) {
294
- response.body = decodeUint8Array(response.rawBody);
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
- callback(error);
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 && !is.undefined(error)) {
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
- // When strictContentLength is enabled, track compressed bytes by listening to
738
- // the native response's data events before decompression
739
- if (options.strictContentLength) {
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 noPipeCookieJarRawBodyPromise = this._noPipe
865
+ const rawCookies = response.headers['set-cookie'];
866
+ const responseRawBodyPromise = this._noPipe
829
867
  && is.object(options.cookieJar)
830
- && !isRedirect
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. The request body is preserved per RFC:
912
- // 307/308 keep the method and replayable bodies, even cross-origin.
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
- if (statusCode === 307 || statusCode === 308) {
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
- // 307/308 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.
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.toString(),
1010
- toUrl: (updatedOptions.url).toString(),
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 || noPipeCookieJarRawBodyPromise !== undefined;
1060
- const success = noPipeCookieJarRawBodyPromise
1061
- ? await noPipeCookieJarRawBodyPromise
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
- this._cancelTimeouts = timedOut(request, timeout, url);
1150
- if (this.options.http2) {
1151
- // Unset stream timeout, as the `timeout` option was used only for connection timeout.
1152
- // We remove all 'timeout' listeners instead of calling setTimeout(0) because:
1153
- // 1. setTimeout(0) causes a memory leak (see https://github.com/sindresorhus/got/issues/690)
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 HTTP2 caching.
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 HTTP2 promise event: ${event}`);
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.host,
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 && is.undefined(currentAuthorizationHeader));
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: explicitCookieHeader,
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
- if (isClientRequest(requestOrResponse)) {
1871
- this._onRequest(requestOrResponse);
1872
- }
1873
- else if (this.writableEnded) {
1874
- void this._onResponse(requestOrResponse);
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
- this._sendBody();
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) {