@depup/got 14.6.6-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.
Files changed (53) hide show
  1. package/README.md +36 -0
  2. package/changes.json +30 -0
  3. package/dist/source/as-promise/index.d.ts +3 -0
  4. package/dist/source/as-promise/index.js +204 -0
  5. package/dist/source/as-promise/types.d.ts +37 -0
  6. package/dist/source/as-promise/types.js +17 -0
  7. package/dist/source/core/calculate-retry-delay.d.ts +4 -0
  8. package/dist/source/core/calculate-retry-delay.js +29 -0
  9. package/dist/source/core/diagnostics-channel.d.ts +89 -0
  10. package/dist/source/core/diagnostics-channel.js +49 -0
  11. package/dist/source/core/errors.d.ts +102 -0
  12. package/dist/source/core/errors.js +147 -0
  13. package/dist/source/core/index.d.ts +192 -0
  14. package/dist/source/core/index.js +1339 -0
  15. package/dist/source/core/options.d.ts +1564 -0
  16. package/dist/source/core/options.js +1838 -0
  17. package/dist/source/core/parse-link-header.d.ts +4 -0
  18. package/dist/source/core/parse-link-header.js +33 -0
  19. package/dist/source/core/response.d.ts +109 -0
  20. package/dist/source/core/response.js +41 -0
  21. package/dist/source/core/timed-out.d.ts +31 -0
  22. package/dist/source/core/timed-out.js +136 -0
  23. package/dist/source/core/utils/defer-to-connect.d.ts +9 -0
  24. package/dist/source/core/utils/defer-to-connect.js +44 -0
  25. package/dist/source/core/utils/get-body-size.d.ts +2 -0
  26. package/dist/source/core/utils/get-body-size.js +37 -0
  27. package/dist/source/core/utils/is-client-request.d.ts +4 -0
  28. package/dist/source/core/utils/is-client-request.js +4 -0
  29. package/dist/source/core/utils/is-form-data.d.ts +7 -0
  30. package/dist/source/core/utils/is-form-data.js +4 -0
  31. package/dist/source/core/utils/is-unix-socket-url.d.ts +17 -0
  32. package/dist/source/core/utils/is-unix-socket-url.js +25 -0
  33. package/dist/source/core/utils/options-to-url.d.ts +12 -0
  34. package/dist/source/core/utils/options-to-url.js +48 -0
  35. package/dist/source/core/utils/proxy-events.d.ts +2 -0
  36. package/dist/source/core/utils/proxy-events.js +15 -0
  37. package/dist/source/core/utils/timer.d.ts +31 -0
  38. package/dist/source/core/utils/timer.js +162 -0
  39. package/dist/source/core/utils/unhandle.d.ts +10 -0
  40. package/dist/source/core/utils/unhandle.js +20 -0
  41. package/dist/source/core/utils/url-to-options.d.ts +14 -0
  42. package/dist/source/core/utils/url-to-options.js +22 -0
  43. package/dist/source/core/utils/weakable-map.d.ts +7 -0
  44. package/dist/source/core/utils/weakable-map.js +24 -0
  45. package/dist/source/create.d.ts +3 -0
  46. package/dist/source/create.js +188 -0
  47. package/dist/source/index.d.ts +16 -0
  48. package/dist/source/index.js +22 -0
  49. package/dist/source/types.d.ts +314 -0
  50. package/dist/source/types.js +1 -0
  51. package/license +9 -0
  52. package/package.json +197 -0
  53. package/readme.md +478 -0
