UNPKG

40.6 kBJavaScriptView Raw
1import process from 'node:process';
2import { Buffer } from 'node:buffer';
3import { Duplex } from 'node:stream';
4import http, { ServerResponse } from 'node:http';
5import timer from '@szmarczak/http-timer';
6import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
7import decompressResponse from 'decompress-response';
8import is from '@sindresorhus/is';
9import { getStreamAsBuffer } from 'get-stream';
10import { FormDataEncoder, isFormData as isFormDataLike } from 'form-data-encoder';
11import getBodySize from './utils/get-body-size.js';
12import isFormData from './utils/is-form-data.js';
13import proxyEvents from './utils/proxy-events.js';
14import timedOut, { TimeoutError as TimedOutTimeoutError } from './timed-out.js';
15import urlToOptions from './utils/url-to-options.js';
16import WeakableMap from './utils/weakable-map.js';
17import calculateRetryDelay from './calculate-retry-delay.js';
18import Options from './options.js';
19import { isResponseOk } from './response.js';
20import isClientRequest from './utils/is-client-request.js';
21import isUnixSocketURL from './utils/is-unix-socket-url.js';
22import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
23const supportsBrotli = is.string(process.versions.brotli);
24const methodsWithoutBody = new Set(['GET', 'HEAD']);
25const cacheableStore = new WeakableMap();
26const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
27const proxiedRequestEvents = [
28 'socket',
29 'connect',
30 'continue',
31 'information',
32 'upgrade',
33];
34const noop = () => { };
35export default class Request extends Duplex {
36 // @ts-expect-error - Ignoring for now.
37 ['constructor'];
38 _noPipe;
39 // @ts-expect-error https://github.com/microsoft/TypeScript/issues/9568
40 options;
41 response;
42 requestUrl;
43 redirectUrls;
44 retryCount;
45 _stopRetry;
46 _downloadedSize;
47 _uploadedSize;
48 _stopReading;
49 _pipedServerResponses;
50 _request;
51 _responseSize;
52 _bodySize;
53 _unproxyEvents;
54 _isFromCache;
55 _cannotHaveBody;
56 _triggerRead;
57 _cancelTimeouts;
58 _removeListeners;
59 _nativeResponse;
60 _flushed;
61 _aborted;
62 // We need this because `this._request` if `undefined` when using cache
63 _requestInitialized;
64 constructor(url, options, defaults) {
65 super({
66 // Don't destroy immediately, as the error may be emitted on unsuccessful retry
67 autoDestroy: false,
68 // It needs to be zero because we're just proxying the data to another stream
69 highWaterMark: 0,
70 });
71 this._downloadedSize = 0;
72 this._uploadedSize = 0;
73 this._stopReading = false;
74 this._pipedServerResponses = new Set();
75 this._cannotHaveBody = false;
76 this._unproxyEvents = noop;
77 this._triggerRead = false;
78 this._cancelTimeouts = noop;
79 this._removeListeners = noop;
80 this._jobs = [];
81 this._flushed = false;
82 this._requestInitialized = false;
83 this._aborted = false;
84 this.redirectUrls = [];
85 this.retryCount = 0;
86 this._stopRetry = noop;
87 this.on('pipe', (source) => {
88 if (source?.headers) {
89 Object.assign(this.options.headers, source.headers);
90 }
91 });
92 this.on('newListener', event => {
93 if (event === 'retry' && this.listenerCount('retry') > 0) {
94 throw new Error('A retry listener has been attached already.');
95 }
96 });
97 try {
98 this.options = new Options(url, options, defaults);
99 if (!this.options.url) {
100 if (this.options.prefixUrl === '') {
101 throw new TypeError('Missing `url` property');
102 }
103 this.options.url = '';
104 }
105 this.requestUrl = this.options.url;
106 }
107 catch (error) {
108 const { options } = error;
109 if (options) {
110 this.options = options;
111 }
112 this.flush = async () => {
113 this.flush = async () => { };
114 this.destroy(error);
115 };
116 return;
117 }
118 // Important! If you replace `body` in a handler with another stream, make sure it's readable first.
119 // The below is run only once.
120 const { body } = this.options;
121 if (is.nodeStream(body)) {
122 body.once('error', error => {
123 if (this._flushed) {
124 this._beforeError(new UploadError(error, this));
125 }
126 else {
127 this.flush = async () => {
128 this.flush = async () => { };
129 this._beforeError(new UploadError(error, this));
130 };
131 }
132 });
133 }
134 if (this.options.signal) {
135 const abort = () => {
136 this.destroy(new AbortError(this));
137 };
138 if (this.options.signal.aborted) {
139 abort();
140 }
141 else {
142 this.options.signal.addEventListener('abort', abort);
143 this._removeListeners = () => {
144 this.options.signal?.removeEventListener('abort', abort);
145 };
146 }
147 }
148 }
149 async flush() {
150 if (this._flushed) {
151 return;
152 }
153 this._flushed = true;
154 try {
155 await this._finalizeBody();
156 if (this.destroyed) {
157 return;
158 }
159 await this._makeRequest();
160 if (this.destroyed) {
161 this._request?.destroy();
162 return;
163 }
164 // Queued writes etc.
165 for (const job of this._jobs) {
166 job();
167 }
168 // Prevent memory leak
169 this._jobs.length = 0;
170 this._requestInitialized = true;
171 }
172 catch (error) {
173 this._beforeError(error);
174 }
175 }
176 _beforeError(error) {
177 if (this._stopReading) {
178 return;
179 }
180 const { response, options } = this;
181 const attemptCount = this.retryCount + (error.name === 'RetryError' ? 0 : 1);
182 this._stopReading = true;
183 if (!(error instanceof RequestError)) {
184 error = new RequestError(error.message, error, this);
185 }
186 const typedError = error;
187 void (async () => {
188 // Node.js parser is really weird.
189 // It emits post-request Parse Errors on the same instance as previous request. WTF.
190 // Therefore, we need to check if it has been destroyed as well.
191 //
192 // Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket,
193 // but makes the response unreadable. So we additionally need to check `response.readable`.
194 if (response?.readable && !response.rawBody && !this._request?.socket?.destroyed) {
195 // @types/node has incorrect typings. `setEncoding` accepts `null` as well.
196 response.setEncoding(this.readableEncoding);
197 const success = await this._setRawBody(response);
198 if (success) {
199 response.body = response.rawBody.toString();
200 }
201 }
202 if (this.listenerCount('retry') !== 0) {
203 let backoff;
204 try {
205 let retryAfter;
206 if (response && 'retry-after' in response.headers) {
207 retryAfter = Number(response.headers['retry-after']);
208 if (Number.isNaN(retryAfter)) {
209 retryAfter = Date.parse(response.headers['retry-after']) - Date.now();
210 if (retryAfter <= 0) {
211 retryAfter = 1;
212 }
213 }
214 else {
215 retryAfter *= 1000;
216 }
217 }
218 const retryOptions = options.retry;
219 backoff = await retryOptions.calculateDelay({
220 attemptCount,
221 retryOptions,
222 error: typedError,
223 retryAfter,
224 computedValue: calculateRetryDelay({
225 attemptCount,
226 retryOptions,
227 error: typedError,
228 retryAfter,
229 computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY,
230 }),
231 });
232 }
233 catch (error_) {
234 void this._error(new RequestError(error_.message, error_, this));
235 return;
236 }
237 if (backoff) {
238 await new Promise(resolve => {
239 const timeout = setTimeout(resolve, backoff);
240 this._stopRetry = () => {
241 clearTimeout(timeout);
242 resolve();
243 };
244 });
245 // Something forced us to abort the retry
246 if (this.destroyed) {
247 return;
248 }
249 try {
250 for (const hook of this.options.hooks.beforeRetry) {
251 // eslint-disable-next-line no-await-in-loop
252 await hook(typedError, this.retryCount + 1);
253 }
254 }
255 catch (error_) {
256 void this._error(new RequestError(error_.message, error, this));
257 return;
258 }
259 // Something forced us to abort the retry
260 if (this.destroyed) {
261 return;
262 }
263 this.destroy();
264 this.emit('retry', this.retryCount + 1, error, (updatedOptions) => {
265 const request = new Request(options.url, updatedOptions, options);
266 request.retryCount = this.retryCount + 1;
267 process.nextTick(() => {
268 void request.flush();
269 });
270 return request;
271 });
272 return;
273 }
274 }
275 void this._error(typedError);
276 })();
277 }
278 _read() {
279 this._triggerRead = true;
280 const { response } = this;
281 if (response && !this._stopReading) {
282 // We cannot put this in the `if` above
283 // because `.read()` also triggers the `end` event
284 if (response.readableLength) {
285 this._triggerRead = false;
286 }
287 let data;
288 while ((data = response.read()) !== null) {
289 this._downloadedSize += data.length; // eslint-disable-line @typescript-eslint/restrict-plus-operands
290 const progress = this.downloadProgress;
291 if (progress.percent < 1) {
292 this.emit('downloadProgress', progress);
293 }
294 this.push(data);
295 }
296 }
297 }
298 _write(chunk, encoding, callback) {
299 const write = () => {
300 this._writeRequest(chunk, encoding, callback);
301 };
302 if (this._requestInitialized) {
303 write();
304 }
305 else {
306 this._jobs.push(write);
307 }
308 }
309 _final(callback) {
310 const endRequest = () => {
311 // We need to check if `this._request` is present,
312 // because it isn't when we use cache.
313 if (!this._request || this._request.destroyed) {
314 callback();
315 return;
316 }
317 this._request.end((error) => {
318 // The request has been destroyed before `_final` finished.
319 // See https://github.com/nodejs/node/issues/39356
320 if (this._request._writableState?.errored) {
321 return;
322 }
323 if (!error) {
324 this._bodySize = this._uploadedSize;
325 this.emit('uploadProgress', this.uploadProgress);
326 this._request.emit('upload-complete');
327 }
328 callback(error);
329 });
330 };
331 if (this._requestInitialized) {
332 endRequest();
333 }
334 else {
335 this._jobs.push(endRequest);
336 }
337 }
338 _destroy(error, callback) {
339 this._stopReading = true;
340 this.flush = async () => { };
341 // Prevent further retries
342 this._stopRetry();
343 this._cancelTimeouts();
344 this._removeListeners();
345 if (this.options) {
346 const { body } = this.options;
347 if (is.nodeStream(body)) {
348 body.destroy();
349 }
350 }
351 if (this._request) {
352 this._request.destroy();
353 }
354 if (error !== null && !is.undefined(error) && !(error instanceof RequestError)) {
355 error = new RequestError(error.message, error, this);
356 }
357 callback(error);
358 }
359 pipe(destination, options) {
360 if (destination instanceof ServerResponse) {
361 this._pipedServerResponses.add(destination);
362 }
363 return super.pipe(destination, options);
364 }
365 unpipe(destination) {
366 if (destination instanceof ServerResponse) {
367 this._pipedServerResponses.delete(destination);
368 }
369 super.unpipe(destination);
370 return this;
371 }
372 async _finalizeBody() {
373 const { options } = this;
374 const { headers } = options;
375 const isForm = !is.undefined(options.form);
376 // eslint-disable-next-line @typescript-eslint/naming-convention
377 const isJSON = !is.undefined(options.json);
378 const isBody = !is.undefined(options.body);
379 const cannotHaveBody = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
380 this._cannotHaveBody = cannotHaveBody;
381 if (isForm || isJSON || isBody) {
382 if (cannotHaveBody) {
383 throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
384 }
385 // Serialize body
386 const noContentType = !is.string(headers['content-type']);
387 if (isBody) {
388 // Body is spec-compliant FormData
389 if (isFormDataLike(options.body)) {
390 const encoder = new FormDataEncoder(options.body);
391 if (noContentType) {
392 headers['content-type'] = encoder.headers['Content-Type'];
393 }
394 if ('Content-Length' in encoder.headers) {
395 headers['content-length'] = encoder.headers['Content-Length'];
396 }
397 options.body = encoder.encode();
398 }
399 // Special case for https://github.com/form-data/form-data
400 if (isFormData(options.body) && noContentType) {
401 headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
402 }
403 }
404 else if (isForm) {
405 if (noContentType) {
406 headers['content-type'] = 'application/x-www-form-urlencoded';
407 }
408 const { form } = options;
409 options.form = undefined;
410 options.body = (new URLSearchParams(form)).toString();
411 }
412 else {
413 if (noContentType) {
414 headers['content-type'] = 'application/json';
415 }
416 const { json } = options;
417 options.json = undefined;
418 options.body = options.stringifyJson(json);
419 }
420 const uploadBodySize = await getBodySize(options.body, options.headers);
421 // See https://tools.ietf.org/html/rfc7230#section-3.3.2
422 // A user agent SHOULD send a Content-Length in a request message when
423 // no Transfer-Encoding is sent and the request method defines a meaning
424 // for an enclosed payload body. For example, a Content-Length header
425 // field is normally sent in a POST request even when the value is 0
426 // (indicating an empty payload body). A user agent SHOULD NOT send a
427 // Content-Length header field when the request message does not contain
428 // a payload body and the method semantics do not anticipate such a
429 // body.
430 if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding']) && !cannotHaveBody && !is.undefined(uploadBodySize)) {
431 headers['content-length'] = String(uploadBodySize);
432 }
433 }
434 if (options.responseType === 'json' && !('accept' in options.headers)) {
435 options.headers.accept = 'application/json';
436 }
437 this._bodySize = Number(headers['content-length']) || undefined;
438 }
439 async _onResponseBase(response) {
440 // This will be called e.g. when using cache so we need to check if this request has been aborted.
441 if (this.isAborted) {
442 return;
443 }
444 const { options } = this;
445 const { url } = options;
446 this._nativeResponse = response;
447 if (options.decompress) {
448 response = decompressResponse(response);
449 }
450 const statusCode = response.statusCode;
451 const typedResponse = response;
452 typedResponse.statusMessage = typedResponse.statusMessage ?? http.STATUS_CODES[statusCode];
453 typedResponse.url = options.url.toString();
454 typedResponse.requestUrl = this.requestUrl;
455 typedResponse.redirectUrls = this.redirectUrls;
456 typedResponse.request = this;
457 typedResponse.isFromCache = this._nativeResponse.fromCache ?? false;
458 typedResponse.ip = this.ip;
459 typedResponse.retryCount = this.retryCount;
460 typedResponse.ok = isResponseOk(typedResponse);
461 this._isFromCache = typedResponse.isFromCache;
462 this._responseSize = Number(response.headers['content-length']) || undefined;
463 this.response = typedResponse;
464 response.once('end', () => {
465 this._responseSize = this._downloadedSize;
466 this.emit('downloadProgress', this.downloadProgress);
467 });
468 response.once('error', (error) => {
469 this._aborted = true;
470 // Force clean-up, because some packages don't do this.
471 // TODO: Fix decompress-response
472 response.destroy();
473 this._beforeError(new ReadError(error, this));
474 });
475 response.once('aborted', () => {
476 this._aborted = true;
477 this._beforeError(new ReadError({
478 name: 'Error',
479 message: 'The server aborted pending request',
480 code: 'ECONNRESET',
481 }, this));
482 });
483 this.emit('downloadProgress', this.downloadProgress);
484 const rawCookies = response.headers['set-cookie'];
485 if (is.object(options.cookieJar) && rawCookies) {
486 let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
487 if (options.ignoreInvalidCookies) {
488 // eslint-disable-next-line @typescript-eslint/no-floating-promises
489 promises = promises.map(async (promise) => {
490 try {
491 await promise;
492 }
493 catch { }
494 });
495 }
496 try {
497 await Promise.all(promises);
498 }
499 catch (error) {
500 this._beforeError(error);
501 return;
502 }
503 }
504 // The above is running a promise, therefore we need to check if this request has been aborted yet again.
505 if (this.isAborted) {
506 return;
507 }
508 if (response.headers.location && redirectCodes.has(statusCode)) {
509 // We're being redirected, we don't care about the response.
510 // It'd be best to abort the request, but we can't because
511 // we would have to sacrifice the TCP connection. We don't want that.
512 const shouldFollow = typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect;
513 if (shouldFollow) {
514 response.resume();
515 this._cancelTimeouts();
516 this._unproxyEvents();
517 if (this.redirectUrls.length >= options.maxRedirects) {
518 this._beforeError(new MaxRedirectsError(this));
519 return;
520 }
521 this._request = undefined;
522 const updatedOptions = new Options(undefined, undefined, this.options);
523 const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
524 const canRewrite = statusCode !== 307 && statusCode !== 308;
525 const userRequestedGet = updatedOptions.methodRewriting && canRewrite;
526 if (serverRequestedGet || userRequestedGet) {
527 updatedOptions.method = 'GET';
528 updatedOptions.body = undefined;
529 updatedOptions.json = undefined;
530 updatedOptions.form = undefined;
531 delete updatedOptions.headers['content-length'];
532 }
533 try {
534 // We need this in order to support UTF-8
535 const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
536 const redirectUrl = new URL(redirectBuffer, url);
537 if (!isUnixSocketURL(url) && isUnixSocketURL(redirectUrl)) {
538 this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
539 return;
540 }
541 // Redirecting to a different site, clear sensitive data.
542 if (redirectUrl.hostname !== url.hostname || redirectUrl.port !== url.port) {
543 if ('host' in updatedOptions.headers) {
544 delete updatedOptions.headers.host;
545 }
546 if ('cookie' in updatedOptions.headers) {
547 delete updatedOptions.headers.cookie;
548 }
549 if ('authorization' in updatedOptions.headers) {
550 delete updatedOptions.headers.authorization;
551 }
552 if (updatedOptions.username || updatedOptions.password) {
553 updatedOptions.username = '';
554 updatedOptions.password = '';
555 }
556 }
557 else {
558 redirectUrl.username = updatedOptions.username;
559 redirectUrl.password = updatedOptions.password;
560 }
561 this.redirectUrls.push(redirectUrl);
562 updatedOptions.prefixUrl = '';
563 updatedOptions.url = redirectUrl;
564 for (const hook of updatedOptions.hooks.beforeRedirect) {
565 // eslint-disable-next-line no-await-in-loop
566 await hook(updatedOptions, typedResponse);
567 }
568 this.emit('redirect', updatedOptions, typedResponse);
569 this.options = updatedOptions;
570 await this._makeRequest();
571 }
572 catch (error) {
573 this._beforeError(error);
574 return;
575 }
576 return;
577 }
578 }
579 // `HTTPError`s always have `error.response.body` defined.
580 // Therefore, we cannot retry if `options.throwHttpErrors` is false.
581 // On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
582 // but that wouldn't be possible since the body would be already read in `error.response.body`.
583 if (options.isStream && options.throwHttpErrors && !isResponseOk(typedResponse)) {
584 this._beforeError(new HTTPError(typedResponse));
585 return;
586 }
587 response.on('readable', () => {
588 if (this._triggerRead) {
589 this._read();
590 }
591 });
592 this.on('resume', () => {
593 response.resume();
594 });
595 this.on('pause', () => {
596 response.pause();
597 });
598 response.once('end', () => {
599 this.push(null);
600 });
601 if (this._noPipe) {
602 const success = await this._setRawBody();
603 if (success) {
604 this.emit('response', response);
605 }
606 return;
607 }
608 this.emit('response', response);
609 for (const destination of this._pipedServerResponses) {
610 if (destination.headersSent) {
611 continue;
612 }
613 // eslint-disable-next-line guard-for-in
614 for (const key in response.headers) {
615 const isAllowed = options.decompress ? key !== 'content-encoding' : true;
616 const value = response.headers[key];
617 if (isAllowed) {
618 destination.setHeader(key, value);
619 }
620 }
621 destination.statusCode = statusCode;
622 }
623 }
624 async _setRawBody(from = this) {
625 if (from.readableEnded) {
626 return false;
627 }
628 try {
629 // Errors are emitted via the `error` event
630 const rawBody = await getStreamAsBuffer(from);
631 // TODO: Switch to this:
632 // let rawBody = await from.toArray();
633 // rawBody = Buffer.concat(rawBody);
634 // On retry Request is destroyed with no error, therefore the above will successfully resolve.
635 // So in order to check if this was really successfull, we need to check if it has been properly ended.
636 if (!this.isAborted) {
637 this.response.rawBody = rawBody;
638 return true;
639 }
640 }
641 catch { }
642 return false;
643 }
644 async _onResponse(response) {
645 try {
646 await this._onResponseBase(response);
647 }
648 catch (error) {
649 /* istanbul ignore next: better safe than sorry */
650 this._beforeError(error);
651 }
652 }
653 _onRequest(request) {
654 const { options } = this;
655 const { timeout, url } = options;
656 timer(request);
657 if (this.options.http2) {
658 // Unset stream timeout, as the `timeout` option was used only for connection timeout.
659 request.setTimeout(0);
660 }
661 this._cancelTimeouts = timedOut(request, timeout, url);
662 const responseEventName = options.cache ? 'cacheableResponse' : 'response';
663 request.once(responseEventName, (response) => {
664 void this._onResponse(response);
665 });
666 request.once('error', (error) => {
667 this._aborted = true;
668 // Force clean-up, because some packages (e.g. nock) don't do this.
669 request.destroy();
670 error = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings, this) : new RequestError(error.message, error, this);
671 this._beforeError(error);
672 });
673 this._unproxyEvents = proxyEvents(request, this, proxiedRequestEvents);
674 this._request = request;
675 this.emit('uploadProgress', this.uploadProgress);
676 this._sendBody();
677 this.emit('request', request);
678 }
679 async _asyncWrite(chunk) {
680 return new Promise((resolve, reject) => {
681 super.write(chunk, error => {
682 if (error) {
683 reject(error);
684 return;
685 }
686 resolve();
687 });
688 });
689 }
690 _sendBody() {
691 // Send body
692 const { body } = this.options;
693 const currentRequest = this.redirectUrls.length === 0 ? this : this._request ?? this;
694 if (is.nodeStream(body)) {
695 body.pipe(currentRequest);
696 }
697 else if (is.generator(body) || is.asyncGenerator(body)) {
698 (async () => {
699 try {
700 for await (const chunk of body) {
701 await this._asyncWrite(chunk);
702 }
703 super.end();
704 }
705 catch (error) {
706 this._beforeError(error);
707 }
708 })();
709 }
710 else if (!is.undefined(body)) {
711 this._writeRequest(body, undefined, () => { });
712 currentRequest.end();
713 }
714 else if (this._cannotHaveBody || this._noPipe) {
715 currentRequest.end();
716 }
717 }
718 _prepareCache(cache) {
719 if (!cacheableStore.has(cache)) {
720 const cacheableRequest = new CacheableRequest(((requestOptions, handler) => {
721 const result = requestOptions._request(requestOptions, handler);
722 // TODO: remove this when `cacheable-request` supports async request functions.
723 if (is.promise(result)) {
724 // We only need to implement the error handler in order to support HTTP2 caching.
725 // The result will be a promise anyway.
726 // @ts-expect-error ignore
727 result.once = (event, handler) => {
728 if (event === 'error') {
729 (async () => {
730 try {
731 await result;
732 }
733 catch (error) {
734 handler(error);
735 }
736 })();
737 }
738 else if (event === 'abort') {
739 // The empty catch is needed here in case when
740 // it rejects before it's `await`ed in `_makeRequest`.
741 (async () => {
742 try {
743 const request = (await result);
744 request.once('abort', handler);
745 }
746 catch { }
747 })();
748 }
749 else {
750 /* istanbul ignore next: safety check */
751 throw new Error(`Unknown HTTP2 promise event: ${event}`);
752 }
753 return result;
754 };
755 }
756 return result;
757 }), cache);
758 cacheableStore.set(cache, cacheableRequest.request());
759 }
760 }
761 async _createCacheableRequest(url, options) {
762 return new Promise((resolve, reject) => {
763 // TODO: Remove `utils/url-to-options.ts` when `cacheable-request` is fixed
764 Object.assign(options, urlToOptions(url));
765 let request;
766 // TODO: Fix `cacheable-response`. This is ugly.
767 const cacheRequest = cacheableStore.get(options.cache)(options, async (response) => {
768 response._readableState.autoDestroy = false;
769 if (request) {
770 const fix = () => {
771 if (response.req) {
772 response.complete = response.req.res.complete;
773 }
774 };
775 response.prependOnceListener('end', fix);
776 fix();
777 (await request).emit('cacheableResponse', response);
778 }
779 resolve(response);
780 });
781 cacheRequest.once('error', reject);
782 cacheRequest.once('request', async (requestOrPromise) => {
783 request = requestOrPromise;
784 resolve(request);
785 });
786 });
787 }
788 async _makeRequest() {
789 const { options } = this;
790 const { headers, username, password } = options;
791 const cookieJar = options.cookieJar;
792 for (const key in headers) {
793 if (is.undefined(headers[key])) {
794 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
795 delete headers[key];
796 }
797 else if (is.null_(headers[key])) {
798 throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
799 }
800 }
801 if (options.decompress && is.undefined(headers['accept-encoding'])) {
802 headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
803 }
804 if (username || password) {
805 const credentials = Buffer.from(`${username}:${password}`).toString('base64');
806 headers.authorization = `Basic ${credentials}`;
807 }
808 // Set cookies
809 if (cookieJar) {
810 const cookieString = await cookieJar.getCookieString(options.url.toString());
811 if (is.nonEmptyString(cookieString)) {
812 headers.cookie = cookieString;
813 }
814 }
815 // Reset `prefixUrl`
816 options.prefixUrl = '';
817 let request;
818 for (const hook of options.hooks.beforeRequest) {
819 // eslint-disable-next-line no-await-in-loop
820 const result = await hook(options);
821 if (!is.undefined(result)) {
822 // @ts-expect-error Skip the type mismatch to support abstract responses
823 request = () => result;
824 break;
825 }
826 }
827 request ||= options.getRequestFunction();
828 const url = options.url;
829 this._requestOptions = options.createNativeRequestOptions();
830 if (options.cache) {
831 this._requestOptions._request = request;
832 this._requestOptions.cache = options.cache;
833 this._requestOptions.body = options.body;
834 this._prepareCache(options.cache);
835 }
836 // Cache support
837 const function_ = options.cache ? this._createCacheableRequest : request;
838 try {
839 // We can't do `await fn(...)`,
840 // because stream `error` event can be emitted before `Promise.resolve()`.
841 let requestOrResponse = function_(url, this._requestOptions);
842 if (is.promise(requestOrResponse)) {
843 requestOrResponse = await requestOrResponse;
844 }
845 // Fallback
846 if (is.undefined(requestOrResponse)) {
847 requestOrResponse = options.getFallbackRequestFunction()(url, this._requestOptions);
848 if (is.promise(requestOrResponse)) {
849 requestOrResponse = await requestOrResponse;
850 }
851 }
852 if (isClientRequest(requestOrResponse)) {
853 this._onRequest(requestOrResponse);
854 }
855 else if (this.writable) {
856 this.once('finish', () => {
857 void this._onResponse(requestOrResponse);
858 });
859 this._sendBody();
860 }
861 else {
862 void this._onResponse(requestOrResponse);
863 }
864 }
865 catch (error) {
866 if (error instanceof CacheableCacheError) {
867 throw new CacheError(error, this);
868 }
869 throw error;
870 }
871 }
872 async _error(error) {
873 try {
874 if (error instanceof HTTPError && !this.options.throwHttpErrors) {
875 // This branch can be reached only when using the Promise API
876 // Skip calling the hooks on purpose.
877 // See https://github.com/sindresorhus/got/issues/2103
878 }
879 else {
880 for (const hook of this.options.hooks.beforeError) {
881 // eslint-disable-next-line no-await-in-loop
882 error = await hook(error);
883 }
884 }
885 }
886 catch (error_) {
887 error = new RequestError(error_.message, error_, this);
888 }
889 this.destroy(error);
890 }
891 _writeRequest(chunk, encoding, callback) {
892 if (!this._request || this._request.destroyed) {
893 // Probably the `ClientRequest` instance will throw
894 return;
895 }
896 this._request.write(chunk, encoding, (error) => {
897 // The `!destroyed` check is required to prevent `uploadProgress` being emitted after the stream was destroyed
898 if (!error && !this._request.destroyed) {
899 this._uploadedSize += Buffer.byteLength(chunk, encoding);
900 const progress = this.uploadProgress;
901 if (progress.percent < 1) {
902 this.emit('uploadProgress', progress);
903 }
904 }
905 callback(error);
906 });
907 }
908 /**
909 The remote IP address.
910 */
911 get ip() {
912 return this.socket?.remoteAddress;
913 }
914 /**
915 Indicates whether the request has been aborted or not.
916 */
917 get isAborted() {
918 return this._aborted;
919 }
920 get socket() {
921 return this._request?.socket ?? undefined;
922 }
923 /**
924 Progress event for downloading (receiving a response).
925 */
926 get downloadProgress() {
927 let percent;
928 if (this._responseSize) {
929 percent = this._downloadedSize / this._responseSize;
930 }
931 else if (this._responseSize === this._downloadedSize) {
932 percent = 1;
933 }
934 else {
935 percent = 0;
936 }
937 return {
938 percent,
939 transferred: this._downloadedSize,
940 total: this._responseSize,
941 };
942 }
943 /**
944 Progress event for uploading (sending a request).
945 */
946 get uploadProgress() {
947 let percent;
948 if (this._bodySize) {
949 percent = this._uploadedSize / this._bodySize;
950 }
951 else if (this._bodySize === this._uploadedSize) {
952 percent = 1;
953 }
954 else {
955 percent = 0;
956 }
957 return {
958 percent,
959 transferred: this._uploadedSize,
960 total: this._bodySize,
961 };
962 }
963 /**
964 The object contains the following properties:
965
966 - `start` - Time when the request started.
967 - `socket` - Time when a socket was assigned to the request.
968 - `lookup` - Time when the DNS lookup finished.
969 - `connect` - Time when the socket successfully connected.
970 - `secureConnect` - Time when the socket securely connected.
971 - `upload` - Time when the request finished uploading.
972 - `response` - Time when the request fired `response` event.
973 - `end` - Time when the response fired `end` event.
974 - `error` - Time when the request fired `error` event.
975 - `abort` - Time when the request fired `abort` event.
976 - `phases`
977 - `wait` - `timings.socket - timings.start`
978 - `dns` - `timings.lookup - timings.socket`
979 - `tcp` - `timings.connect - timings.lookup`
980 - `tls` - `timings.secureConnect - timings.connect`
981 - `request` - `timings.upload - (timings.secureConnect || timings.connect)`
982 - `firstByte` - `timings.response - timings.upload`
983 - `download` - `timings.end - timings.response`
984 - `total` - `(timings.end || timings.error || timings.abort) - timings.start`
985
986 If something has not been measured yet, it will be `undefined`.
987
988 __Note__: The time is a `number` representing the milliseconds elapsed since the UNIX epoch.
989 */
990 get timings() {
991 return this._request?.timings;
992 }
993 /**
994 Whether the response was retrieved from the cache.
995 */
996 get isFromCache() {
997 return this._isFromCache;
998 }
999 get reusedSocket() {
1000 return this._request?.reusedSocket;
1001 }
1002}