@@ -0,0 +1,1339 @@
1
+ import process from 'node:process';
2
+ import { Buffer } from 'node:buffer';
3
+ import { Duplex } from 'node:stream';
4
+ import http, { ServerResponse } from 'node:http';
5
+ import { byteLength } from 'byte-counter';
6
+ import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
7
+ import decompressResponse from 'decompress-response';
8
+ import is, { isBuffer } from '@sindresorhus/is';
9
+ import { FormDataEncoder, isFormData as isFormDataLike } from 'form-data-encoder';
10
+ import timer from './utils/timer.js';
11
+ import getBodySize from './utils/get-body-size.js';
12
+ import isFormData from './utils/is-form-data.js';
13
+ import proxyEvents from './utils/proxy-events.js';
14
+ import timedOut, { TimeoutError as TimedOutTimeoutError } from './timed-out.js';
15
+ import urlToOptions from './utils/url-to-options.js';
16
+ import WeakableMap from './utils/weakable-map.js';
17
+ import calculateRetryDelay from './calculate-retry-delay.js';
18
+ import Options from './options.js';
19
+ import { isResponseOk } from './response.js';
20
+ import isClientRequest from './utils/is-client-request.js';
21
+ import isUnixSocketURL, { getUnixSocketPath } from './utils/is-unix-socket-url.js';
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';
24
+ const supportsBrotli = is.string(process.versions.brotli);
25
+ const supportsZstd = is.string(process.versions.zstd);
26
+ const methodsWithoutBody = new Set(['GET', 'HEAD']);
27
+ const cacheableStore = new WeakableMap();
28
+ const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
29
+ // Track errors that have been processed by beforeError hooks to preserve custom error types
30
+ const errorsProcessedByHooks = new WeakSet();
31
+ const proxiedRequestEvents = [
32
+ 'socket',
33
+ 'connect',
34
+ 'continue',
35
+ 'information',
36
+ 'upgrade',
37
+ ];
38
+ const noop = () => { };
39
+ export default class Request extends Duplex {
40
+ // @ts-expect-error - Ignoring for now.
41
+ ['constructor'];
42
+ _noPipe;
43
+ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/9568
44
+ options;
45
+ response;
46
+ requestUrl;
47
+ redirectUrls = [];
48
+ retryCount = 0;
49
+ _stopReading = false;
50
+ _stopRetry = noop;
51
+ _downloadedSize = 0;
52
+ _uploadedSize = 0;
53
+ _pipedServerResponses = new Set();
54
+ _request;
55
+ _responseSize;
56
+ _bodySize;
57
+ _unproxyEvents = noop;
58
+ _isFromCache;
59
+ _triggerRead = false;
60
+ _jobs = [];
61
+ _cancelTimeouts = noop;
62
+ _removeListeners = noop;
63
+ _nativeResponse;
64
+ _flushed = false;
65
+ _aborted = false;
66
+ _expectedContentLength;
67
+ _compressedBytesCount;
68
+ _requestId = generateRequestId();
69
+ // We need this because `this._request` if `undefined` when using cache
70
+ _requestInitialized = false;
71
+ constructor(url, options, defaults) {
72
+ super({
73
+ // Don't destroy immediately, as the error may be emitted on unsuccessful retry
74
+ autoDestroy: false,
75
+ // It needs to be zero because we're just proxying the data to another stream
76
+ highWaterMark: 0,
77
+ });
78
+ this.on('pipe', (source) => {
79
+ if (this.options.copyPipedHeaders && source?.headers) {
80
+ Object.assign(this.options.headers, source.headers);
81
+ }
82
+ });
83
+ this.on('newListener', event => {
84
+ if (event === 'retry' && this.listenerCount('retry') > 0) {
85
+ throw new Error('A retry listener has been attached already.');
86
+ }
87
+ });
88
+ try {
89
+ this.options = new Options(url, options, defaults);
90
+ if (!this.options.url) {
91
+ if (this.options.prefixUrl === '') {
92
+ throw new TypeError('Missing `url` property');
93
+ }
94
+ this.options.url = '';
95
+ }
96
+ this.requestUrl = this.options.url;
97
+ // Publish request creation event
98
+ publishRequestCreate({
99
+ requestId: this._requestId,
100
+ url: this.options.url?.toString() ?? '',
101
+ method: this.options.method,
102
+ });
103
+ }
104
+ catch (error) {
105
+ const { options } = error;
106
+ if (options) {
107
+ this.options = options;
108
+ }
109
+ this.flush = async () => {
110
+ this.flush = async () => { };
111
+ // Defer error emission to next tick to allow user to attach error handlers
112
+ process.nextTick(() => {
113
+ // _beforeError requires options to access retry logic and hooks
114
+ if (this.options) {
115
+ this._beforeError(error);
116
+ }
117
+ else {
118
+ // Options is undefined, skip _beforeError and destroy directly
119
+ const requestError = error instanceof RequestError ? error : new RequestError(error.message, error, this);
120
+ this.destroy(requestError);
121
+ }
122
+ });
123
+ };
124
+ return;
125
+ }
126
+ // Important! If you replace `body` in a handler with another stream, make sure it's readable first.
127
+ // The below is run only once.
128
+ const { body } = this.options;
129
+ if (is.nodeStream(body)) {
130
+ body.once('error', error => {
131
+ if (this._flushed) {
132
+ this._beforeError(new UploadError(error, this));
133
+ }
134
+ else {
135
+ this.flush = async () => {
136
+ this.flush = async () => { };
137
+ this._beforeError(new UploadError(error, this));
138
+ };
139
+ }
140
+ });
141
+ }
142
+ if (this.options.signal) {
143
+ const abort = () => {
144
+ // See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static#return_value
145
+ if (this.options.signal?.reason?.name === 'TimeoutError') {
146
+ this.destroy(new TimeoutError(this.options.signal.reason, this.timings, this));
147
+ }
148
+ else {
149
+ this.destroy(new AbortError(this));
150
+ }
151
+ };
152
+ if (this.options.signal.aborted) {
153
+ abort();
154
+ }
155
+ else {
156
+ this.options.signal.addEventListener('abort', abort);
157
+ this._removeListeners = () => {
158
+ this.options.signal?.removeEventListener('abort', abort);
159
+ };
160
+ }
161
+ }
162
+ }
163
+ async flush() {
164
+ if (this._flushed) {
165
+ return;
166
+ }
167
+ this._flushed = true;
168
+ try {
169
+ await this._finalizeBody();
170
+ if (this.destroyed) {
171
+ return;
172
+ }
173
+ await this._makeRequest();
174
+ if (this.destroyed) {
175
+ this._request?.destroy();
176
+ return;
177
+ }
178
+ // Queued writes etc.
179
+ for (const job of this._jobs) {
180
+ job();
181
+ }
182
+ // Prevent memory leak
183
+ this._jobs.length = 0;
184
+ this._requestInitialized = true;
185
+ }
186
+ catch (error) {
187
+ this._beforeError(error);
188
+ }
189
+ }
190
+ _beforeError(error) {
191
+ if (this._stopReading) {
192
+ return;
193
+ }
194
+ const { response, options } = this;
195
+ const attemptCount = this.retryCount + (error.name === 'RetryError' ? 0 : 1);
196
+ this._stopReading = true;
197
+ if (!(error instanceof RequestError)) {
198
+ error = new RequestError(error.message, error, this);
199
+ }
200
+ const typedError = error;
201
+ void (async () => {
202
+ // Node.js parser is really weird.
203
+ // It emits post-request Parse Errors on the same instance as previous request. WTF.
204
+ // Therefore, we need to check if it has been destroyed as well.
205
+ //
206
+ // Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket,
207
+ // but makes the response unreadable. So we additionally need to check `response.readable`.
208
+ if (response?.readable && !response.rawBody && !this._request?.socket?.destroyed) {
209
+ // @types/node has incorrect typings. `setEncoding` accepts `null` as well.
210
+ response.setEncoding(this.readableEncoding);
211
+ const success = await this._setRawBody(response);
212
+ if (success) {
213
+ response.body = response.rawBody.toString();
214
+ }
215
+ }
216
+ if (this.listenerCount('retry') !== 0) {
217
+ let backoff;
218
+ try {
219
+ let retryAfter;
220
+ if (response && 'retry-after' in response.headers) {
221
+ retryAfter = Number(response.headers['retry-after']);
222
+ if (Number.isNaN(retryAfter)) {
223
+ retryAfter = Date.parse(response.headers['retry-after']) - Date.now();
224
+ if (retryAfter <= 0) {
225
+ retryAfter = 1;
226
+ }
227
+ }
228
+ else {
229
+ retryAfter *= 1000;
230
+ }
231
+ }
232
+ const retryOptions = options.retry;
233
+ const computedValue = calculateRetryDelay({
234
+ attemptCount,
235
+ retryOptions,
236
+ error: typedError,
237
+ retryAfter,
238
+ computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY,
239
+ });
240
+ // When enforceRetryRules is true, respect the retry rules (limit, methods, statusCodes, errorCodes)
241
+ // before calling the user's calculateDelay function. If computedValue is 0 (meaning retry is not allowed
242
+ // based on these rules), skip calling calculateDelay entirely.
243
+ // When false (default), always call calculateDelay, allowing it to override retry decisions.
244
+ if (retryOptions.enforceRetryRules && computedValue === 0) {
245
+ backoff = 0;
246
+ }
247
+ else {
248
+ backoff = await retryOptions.calculateDelay({
249
+ attemptCount,
250
+ retryOptions,
251
+ error: typedError,
252
+ retryAfter,
253
+ computedValue,
254
+ });
255
+ }
256
+ }
257
+ catch (error_) {
258
+ void this._error(new RequestError(error_.message, error_, this));
259
+ return;
260
+ }
261
+ if (backoff) {
262
+ await new Promise(resolve => {
263
+ const timeout = setTimeout(resolve, backoff);
264
+ this._stopRetry = () => {
265
+ clearTimeout(timeout);
266
+ resolve();
267
+ };
268
+ });
269
+ // Something forced us to abort the retry
270
+ if (this.destroyed) {
271
+ return;
272
+ }
273
+ // Capture body BEFORE hooks run to detect reassignment
274
+ const bodyBeforeHooks = this.options.body;
275
+ try {
276
+ for (const hook of this.options.hooks.beforeRetry) {
277
+ // eslint-disable-next-line no-await-in-loop
278
+ await hook(typedError, this.retryCount + 1);
279
+ }
280
+ }
281
+ catch (error_) {
282
+ void this._error(new RequestError(error_.message, error_, this));
283
+ return;
284
+ }
285
+ // Something forced us to abort the retry
286
+ if (this.destroyed) {
287
+ return;
288
+ }
289
+ // Preserve stream body reassigned in beforeRetry hooks.
290
+ const bodyAfterHooks = this.options.body;
291
+ const bodyWasReassigned = bodyBeforeHooks !== bodyAfterHooks;
292
+ // Resource cleanup and preservation logic for retry with body reassignment.
293
+ // The Promise wrapper (as-promise/index.ts) compares body identity to detect consumed streams,
294
+ // so we must preserve the body reference across destroy(). However, destroy() calls _destroy()
295
+ // which destroys this.options.body, creating a complex dance of clear/restore operations.
296
+ //
297
+ // Key constraints:
298
+ // 1. If body was reassigned, we must NOT destroy the NEW stream (it will be used for retry)
299
+ // 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks
300
+ // 3. We must restore the body reference after destroy() for identity checks in promise wrapper
301
+ // 4. We cannot use the normal setter after destroy() because it validates stream readability
302
+ if (bodyWasReassigned) {
303
+ const oldBody = bodyBeforeHooks;
304
+ // Temporarily clear body to prevent destroy() from destroying the new stream
305
+ this.options.body = undefined;
306
+ this.destroy();
307
+ // Clean up the old stream resource if it's a stream and different from new body
308
+ // (edge case: if old and new are same stream object, don't destroy it)
309
+ if (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {
310
+ oldBody.destroy();
311
+ }
312
+ // Restore new body for promise wrapper's identity check
313
+ // We bypass the setter because it validates stream.readable (which fails for destroyed request)
314
+ // Type assertion is necessary here to access private _internals without exposing internal API
315
+ if (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {
316
+ throw new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');
317
+ }
318
+ this.options._internals.body = bodyAfterHooks;
319
+ }
320
+ else {
321
+ // Body wasn't reassigned - use normal destroy flow which handles body cleanup
322
+ this.destroy();
323
+ // Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()
324
+ // and should not be accessed. The promise wrapper will see that body identity hasn't changed
325
+ // and will detect it's a consumed stream, which is the correct behavior.
326
+ }
327
+ // Publish retry event
328
+ publishRetry({
329
+ requestId: this._requestId,
330
+ retryCount: this.retryCount + 1,
331
+ error: typedError,
332
+ delay: backoff,
333
+ });
334
+ this.emit('retry', this.retryCount + 1, error, (updatedOptions) => {
335
+ const request = new Request(options.url, updatedOptions, options);
336
+ request.retryCount = this.retryCount + 1;
337
+ process.nextTick(() => {
338
+ void request.flush();
339
+ });
340
+ return request;
341
+ });
342
+ return;
343
+ }
344
+ }
345
+ void this._error(typedError);
346
+ })();
347
+ }
348
+ _read() {
349
+ this._triggerRead = true;
350
+ const { response } = this;
351
+ if (response && !this._stopReading) {
352
+ // We cannot put this in the `if` above
353
+ // because `.read()` also triggers the `end` event
354
+ if (response.readableLength) {
355
+ this._triggerRead = false;
356
+ }
357
+ let data;
358
+ while ((data = response.read()) !== null) {
359
+ this._downloadedSize += data.length; // eslint-disable-line @typescript-eslint/restrict-plus-operands
360
+ const progress = this.downloadProgress;
361
+ if (progress.percent < 1) {
362
+ this.emit('downloadProgress', progress);
363
+ }
364
+ this.push(data);
365
+ }
366
+ }
367
+ }
368
+ _write(chunk, encoding, callback) {
369
+ const write = () => {
370
+ this._writeRequest(chunk, encoding, callback);
371
+ };
372
+ if (this._requestInitialized) {
373
+ write();
374
+ }
375
+ else {
376
+ this._jobs.push(write);
377
+ }
378
+ }
379
+ _final(callback) {
380
+ const endRequest = () => {
381
+ // We need to check if `this._request` is present,
382
+ // because it isn't when we use cache.
383
+ if (!this._request || this._request.destroyed) {
384
+ callback();
385
+ return;
386
+ }
387
+ this._request.end((error) => {
388
+ // The request has been destroyed before `_final` finished.
389
+ // See https://github.com/nodejs/node/issues/39356
390
+ if (this._request?._writableState?.errored) {
391
+ return;
392
+ }
393
+ if (!error) {
394
+ this._bodySize = this._uploadedSize;
395
+ this.emit('uploadProgress', this.uploadProgress);
396
+ this._request?.emit('upload-complete');
397
+ }
398
+ callback(error);
399
+ });
400
+ };
401
+ if (this._requestInitialized) {
402
+ endRequest();
403
+ }
404
+ else {
405
+ this._jobs.push(endRequest);
406
+ }
407
+ }
408
+ _destroy(error, callback) {
409
+ this._stopReading = true;
410
+ this.flush = async () => { };
411
+ // Prevent further retries
412
+ this._stopRetry();
413
+ this._cancelTimeouts();
414
+ this._removeListeners();
415
+ if (this.options) {
416
+ const { body } = this.options;
417
+ if (is.nodeStream(body)) {
418
+ body.destroy();
419
+ }
420
+ }
421
+ if (this._request) {
422
+ this._request.destroy();
423
+ }
424
+ // Workaround: http-timer only sets timings.end when the response emits 'end'.
425
+ // When a stream is destroyed before completion, the 'end' event may not fire,
426
+ // leaving timings.end undefined. This should ideally be fixed in http-timer
427
+ // by listening to the 'close' event, but we handle it here for now.
428
+ // Only set timings.end if there was no error or abort (to maintain semantic correctness).
429
+ const timings = this._request?.timings;
430
+ if (timings && is.undefined(timings.end) && !is.undefined(timings.response) && is.undefined(timings.error) && is.undefined(timings.abort)) {
431
+ timings.end = Date.now();
432
+ if (is.undefined(timings.phases.total)) {
433
+ timings.phases.download = timings.end - timings.response;
434
+ timings.phases.total = timings.end - timings.start;
435
+ }
436
+ }
437
+ // Preserve custom errors returned by beforeError hooks.
438
+ // For other errors, wrap non-RequestError instances for consistency.
439
+ if (error !== null && !is.undefined(error)) {
440
+ const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
441
+ if (!processedByHooks && !(error instanceof RequestError)) {
442
+ error = error instanceof Error
443
+ ? new RequestError(error.message, error, this)
444
+ : new RequestError(String(error), {}, this);
445
+ }
446
+ }
447
+ callback(error);
448
+ }
449
+ pipe(destination, options) {
450
+ if (destination instanceof ServerResponse) {
451
+ this._pipedServerResponses.add(destination);
452
+ }
453
+ return super.pipe(destination, options);
454
+ }
455
+ unpipe(destination) {
456
+ if (destination instanceof ServerResponse) {
457
+ this._pipedServerResponses.delete(destination);
458
+ }
459
+ super.unpipe(destination);
460
+ return this;
461
+ }
462
+ _checkContentLengthMismatch() {
463
+ if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
464
+ // Use compressed bytes count when available (for compressed responses),
465
+ // otherwise use _downloadedSize (for uncompressed responses)
466
+ const actualSize = this._compressedBytesCount ?? this._downloadedSize;
467
+ if (actualSize !== this._expectedContentLength) {
468
+ this._beforeError(new ReadError({
469
+ message: `Content-Length mismatch: expected ${this._expectedContentLength} bytes, received ${actualSize} bytes`,
470
+ name: 'Error',
471
+ code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH',
472
+ }, this));
473
+ return true;
474
+ }
475
+ }
476
+ return false;
477
+ }
478
+ async _finalizeBody() {
479
+ const { options } = this;
480
+ const { headers } = options;
481
+ const isForm = !is.undefined(options.form);
482
+ // eslint-disable-next-line @typescript-eslint/naming-convention
483
+ const isJSON = !is.undefined(options.json);
484
+ const isBody = !is.undefined(options.body);
485
+ const cannotHaveBody = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
486
+ if (isForm || isJSON || isBody) {
487
+ if (cannotHaveBody) {
488
+ throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
489
+ }
490
+ // Serialize body
491
+ const noContentType = !is.string(headers['content-type']);
492
+ if (isBody) {
493
+ // Body is spec-compliant FormData
494
+ if (isFormDataLike(options.body)) {
495
+ const encoder = new FormDataEncoder(options.body);
496
+ if (noContentType) {
497
+ headers['content-type'] = encoder.headers['Content-Type'];
498
+ }
499
+ if ('Content-Length' in encoder.headers) {
500
+ headers['content-length'] = encoder.headers['Content-Length'];
501
+ }
502
+ options.body = encoder.encode();
503
+ }
504
+ // Special case for https://github.com/form-data/form-data
505
+ if (isFormData(options.body) && noContentType) {
506
+ headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
507
+ }
508
+ }
509
+ else if (isForm) {
510
+ if (noContentType) {
511
+ headers['content-type'] = 'application/x-www-form-urlencoded';
512
+ }
513
+ const { form } = options;
514
+ options.form = undefined;
515
+ options.body = (new URLSearchParams(form)).toString();
516
+ }
517
+ else {
518
+ if (noContentType) {
519
+ headers['content-type'] = 'application/json';
520
+ }
521
+ const { json } = options;
522
+ options.json = undefined;
523
+ options.body = options.stringifyJson(json);
524
+ }
525
+ const uploadBodySize = await getBodySize(options.body, options.headers);
526
+ // See https://tools.ietf.org/html/rfc7230#section-3.3.2
527
+ // A user agent SHOULD send a Content-Length in a request message when
528
+ // no Transfer-Encoding is sent and the request method defines a meaning
529
+ // for an enclosed payload body. For example, a Content-Length header
530
+ // field is normally sent in a POST request even when the value is 0
531
+ // (indicating an empty payload body). A user agent SHOULD NOT send a
532
+ // Content-Length header field when the request message does not contain
533
+ // a payload body and the method semantics do not anticipate such a
534
+ // body.
535
+ if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding']) && !cannotHaveBody && !is.undefined(uploadBodySize)) {
536
+ headers['content-length'] = String(uploadBodySize);
537
+ }
538
+ }
539
+ if (options.responseType === 'json' && !('accept' in options.headers)) {
540
+ options.headers.accept = 'application/json';
541
+ }
542
+ this._bodySize = Number(headers['content-length']) || undefined;
543
+ }
544
+ async _onResponseBase(response) {
545
+ // This will be called e.g. when using cache so we need to check if this request has been aborted.
546
+ if (this.isAborted) {
547
+ return;
548
+ }
549
+ const { options } = this;
550
+ const { url } = options;
551
+ this._nativeResponse = response;
552
+ const statusCode = response.statusCode;
553
+ const { method } = options;
554
+ // Skip decompression for responses that must not have bodies per RFC 9110:
555
+ // - HEAD responses (any status code)
556
+ // - 1xx (Informational): 100, 101, 102, 103, etc.
557
+ // - 204 (No Content)
558
+ // - 205 (Reset Content)
559
+ // - 304 (Not Modified)
560
+ const hasNoBody = method === 'HEAD'
561
+ || (statusCode >= 100 && statusCode < 200)
562
+ || statusCode === 204
563
+ || statusCode === 205
564
+ || statusCode === 304;
565
+ if (options.decompress && !hasNoBody) {
566
+ // When strictContentLength is enabled, track compressed bytes by listening to
567
+ // the native response's data events before decompression
568
+ if (options.strictContentLength) {
569
+ this._compressedBytesCount = 0;
570
+ this._nativeResponse.on('data', (chunk) => {
571
+ this._compressedBytesCount += byteLength(chunk);
572
+ });
573
+ }
574
+ response = decompressResponse(response);
575
+ }
576
+ const typedResponse = response;
577
+ typedResponse.statusMessage = typedResponse.statusMessage || http.STATUS_CODES[statusCode]; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- The status message can be empty.
578
+ typedResponse.url = options.url.toString();
579
+ typedResponse.requestUrl = this.requestUrl;
580
+ typedResponse.redirectUrls = this.redirectUrls;
581
+ typedResponse.request = this;
582
+ typedResponse.isFromCache = this._nativeResponse.fromCache ?? false;
583
+ typedResponse.ip = this.ip;
584
+ typedResponse.retryCount = this.retryCount;
585
+ typedResponse.ok = isResponseOk(typedResponse);
586
+ this._isFromCache = typedResponse.isFromCache;
587
+ this._responseSize = Number(response.headers['content-length']) || undefined;
588
+ this.response = typedResponse;
589
+ // Publish response start event
590
+ publishResponseStart({
591
+ requestId: this._requestId,
592
+ url: typedResponse.url,
593
+ statusCode,
594
+ headers: response.headers,
595
+ isFromCache: typedResponse.isFromCache,
596
+ });
597
+ response.once('error', (error) => {
598
+ this._aborted = true;
599
+ // Force clean-up, because some packages don't do this.
600
+ // TODO: Fix decompress-response
601
+ response.destroy();
602
+ this._beforeError(new ReadError(error, this));
603
+ });
604
+ response.once('aborted', () => {
605
+ this._aborted = true;
606
+ // Check if there's a content-length mismatch to provide a more specific error
607
+ if (!this._checkContentLengthMismatch()) {
608
+ this._beforeError(new ReadError({
609
+ name: 'Error',
610
+ message: 'The server aborted pending request',
611
+ code: 'ECONNRESET',
612
+ }, this));
613
+ }
614
+ });
615
+ const rawCookies = response.headers['set-cookie'];
616
+ if (is.object(options.cookieJar) && rawCookies) {
617
+ let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
618
+ if (options.ignoreInvalidCookies) {
619
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
620
+ promises = promises.map(async (promise) => {
621
+ try {
622
+ await promise;
623
+ }
624
+ catch { }
625
+ });
626
+ }
627
+ try {
628
+ await Promise.all(promises);
629
+ }
630
+ catch (error) {
631
+ this._beforeError(error);
632
+ return;
633
+ }
634
+ }
635
+ // The above is running a promise, therefore we need to check if this request has been aborted yet again.
636
+ if (this.isAborted) {
637
+ return;
638
+ }
639
+ if (response.headers.location && redirectCodes.has(statusCode)) {
640
+ // We're being redirected, we don't care about the response.
641
+ // It'd be best to abort the request, but we can't because
642
+ // we would have to sacrifice the TCP connection. We don't want that.
643
+ const shouldFollow = typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect;
644
+ if (shouldFollow) {
645
+ response.resume();
646
+ this._cancelTimeouts();
647
+ this._unproxyEvents();
648
+ if (this.redirectUrls.length >= options.maxRedirects) {
649
+ this._beforeError(new MaxRedirectsError(this));
650
+ return;
651
+ }
652
+ this._request = undefined;
653
+ // Reset download progress for the new request
654
+ this._downloadedSize = 0;
655
+ const updatedOptions = new Options(undefined, undefined, this.options);
656
+ const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
657
+ const canRewrite = statusCode !== 307 && statusCode !== 308;
658
+ const userRequestedGet = updatedOptions.methodRewriting && canRewrite;
659
+ if (serverRequestedGet || userRequestedGet) {
660
+ updatedOptions.method = 'GET';
661
+ updatedOptions.body = undefined;
662
+ updatedOptions.json = undefined;
663
+ updatedOptions.form = undefined;
664
+ delete updatedOptions.headers['content-length'];
665
+ }
666
+ try {
667
+ // We need this in order to support UTF-8
668
+ const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
669
+ const redirectUrl = new URL(redirectBuffer, url);
670
+ if (!isUnixSocketURL(url) && isUnixSocketURL(redirectUrl)) {
671
+ this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
672
+ return;
673
+ }
674
+ // Redirecting to a different site, clear sensitive data.
675
+ // For UNIX sockets, different socket paths are also different origins.
676
+ const isDifferentOrigin = redirectUrl.hostname !== url.hostname
677
+ || redirectUrl.port !== url.port
678
+ || getUnixSocketPath(url) !== getUnixSocketPath(redirectUrl);
679
+ if (isDifferentOrigin) {
680
+ if ('host' in updatedOptions.headers) {
681
+ delete updatedOptions.headers.host;
682
+ }
683
+ if ('cookie' in updatedOptions.headers) {
684
+ delete updatedOptions.headers.cookie;
685
+ }
686
+ if ('authorization' in updatedOptions.headers) {
687
+ delete updatedOptions.headers.authorization;
688
+ }
689
+ if (updatedOptions.username || updatedOptions.password) {
690
+ updatedOptions.username = '';
691
+ updatedOptions.password = '';
692
+ }
693
+ }
694
+ else {
695
+ redirectUrl.username = updatedOptions.username;
696
+ redirectUrl.password = updatedOptions.password;
697
+ }
698
+ this.redirectUrls.push(redirectUrl);
699
+ updatedOptions.url = redirectUrl;
700
+ for (const hook of updatedOptions.hooks.beforeRedirect) {
701
+ // eslint-disable-next-line no-await-in-loop
702
+ await hook(updatedOptions, typedResponse);
703
+ }
704
+ // Publish redirect event
705
+ publishRedirect({
706
+ requestId: this._requestId,
707
+ fromUrl: url.toString(),
708
+ toUrl: redirectUrl.toString(),
709
+ statusCode,
710
+ });
711
+ this.emit('redirect', updatedOptions, typedResponse);
712
+ this.options = updatedOptions;
713
+ await this._makeRequest();
714
+ }
715
+ catch (error) {
716
+ this._beforeError(error);
717
+ return;
718
+ }
719
+ return;
720
+ }
721
+ }
722
+ // `HTTPError`s always have `error.response.body` defined.
723
+ // Therefore, we cannot retry if `options.throwHttpErrors` is false.
724
+ // On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
725
+ // but that wouldn't be possible since the body would be already read in `error.response.body`.
726
+ if (options.isStream && options.throwHttpErrors && !isResponseOk(typedResponse)) {
727
+ this._beforeError(new HTTPError(typedResponse));
728
+ return;
729
+ }
730
+ // Store the expected content-length from the native response for validation.
731
+ // This is the content-length before decompression, which is what actually gets transferred.
732
+ // Skip storing for responses that shouldn't have bodies per RFC 9110.
733
+ // When decompression occurs, only store if strictContentLength is enabled.
734
+ const wasDecompressed = response !== this._nativeResponse;
735
+ if (!hasNoBody && (!wasDecompressed || options.strictContentLength)) {
736
+ const contentLengthHeader = this._nativeResponse.headers['content-length'];
737
+ if (contentLengthHeader !== undefined) {
738
+ const expectedLength = Number(contentLengthHeader);
739
+ if (!Number.isNaN(expectedLength) && expectedLength >= 0) {
740
+ this._expectedContentLength = expectedLength;
741
+ }
742
+ }
743
+ }
744
+ // Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
745
+ response.once('end', () => {
746
+ // Validate content-length if it was provided
747
+ // Per RFC 9112: "If the sender closes the connection before the indicated number
748
+ // of octets are received, the recipient MUST consider the message to be incomplete"
749
+ if (this._checkContentLengthMismatch()) {
750
+ return;
751
+ }
752
+ this._responseSize = this._downloadedSize;
753
+ this.emit('downloadProgress', this.downloadProgress);
754
+ // Publish response end event
755
+ publishResponseEnd({
756
+ requestId: this._requestId,
757
+ url: typedResponse.url,
758
+ statusCode,
759
+ bodySize: this._downloadedSize,
760
+ timings: this.timings,
761
+ });
762
+ this.push(null);
763
+ });
764
+ this.emit('downloadProgress', this.downloadProgress);
765
+ response.on('readable', () => {
766
+ if (this._triggerRead) {
767
+ this._read();
768
+ }
769
+ });
770
+ this.on('resume', () => {
771
+ response.resume();
772
+ });
773
+ this.on('pause', () => {
774
+ response.pause();
775
+ });
776
+ if (this._noPipe) {
777
+ const success = await this._setRawBody();
778
+ if (success) {
779
+ this.emit('response', response);
780
+ }
781
+ return;
782
+ }
783
+ this.emit('response', response);
784
+ for (const destination of this._pipedServerResponses) {
785
+ if (destination.headersSent) {
786
+ continue;
787
+ }
788
+ // Check if decompression actually occurred by comparing stream objects.
789
+ // decompressResponse wraps the response stream when it decompresses,
790
+ // so response !== this._nativeResponse indicates decompression happened.
791
+ const wasDecompressed = response !== this._nativeResponse;
792
+ for (const key in response.headers) {
793
+ if (Object.hasOwn(response.headers, key)) {
794
+ const value = response.headers[key];
795
+ // When decompression occurred, skip content-encoding and content-length
796
+ // as they refer to the compressed data, not the decompressed stream.
797
+ if (wasDecompressed && (key === 'content-encoding' || key === 'content-length')) {
798
+ continue;
799
+ }
800
+ // Skip if value is undefined
801
+ if (value !== undefined) {
802
+ destination.setHeader(key, value);
803
+ }
804
+ }
805
+ }
806
+ destination.statusCode = statusCode;
807
+ }
808
+ }
809
+ async _setRawBody(from = this) {
810
+ if (from.readableEnded) {
811
+ return false;
812
+ }
813
+ try {
814
+ // Errors are emitted via the `error` event
815
+ const fromArray = await from.toArray();
816
+ const rawBody = isBuffer(fromArray.at(0)) ? Buffer.concat(fromArray) : Buffer.from(fromArray.join(''));
817
+ // On retry Request is destroyed with no error, therefore the above will successfully resolve.
818
+ // So in order to check if this was really successfull, we need to check if it has been properly ended.
819
+ if (!this.isAborted) {
820
+ this.response.rawBody = rawBody;
821
+ return true;
822
+ }
823
+ }
824
+ catch { }
825
+ return false;
826
+ }
827
+ async _onResponse(response) {
828
+ try {
829
+ await this._onResponseBase(response);
830
+ }
831
+ catch (error) {
832
+ /* istanbul ignore next: better safe than sorry */
833
+ this._beforeError(error);
834
+ }
835
+ }
836
+ _onRequest(request) {
837
+ const { options } = this;
838
+ const { timeout, url } = options;
839
+ // Publish request start event
840
+ publishRequestStart({
841
+ requestId: this._requestId,
842
+ url: url?.toString() ?? '',
843
+ method: options.method,
844
+ headers: options.headers,
845
+ });
846
+ timer(request);
847
+ this._cancelTimeouts = timedOut(request, timeout, url);
848
+ if (this.options.http2) {
849
+ // Unset stream timeout, as the `timeout` option was used only for connection timeout.
850
+ // We remove all 'timeout' listeners instead of calling setTimeout(0) because:
851
+ // 1. setTimeout(0) causes a memory leak (see https://github.com/sindresorhus/got/issues/690)
852
+ // 2. With HTTP/2 connection reuse, setTimeout(0) accumulates listeners on the socket
853
+ // 3. removeAllListeners('timeout') properly cleans up without the memory leak
854
+ request.removeAllListeners('timeout');
855
+ // For HTTP/2, wait for socket and remove timeout listeners from it
856
+ request.once('socket', (socket) => {
857
+ socket.removeAllListeners('timeout');
858
+ });
859
+ }
860
+ const responseEventName = options.cache ? 'cacheableResponse' : 'response';
861
+ request.once(responseEventName, (response) => {
862
+ void this._onResponse(response);
863
+ });
864
+ request.once('error', (error) => {
865
+ this._aborted = true;
866
+ // Force clean-up, because some packages (e.g. nock) don't do this.
867
+ request.destroy();
868
+ error = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings, this) : new RequestError(error.message, error, this);
869
+ this._beforeError(error);
870
+ });
871
+ this._unproxyEvents = proxyEvents(request, this, proxiedRequestEvents);
872
+ this._request = request;
873
+ this.emit('uploadProgress', this.uploadProgress);
874
+ this._sendBody();
875
+ this.emit('request', request);
876
+ }
877
+ async _asyncWrite(chunk) {
878
+ return new Promise((resolve, reject) => {
879
+ super.write(chunk, error => {
880
+ if (error) {
881
+ reject(error);
882
+ return;
883
+ }
884
+ resolve();
885
+ });
886
+ });
887
+ }
888
+ _sendBody() {
889
+ // Send body
890
+ const { body } = this.options;
891
+ const currentRequest = this.redirectUrls.length === 0 ? this : this._request ?? this;
892
+ if (is.nodeStream(body)) {
893
+ body.pipe(currentRequest);
894
+ }
895
+ else if (is.buffer(body)) {
896
+ // Buffer should be sent directly without conversion
897
+ this._writeRequest(body, undefined, () => { });
898
+ currentRequest.end();
899
+ }
900
+ else if (is.typedArray(body)) {
901
+ // Typed arrays should be treated like buffers, not iterated over
902
+ // Create a Uint8Array view over the data (Node.js streams accept Uint8Array)
903
+ const typedArray = body;
904
+ const uint8View = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
905
+ this._writeRequest(uint8View, undefined, () => { });
906
+ currentRequest.end();
907
+ }
908
+ else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
909
+ (async () => {
910
+ try {
911
+ for await (const chunk of body) {
912
+ await this._asyncWrite(chunk);
913
+ }
914
+ super.end();
915
+ }
916
+ catch (error) {
917
+ this._beforeError(error);
918
+ }
919
+ })();
920
+ }
921
+ else if (is.undefined(body)) {
922
+ // No body to send, end the request
923
+ const cannotHaveBody = methodsWithoutBody.has(this.options.method) && !(this.options.method === 'GET' && this.options.allowGetBody);
924
+ if ((this._noPipe ?? false) || cannotHaveBody || currentRequest !== this) {
925
+ currentRequest.end();
926
+ }
927
+ }
928
+ else {
929
+ this._writeRequest(body, undefined, () => { });
930
+ currentRequest.end();
931
+ }
932
+ }
933
+ _prepareCache(cache) {
934
+ if (cacheableStore.has(cache)) {
935
+ return;
936
+ }
937
+ const cacheableRequest = new CacheableRequest(((requestOptions, handler) => {
938
+ /**
939
+ Wraps the cacheable-request handler to run beforeCache hooks.
940
+ These hooks control caching behavior by:
941
+ - Directly mutating the response object (changes apply to what gets cached)
942
+ - Returning `false` to prevent caching
943
+ - Returning `void`/`undefined` to use default caching behavior
944
+
945
+ Hooks use direct mutation - they can modify response.headers, response.statusCode, etc.
946
+ Mutations take effect immediately and determine what gets cached.
947
+ */
948
+ const wrappedHandler = handler ? (response) => {
949
+ const { beforeCacheHooks, gotRequest } = requestOptions;
950
+ // Early return if no hooks - cache the original response
951
+ if (!beforeCacheHooks || beforeCacheHooks.length === 0) {
952
+ handler(response);
953
+ return;
954
+ }
955
+ try {
956
+ // Call each beforeCache hook with the response
957
+ // Hooks can directly mutate the response - mutations take effect immediately
958
+ for (const hook of beforeCacheHooks) {
959
+ const result = hook(response);
960
+ if (result === false) {
961
+ // Prevent caching by adding no-cache headers
962
+ // Mutate the response directly to add headers
963
+ response.headers['cache-control'] = 'no-cache, no-store, must-revalidate';
964
+ response.headers.pragma = 'no-cache';
965
+ response.headers.expires = '0';
966
+ handler(response);
967
+ // Don't call remaining hooks - we've decided not to cache
968
+ return;
969
+ }
970
+ if (is.promise(result)) {
971
+ // BeforeCache hooks must be synchronous because cacheable-request's handler is synchronous
972
+ 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.');
973
+ }
974
+ if (result !== undefined) {
975
+ // Hooks should return false or undefined only
976
+ // Mutations work directly - no need to return the response
977
+ throw new TypeError('beforeCache hook must return false or undefined. To modify the response, mutate it directly.');
978
+ }
979
+ // Else: void/undefined = continue
980
+ }
981
+ }
982
+ catch (error) {
983
+ // Convert hook errors to RequestError and propagate
984
+ // This is consistent with how other hooks handle errors
985
+ if (gotRequest) {
986
+ gotRequest._beforeError(error instanceof RequestError ? error : new RequestError(error.message, error, gotRequest));
987
+ // Don't call handler when error was propagated successfully
988
+ return;
989
+ }
990
+ // If gotRequest is missing, log the error to aid debugging
991
+ // We still call the handler to prevent the request from hanging
992
+ console.error('Got: beforeCache hook error (request context unavailable):', error);
993
+ // Call handler with response (potentially partially modified)
994
+ handler(response);
995
+ return;
996
+ }
997
+ // All hooks ran successfully
998
+ // Cache the response with any mutations applied
999
+ handler(response);
1000
+ } : handler;
1001
+ const result = requestOptions._request(requestOptions, wrappedHandler);
1002
+ // TODO: remove this when `cacheable-request` supports async request functions.
1003
+ if (is.promise(result)) {
1004
+ // We only need to implement the error handler in order to support HTTP2 caching.
1005
+ // The result will be a promise anyway.
1006
+ // @ts-expect-error ignore
1007
+ result.once = (event, handler) => {
1008
+ if (event === 'error') {
1009
+ (async () => {
1010
+ try {
1011
+ await result;
1012
+ }
1013
+ catch (error) {
1014
+ handler(error);
1015
+ }
1016
+ })();
1017
+ }
1018
+ else if (event === 'abort' || event === 'destroy') {
1019
+ // The empty catch is needed here in case when
1020
+ // it rejects before it's `await`ed in `_makeRequest`.
1021
+ (async () => {
1022
+ try {
1023
+ const request = (await result);
1024
+ request.once(event, handler);
1025
+ }
1026
+ catch { }
1027
+ })();
1028
+ }
1029
+ else {
1030
+ /* istanbul ignore next: safety check */
1031
+ throw new Error(`Unknown HTTP2 promise event: ${event}`);
1032
+ }
1033
+ return result;
1034
+ };
1035
+ }
1036
+ return result;
1037
+ }), cache);
1038
+ cacheableStore.set(cache, cacheableRequest.request());
1039
+ }
1040
+ async _createCacheableRequest(url, options) {
1041
+ return new Promise((resolve, reject) => {
1042
+ // TODO: Remove `utils/url-to-options.ts` when `cacheable-request` is fixed
1043
+ Object.assign(options, urlToOptions(url));
1044
+ let request;
1045
+ // TODO: Fix `cacheable-response`. This is ugly.
1046
+ const cacheRequest = cacheableStore.get(options.cache)(options, async (response) => {
1047
+ response._readableState.autoDestroy = false;
1048
+ if (request) {
1049
+ const fix = () => {
1050
+ // For ResponseLike objects from cache, set complete to true if not already set.
1051
+ // For real HTTP responses, copy from the underlying response.
1052
+ if (response.req) {
1053
+ response.complete = response.req.res.complete;
1054
+ }
1055
+ else if (response.complete === undefined) {
1056
+ // ResponseLike from cache should have complete = true
1057
+ response.complete = true;
1058
+ }
1059
+ };
1060
+ response.prependOnceListener('end', fix);
1061
+ fix();
1062
+ (await request).emit('cacheableResponse', response);
1063
+ }
1064
+ resolve(response);
1065
+ });
1066
+ cacheRequest.once('error', reject);
1067
+ cacheRequest.once('request', async (requestOrPromise) => {
1068
+ request = requestOrPromise;
1069
+ resolve(request);
1070
+ });
1071
+ });
1072
+ }
1073
+ async _makeRequest() {
1074
+ const { options } = this;
1075
+ const { headers, username, password } = options;
1076
+ const cookieJar = options.cookieJar;
1077
+ for (const key in headers) {
1078
+ if (is.undefined(headers[key])) {
1079
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1080
+ delete headers[key];
1081
+ }
1082
+ else if (is.null(headers[key])) {
1083
+ throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
1084
+ }
1085
+ }
1086
+ if (options.decompress && is.undefined(headers['accept-encoding'])) {
1087
+ const encodings = ['gzip', 'deflate'];
1088
+ if (supportsBrotli) {
1089
+ encodings.push('br');
1090
+ }
1091
+ if (supportsZstd) {
1092
+ encodings.push('zstd');
1093
+ }
1094
+ headers['accept-encoding'] = encodings.join(', ');
1095
+ }
1096
+ if (username || password) {
1097
+ const credentials = Buffer.from(`${username}:${password}`).toString('base64');
1098
+ headers.authorization = `Basic ${credentials}`;
1099
+ }
1100
+ // Set cookies
1101
+ if (cookieJar) {
1102
+ const cookieString = await cookieJar.getCookieString(options.url.toString());
1103
+ if (is.nonEmptyString(cookieString)) {
1104
+ headers.cookie = cookieString;
1105
+ }
1106
+ }
1107
+ let request;
1108
+ for (const hook of options.hooks.beforeRequest) {
1109
+ // eslint-disable-next-line no-await-in-loop
1110
+ const result = await hook(options, { retryCount: this.retryCount });
1111
+ if (!is.undefined(result)) {
1112
+ // @ts-expect-error Skip the type mismatch to support abstract responses
1113
+ request = () => result;
1114
+ break;
1115
+ }
1116
+ }
1117
+ request ||= options.getRequestFunction();
1118
+ const url = options.url;
1119
+ this._requestOptions = options.createNativeRequestOptions();
1120
+ if (options.cache) {
1121
+ this._requestOptions._request = request;
1122
+ this._requestOptions.cache = options.cache;
1123
+ this._requestOptions.body = options.body;
1124
+ this._requestOptions.beforeCacheHooks = options.hooks.beforeCache;
1125
+ this._requestOptions.gotRequest = this;
1126
+ try {
1127
+ this._prepareCache(options.cache);
1128
+ }
1129
+ catch (error) {
1130
+ throw new CacheError(error, this);
1131
+ }
1132
+ }
1133
+ // Cache support
1134
+ const function_ = options.cache ? this._createCacheableRequest : request;
1135
+ try {
1136
+ // We can't do `await fn(...)`,
1137
+ // because stream `error` event can be emitted before `Promise.resolve()`.
1138
+ let requestOrResponse = function_(url, this._requestOptions);
1139
+ if (is.promise(requestOrResponse)) {
1140
+ requestOrResponse = await requestOrResponse;
1141
+ }
1142
+ // Fallback
1143
+ if (is.undefined(requestOrResponse)) {
1144
+ requestOrResponse = options.getFallbackRequestFunction()(url, this._requestOptions);
1145
+ if (is.promise(requestOrResponse)) {
1146
+ requestOrResponse = await requestOrResponse;
1147
+ }
1148
+ }
1149
+ if (isClientRequest(requestOrResponse)) {
1150
+ this._onRequest(requestOrResponse);
1151
+ }
1152
+ else if (this.writableEnded) {
1153
+ void this._onResponse(requestOrResponse);
1154
+ }
1155
+ else {
1156
+ this.once('finish', () => {
1157
+ void this._onResponse(requestOrResponse);
1158
+ });
1159
+ this._sendBody();
1160
+ }
1161
+ }
1162
+ catch (error) {
1163
+ if (error instanceof CacheableCacheError) {
1164
+ throw new CacheError(error, this);
1165
+ }
1166
+ throw error;
1167
+ }
1168
+ }
1169
+ async _error(error) {
1170
+ try {
1171
+ if (this.options && error instanceof HTTPError && !this.options.throwHttpErrors) {
1172
+ // This branch can be reached only when using the Promise API
1173
+ // Skip calling the hooks on purpose.
1174
+ // See https://github.com/sindresorhus/got/issues/2103
1175
+ }
1176
+ else if (this.options) {
1177
+ const hooks = this.options.hooks.beforeError;
1178
+ if (hooks.length > 0) {
1179
+ for (const hook of hooks) {
1180
+ // eslint-disable-next-line no-await-in-loop
1181
+ error = await hook(error);
1182
+ // Validate hook return value
1183
+ if (!(error instanceof Error)) {
1184
+ throw new TypeError(`The \`beforeError\` hook must return an Error instance. Received ${is.string(error) ? 'string' : String(typeof error)}.`);
1185
+ }
1186
+ }
1187
+ // Mark this error as processed by hooks so _destroy preserves custom error types.
1188
+ // Only mark non-RequestError errors, since RequestErrors are already preserved
1189
+ // by the instanceof check in _destroy (line 642).
1190
+ if (!(error instanceof RequestError)) {
1191
+ errorsProcessedByHooks.add(error);
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ catch (error_) {
1197
+ error = new RequestError(error_.message, error_, this);
1198
+ }
1199
+ // Publish error event
1200
+ publishError({
1201
+ requestId: this._requestId,
1202
+ url: this.options?.url?.toString() ?? '',
1203
+ error,
1204
+ timings: this.timings,
1205
+ });
1206
+ this.destroy(error);
1207
+ // Manually emit error for Promise API to ensure it receives it.
1208
+ // Node.js streams may not re-emit if an error was already emitted during retry attempts.
1209
+ // Only emit for Promise API (_noPipe = true) to avoid double emissions in stream mode.
1210
+ // Use process.nextTick to defer emission and allow destroy() to complete first.
1211
+ // See https://github.com/sindresorhus/got/issues/1995
1212
+ if (this._noPipe) {
1213
+ process.nextTick(() => {
1214
+ this.emit('error', error);
1215
+ });
1216
+ }
1217
+ }
1218
+ _writeRequest(chunk, encoding, callback) {
1219
+ if (!this._request || this._request.destroyed) {
1220
+ // When there's no request (e.g., using cached response from beforeRequest hook),
1221
+ // we still need to call the callback to allow the stream to finish properly.
1222
+ callback();
1223
+ return;
1224
+ }
1225
+ this._request.write(chunk, encoding, (error) => {
1226
+ // The `!destroyed` check is required to prevent `uploadProgress` being emitted after the stream was destroyed
1227
+ if (!error && !this._request.destroyed) {
1228
+ // For strings, encode them first to measure the actual bytes that will be sent
1229
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk;
1230
+ this._uploadedSize += byteLength(bytes);
1231
+ const progress = this.uploadProgress;
1232
+ if (progress.percent < 1) {
1233
+ this.emit('uploadProgress', progress);
1234
+ }
1235
+ }
1236
+ callback(error);
1237
+ });
1238
+ }
1239
+ /**
1240
+ The remote IP address.
1241
+ */
1242
+ get ip() {
1243
+ return this.socket?.remoteAddress;
1244
+ }
1245
+ /**
1246
+ Indicates whether the request has been aborted or not.
1247
+ */
1248
+ get isAborted() {
1249
+ return this._aborted;
1250
+ }
1251
+ get socket() {
1252
+ return this._request?.socket ?? undefined;
1253
+ }
1254
+ /**
1255
+ Progress event for downloading (receiving a response).
1256
+ */
1257
+ get downloadProgress() {
1258
+ let percent;
1259
+ if (this._responseSize) {
1260
+ percent = this._downloadedSize / this._responseSize;
1261
+ }
1262
+ else if (this._responseSize === this._downloadedSize) {
1263
+ percent = 1;
1264
+ }
1265
+ else {
1266
+ percent = 0;
1267
+ }
1268
+ return {
1269
+ percent,
1270
+ transferred: this._downloadedSize,
1271
+ total: this._responseSize,
1272
+ };
1273
+ }
1274
+ /**
1275
+ Progress event for uploading (sending a request).
1276
+ */
1277
+ get uploadProgress() {
1278
+ let percent;
1279
+ if (this._bodySize) {
1280
+ percent = this._uploadedSize / this._bodySize;
1281
+ }
1282
+ else if (this._bodySize === this._uploadedSize) {
1283
+ percent = 1;
1284
+ }
1285
+ else {
1286
+ percent = 0;
1287
+ }
1288
+ return {
1289
+ percent,
1290
+ transferred: this._uploadedSize,
1291
+ total: this._bodySize,
1292
+ };
1293
+ }
1294
+ /**
1295
+ The object contains the following properties:
1296
+
1297
+ - `start` - Time when the request started.
1298
+ - `socket` - Time when a socket was assigned to the request.
1299
+ - `lookup` - Time when the DNS lookup finished.
1300
+ - `connect` - Time when the socket successfully connected.
1301
+ - `secureConnect` - Time when the socket securely connected.
1302
+ - `upload` - Time when the request finished uploading.
1303
+ - `response` - Time when the request fired `response` event.
1304
+ - `end` - Time when the response fired `end` event.
1305
+ - `error` - Time when the request fired `error` event.
1306
+ - `abort` - Time when the request fired `abort` event.
1307
+ - `phases`
1308
+ - `wait` - `timings.socket - timings.start`
1309
+ - `dns` - `timings.lookup - timings.socket`
1310
+ - `tcp` - `timings.connect - timings.lookup`
1311
+ - `tls` - `timings.secureConnect - timings.connect`
1312
+ - `request` - `timings.upload - (timings.secureConnect || timings.connect)`
1313
+ - `firstByte` - `timings.response - timings.upload`
1314
+ - `download` - `timings.end - timings.response`
1315
+ - `total` - `(timings.end || timings.error || timings.abort) - timings.start`
1316
+
1317
+ If something has not been measured yet, it will be `undefined`.
1318
+
1319
+ __Note__: The time is a `number` representing the milliseconds elapsed since the UNIX epoch.
1320
+ */
1321
+ get timings() {
1322
+ return this._request?.timings;
1323
+ }
1324
+ /**
1325
+ Whether the response was retrieved from the cache.
1326
+ */
1327
+ get isFromCache() {
1328
+ return this._isFromCache;
1329
+ }
1330
+ get reusedSocket() {
1331
+ return this._request?.reusedSocket;
1332
+ }
1333
+ /**
1334
+ Whether the stream is read-only. Returns `true` when `body`, `json`, or `form` options are provided.
1335
+ */
1336
+ get isReadonly() {
1337
+ return !is.undefined(this.options?.body) || !is.undefined(this.options?.json) || !is.undefined(this.options?.form);
1338
+ }
1339
+ }