UNPKG

thenavisapp

Version:

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

1,314 lines (1,058 loc) 37.2 kB
import process from 'node:process'; import {Buffer} from 'node:buffer'; import {Duplex, type Readable} from 'node:stream'; import http, {ServerResponse, type ClientRequest, type RequestOptions} from 'node:http'; import type {Socket} from 'node:net'; import timer, {type ClientRequestWithTimings, type Timings, type IncomingMessageWithTimings} from '@szmarczak/http-timer'; import CacheableRequest, { CacheError as CacheableCacheError, type StorageAdapter, type CacheableRequestFunction, type CacheableOptions, } from 'cacheable-request'; import decompressResponse from 'decompress-response'; import is from '@sindresorhus/is'; import {getStreamAsBuffer} from 'get-stream'; import {FormDataEncoder, isFormData as isFormDataLike} from 'form-data-encoder'; import type ResponseLike from 'responselike'; import getBodySize from './utils/get-body-size.js'; import isFormData from './utils/is-form-data.js'; import proxyEvents from './utils/proxy-events.js'; import timedOut, {TimeoutError as TimedOutTimeoutError} from './timed-out.js'; import urlToOptions from './utils/url-to-options.js'; import WeakableMap from './utils/weakable-map.js'; import calculateRetryDelay from './calculate-retry-delay.js'; import Options, { type PromiseCookieJar, type NativeRequestOptions, type RetryOptions, type OptionsError, type OptionsInit, } from './options.js'; import {isResponseOk, type PlainResponse, type Response} from './response.js'; import isClientRequest from './utils/is-client-request.js'; import isUnixSocketURL from './utils/is-unix-socket-url.js'; import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js'; type Error = NodeJS.ErrnoException; export type Progress = { percent: number; transferred: number; total?: number; }; const supportsBrotli = is.string(process.versions.brotli); const methodsWithoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']); export type GotEventFunction<T> = /** `request` event to get the request object of the request. __Tip__: You can use `request` event to abort requests. @example ``` import got from 'got'; got.stream('https://github.com') .on('request', request => setTimeout(() => request.destroy(), 50)); ``` */ ((name: 'request', listener: (request: ClientRequest) => void) => T) /** The `response` event to get the response object of the final request. */ & (<R extends Response>(name: 'response', listener: (response: R) => void) => T) /** The `redirect` event to get the response object of a redirect. The second argument is options for the next request to the redirect location. */ & (<R extends Response, N extends Options>(name: 'redirect', listener: (response: R, nextOptions: N) => void) => T) /** Progress events for uploading (sending a request) and downloading (receiving a response). The `progress` argument is an object like: ``` { percent: 0.1, transferred: 1024, total: 10240 } ``` If the `content-length` header is missing, `total` will be `undefined`. @example ``` import got from 'got'; const response = await got('https://sindresorhus.com') .on('downloadProgress', progress => { // Report download progress }) .on('uploadProgress', progress => { // Report upload progress }); console.log(response); ``` */ & ((name: 'uploadProgress' | 'downloadProgress', listener: (progress: Progress) => void) => T) /** To enable retrying on a Got stream, it is required to have a `retry` handler attached. When this event is emitted, you should reset the stream you were writing to and prepare the body again. See `got.options.retry` for more information. */ & ((name: 'retry', listener: (retryCount: number, error: RequestError) => void) => T); export type RequestEvents<T> = { on: GotEventFunction<T>; once: GotEventFunction<T>; off: GotEventFunction<T>; }; const cacheableStore = new WeakableMap<string | StorageAdapter, CacheableRequestFunction>(); const redirectCodes: ReadonlySet<number> = new Set([300, 301, 302, 303, 304, 307, 308]); const proxiedRequestEvents = [ 'socket', 'connect', 'continue', 'information', 'upgrade', ] as const; const noop = (): void => {}; type UrlType = ConstructorParameters<typeof Options>[0]; type OptionsType = ConstructorParameters<typeof Options>[1]; type DefaultsType = ConstructorParameters<typeof Options>[2]; export default class Request extends Duplex implements RequestEvents<Request> { // @ts-expect-error - Ignoring for now. override ['constructor']: typeof Request; _noPipe?: boolean; // @ts-expect-error https://github.com/microsoft/TypeScript/issues/9568 options: Options; response?: PlainResponse; requestUrl?: URL; redirectUrls: URL[]; retryCount: number; declare private _requestOptions: NativeRequestOptions; private _stopRetry: () => void; private _downloadedSize: number; private _uploadedSize: number; private _stopReading: boolean; private readonly _pipedServerResponses: Set<ServerResponse>; private _request?: ClientRequest; private _responseSize?: number; private _bodySize?: number; private _unproxyEvents: () => void; private _isFromCache?: boolean; private _cannotHaveBody: boolean; private _triggerRead: boolean; declare private readonly _jobs: Array<() => void>; private _cancelTimeouts: () => void; private readonly _removeListeners: () => void; private _nativeResponse?: IncomingMessageWithTimings; private _flushed: boolean; private _aborted: boolean; // We need this because `this._request` if `undefined` when using cache private _requestInitialized: boolean; constructor(url: UrlType, options?: OptionsType, defaults?: DefaultsType) { super({ // Don't destroy immediately, as the error may be emitted on unsuccessful retry autoDestroy: false, // It needs to be zero because we're just proxying the data to another stream highWaterMark: 0, }); this._downloadedSize = 0; this._uploadedSize = 0; this._stopReading = false; this._pipedServerResponses = new Set<ServerResponse>(); this._cannotHaveBody = false; this._unproxyEvents = noop; this._triggerRead = false; this._cancelTimeouts = noop; this._removeListeners = noop; this._jobs = []; this._flushed = false; this._requestInitialized = false; this._aborted = false; this.redirectUrls = []; this.retryCount = 0; this._stopRetry = noop; this.on('pipe', (source: any) => { if (source?.headers) { Object.assign(this.options.headers, source.headers); } }); this.on('newListener', event => { if (event === 'retry' && this.listenerCount('retry') > 0) { throw new Error('A retry listener has been attached already.'); } }); try { this.options = new Options(url, options, defaults); if (!this.options.url) { if (this.options.prefixUrl === '') { throw new TypeError('Missing `url` property'); } this.options.url = ''; } this.requestUrl = this.options.url as URL; } catch (error: any) { const {options} = error as OptionsError; if (options) { this.options = options; } this.flush = async () => { this.flush = async () => {}; this.destroy(error); }; return; } // Important! If you replace `body` in a handler with another stream, make sure it's readable first. // The below is run only once. const {body} = this.options; if (is.nodeStream(body)) { body.once('error', error => { if (this._flushed) { this._beforeError(new UploadError(error, this)); } else { this.flush = async () => { this.flush = async () => {}; this._beforeError(new UploadError(error, this)); }; } }); } if (this.options.signal) { const abort = () => { this.destroy(new AbortError(this)); }; if (this.options.signal.aborted) { abort(); } else { this.options.signal.addEventListener('abort', abort); this._removeListeners = () => { this.options.signal?.removeEventListener('abort', abort); }; } } } async flush() { if (this._flushed) { return; } this._flushed = true; try { await this._finalizeBody(); if (this.destroyed) { return; } await this._makeRequest(); if (this.destroyed) { this._request?.destroy(); return; } // Queued writes etc. for (const job of this._jobs) { job(); } // Prevent memory leak this._jobs.length = 0; this._requestInitialized = true; } catch (error: any) { this._beforeError(error); } } _beforeError(error: Error): void { if (this._stopReading) { return; } const {response, options} = this; const attemptCount = this.retryCount + (error.name === 'RetryError' ? 0 : 1); this._stopReading = true; if (!(error instanceof RequestError)) { error = new RequestError(error.message, error, this); } const typedError = error as RequestError; void (async () => { // Node.js parser is really weird. // It emits post-request Parse Errors on the same instance as previous request. WTF. // Therefore, we need to check if it has been destroyed as well. // // Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket, // but makes the response unreadable. So we additionally need to check `response.readable`. if (response?.readable && !response.rawBody && !this._request?.socket?.destroyed) { // @types/node has incorrect typings. `setEncoding` accepts `null` as well. response.setEncoding(this.readableEncoding!); const success = await this._setRawBody(response); if (success) { response.body = response.rawBody!.toString(); } } if (this.listenerCount('retry') !== 0) { let backoff: number; try { let retryAfter; if (response && 'retry-after' in response.headers) { retryAfter = Number(response.headers['retry-after']); if (Number.isNaN(retryAfter)) { retryAfter = Date.parse(response.headers['retry-after']!) - Date.now(); if (retryAfter <= 0) { retryAfter = 1; } } else { retryAfter *= 1000; } } const retryOptions = options.retry as RetryOptions; backoff = await retryOptions.calculateDelay({ attemptCount, retryOptions, error: typedError, retryAfter, computedValue: calculateRetryDelay({ attemptCount, retryOptions, error: typedError, retryAfter, computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY, }), }); } catch (error_: any) { void this._error(new RequestError(error_.message, error_, this)); return; } if (backoff) { await new Promise<void>(resolve => { const timeout = setTimeout(resolve, backoff); this._stopRetry = () => { clearTimeout(timeout); resolve(); }; }); // Something forced us to abort the retry if (this.destroyed) { return; } try { for (const hook of this.options.hooks.beforeRetry) { // eslint-disable-next-line no-await-in-loop await hook(typedError, this.retryCount + 1); } } catch (error_: any) { void this._error(new RequestError(error_.message, error, this)); return; } // Something forced us to abort the retry if (this.destroyed) { return; } this.destroy(); this.emit('retry', this.retryCount + 1, error, (updatedOptions?: OptionsInit) => { const request = new Request(options.url, updatedOptions, options); request.retryCount = this.retryCount + 1; process.nextTick(() => { void request.flush(); }); return request; }); return; } } void this._error(typedError); })(); } override _read(): void { this._triggerRead = true; const {response} = this; if (response && !this._stopReading) { // We cannot put this in the `if` above // because `.read()` also triggers the `end` event if (response.readableLength) { this._triggerRead = false; } let data; while ((data = response.read()) !== null) { this._downloadedSize += data.length; // eslint-disable-line @typescript-eslint/restrict-plus-operands const progress = this.downloadProgress; if (progress.percent < 1) { this.emit('downloadProgress', progress); } this.push(data); } } } override _write(chunk: unknown, encoding: BufferEncoding | undefined, callback: (error?: Error | null) => void): void { // eslint-disable-line @typescript-eslint/ban-types const write = (): void => { this._writeRequest(chunk, encoding, callback); }; if (this._requestInitialized) { write(); } else { this._jobs.push(write); } } override _final(callback: (error?: Error | null) => void): void { // eslint-disable-line @typescript-eslint/ban-types const endRequest = (): void => { // We need to check if `this._request` is present, // because it isn't when we use cache. if (!this._request || this._request.destroyed) { callback(); return; } this._request.end((error?: Error | null) => { // eslint-disable-line @typescript-eslint/ban-types // The request has been destroyed before `_final` finished. // See https://github.com/nodejs/node/issues/39356 if ((this._request as any)._writableState?.errored) { return; } if (!error) { this._bodySize = this._uploadedSize; this.emit('uploadProgress', this.uploadProgress); this._request!.emit('upload-complete'); } callback(error); }); }; if (this._requestInitialized) { endRequest(); } else { this._jobs.push(endRequest); } } override _destroy(error: Error | null, callback: (error: Error | null) => void): void { // eslint-disable-line @typescript-eslint/ban-types this._stopReading = true; this.flush = async () => {}; // Prevent further retries this._stopRetry(); this._cancelTimeouts(); this._removeListeners(); if (this.options) { const {body} = this.options; if (is.nodeStream(body)) { body.destroy(); } } if (this._request) { this._request.destroy(); } if (error !== null && !is.undefined(error) && !(error instanceof RequestError)) { error = new RequestError(error.message, error, this); } callback(error); } override pipe<T extends NodeJS.WritableStream>(destination: T, options?: {end?: boolean}): T { if (destination instanceof ServerResponse) { this._pipedServerResponses.add(destination); } return super.pipe(destination, options); } override unpipe<T extends NodeJS.WritableStream>(destination: T): this { if (destination instanceof ServerResponse) { this._pipedServerResponses.delete(destination); } super.unpipe(destination); return this; } private async _finalizeBody(): Promise<void> { const {options} = this; const {headers} = options; const isForm = !is.undefined(options.form); // eslint-disable-next-line @typescript-eslint/naming-convention const isJSON = !is.undefined(options.json); const isBody = !is.undefined(options.body); const cannotHaveBody = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody); this._cannotHaveBody = cannotHaveBody; if (isForm || isJSON || isBody) { if (cannotHaveBody) { throw new TypeError(`The \`${options.method}\` method cannot be used with a body`); } // Serialize body const noContentType = !is.string(headers['content-type']); if (isBody) { // Body is spec-compliant FormData if (isFormDataLike(options.body)) { const encoder = new FormDataEncoder(options.body); if (noContentType) { headers['content-type'] = encoder.headers['Content-Type']; } if ('Content-Length' in encoder.headers) { headers['content-length'] = encoder.headers['Content-Length']; } options.body = encoder.encode(); } // Special case for https://github.com/form-data/form-data if (isFormData(options.body) && noContentType) { headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`; } } else if (isForm) { if (noContentType) { headers['content-type'] = 'application/x-www-form-urlencoded'; } const {form} = options; options.form = undefined; options.body = (new URLSearchParams(form as Record<string, string>)).toString(); } else { if (noContentType) { headers['content-type'] = 'application/json'; } const {json} = options; options.json = undefined; options.body = options.stringifyJson(json); } const uploadBodySize = await getBodySize(options.body, options.headers); // See https://tools.ietf.org/html/rfc7230#section-3.3.2 // A user agent SHOULD send a Content-Length in a request message when // no Transfer-Encoding is sent and the request method defines a meaning // for an enclosed payload body. For example, a Content-Length header // field is normally sent in a POST request even when the value is 0 // (indicating an empty payload body). A user agent SHOULD NOT send a // Content-Length header field when the request message does not contain // a payload body and the method semantics do not anticipate such a // body. if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding']) && !cannotHaveBody && !is.undefined(uploadBodySize)) { headers['content-length'] = String(uploadBodySize); } } if (options.responseType === 'json' && !('accept' in options.headers)) { options.headers.accept = 'application/json'; } this._bodySize = Number(headers['content-length']) || undefined; } private async _onResponseBase(response: IncomingMessageWithTimings): Promise<void> { // This will be called e.g. when using cache so we need to check if this request has been aborted. if (this.isAborted) { return; } const {options} = this; const {url} = options; this._nativeResponse = response; if (options.decompress) { response = decompressResponse(response); } const statusCode = response.statusCode!; const typedResponse = response as PlainResponse; typedResponse.statusMessage = typedResponse.statusMessage ?? http.STATUS_CODES[statusCode]; typedResponse.url = options.url!.toString(); typedResponse.requestUrl = this.requestUrl!; typedResponse.redirectUrls = this.redirectUrls; typedResponse.request = this; typedResponse.isFromCache = (this._nativeResponse as any).fromCache ?? false; typedResponse.ip = this.ip; typedResponse.retryCount = this.retryCount; typedResponse.ok = isResponseOk(typedResponse); this._isFromCache = typedResponse.isFromCache; this._responseSize = Number(response.headers['content-length']) || undefined; this.response = typedResponse; response.once('end', () => { this._responseSize = this._downloadedSize; this.emit('downloadProgress', this.downloadProgress); }); response.once('error', (error: Error) => { this._aborted = true; // Force clean-up, because some packages don't do this. // TODO: Fix decompress-response response.destroy(); this._beforeError(new ReadError(error, this)); }); response.once('aborted', () => { this._aborted = true; this._beforeError(new ReadError({ name: 'Error', message: 'The server aborted pending request', code: 'ECONNRESET', }, this)); }); this.emit('downloadProgress', this.downloadProgress); const rawCookies = response.headers['set-cookie']; if (is.object(options.cookieJar) && rawCookies) { let promises: Array<Promise<unknown>> = rawCookies.map(async (rawCookie: string) => (options.cookieJar as PromiseCookieJar).setCookie(rawCookie, url!.toString())); if (options.ignoreInvalidCookies) { // eslint-disable-next-line @typescript-eslint/no-floating-promises promises = promises.map(async promise => { try { await promise; } catch {} }); } try { await Promise.all(promises); } catch (error: any) { this._beforeError(error); return; } } // The above is running a promise, therefore we need to check if this request has been aborted yet again. if (this.isAborted) { return; } if (response.headers.location && redirectCodes.has(statusCode)) { // We're being redirected, we don't care about the response. // It'd be best to abort the request, but we can't because // we would have to sacrifice the TCP connection. We don't want that. const shouldFollow = typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect; if (shouldFollow) { response.resume(); this._cancelTimeouts(); this._unproxyEvents(); if (this.redirectUrls.length >= options.maxRedirects) { this._beforeError(new MaxRedirectsError(this)); return; } this._request = undefined; const updatedOptions = new Options(undefined, undefined, this.options); const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD'; const canRewrite = statusCode !== 307 && statusCode !== 308; const userRequestedGet = updatedOptions.methodRewriting && canRewrite; if (serverRequestedGet || userRequestedGet) { updatedOptions.method = 'GET'; updatedOptions.body = undefined; updatedOptions.json = undefined; updatedOptions.form = undefined; delete updatedOptions.headers['content-length']; } try { // We need this in order to support UTF-8 const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString(); const redirectUrl = new URL(redirectBuffer, url); if (!isUnixSocketURL(url as URL) && isUnixSocketURL(redirectUrl)) { this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this)); return; } // Redirecting to a different site, clear sensitive data. if (redirectUrl.hostname !== (url as URL).hostname || redirectUrl.port !== (url as URL).port) { if ('host' in updatedOptions.headers) { delete updatedOptions.headers.host; } if ('cookie' in updatedOptions.headers) { delete updatedOptions.headers.cookie; } if ('authorization' in updatedOptions.headers) { delete updatedOptions.headers.authorization; } if (updatedOptions.username || updatedOptions.password) { updatedOptions.username = ''; updatedOptions.password = ''; } } else { redirectUrl.username = updatedOptions.username; redirectUrl.password = updatedOptions.password; } this.redirectUrls.push(redirectUrl); updatedOptions.prefixUrl = ''; updatedOptions.url = redirectUrl; for (const hook of updatedOptions.hooks.beforeRedirect) { // eslint-disable-next-line no-await-in-loop await hook(updatedOptions, typedResponse); } this.emit('redirect', updatedOptions, typedResponse); this.options = updatedOptions; await this._makeRequest(); } catch (error: any) { this._beforeError(error); return; } return; } } // `HTTPError`s always have `error.response.body` defined. // Therefore, we cannot retry if `options.throwHttpErrors` is false. // On the last retry, if `options.throwHttpErrors` is false, we would need to return the body, // but that wouldn't be possible since the body would be already read in `error.response.body`. if (options.isStream && options.throwHttpErrors && !isResponseOk(typedResponse)) { this._beforeError(new HTTPError(typedResponse)); return; } response.on('readable', () => { if (this._triggerRead) { this._read(); } }); this.on('resume', () => { response.resume(); }); this.on('pause', () => { response.pause(); }); response.once('end', () => { this.push(null); }); if (this._noPipe) { const success = await this._setRawBody(); if (success) { this.emit('response', response); } return; } this.emit('response', response); for (const destination of this._pipedServerResponses) { if (destination.headersSent) { continue; } // eslint-disable-next-line guard-for-in for (const key in response.headers) { const isAllowed = options.decompress ? key !== 'content-encoding' : true; const value = response.headers[key]; if (isAllowed) { destination.setHeader(key, value!); } } destination.statusCode = statusCode; } } private async _setRawBody(from: Readable = this): Promise<boolean> { if (from.readableEnded) { return false; } try { // Errors are emitted via the `error` event const rawBody = await getStreamAsBuffer(from); // TODO: Switch to this: // let rawBody = await from.toArray(); // rawBody = Buffer.concat(rawBody); // On retry Request is destroyed with no error, therefore the above will successfully resolve. // So in order to check if this was really successfull, we need to check if it has been properly ended. if (!this.isAborted) { this.response!.rawBody = rawBody; return true; } } catch {} return false; } private async _onResponse(response: IncomingMessageWithTimings): Promise<void> { try { await this._onResponseBase(response); } catch (error: any) { /* istanbul ignore next: better safe than sorry */ this._beforeError(error); } } private _onRequest(request: ClientRequest): void { const {options} = this; const {timeout, url} = options; timer(request); if (this.options.http2) { // Unset stream timeout, as the `timeout` option was used only for connection timeout. request.setTimeout(0); } this._cancelTimeouts = timedOut(request, timeout, url as URL); const responseEventName = options.cache ? 'cacheableResponse' : 'response'; request.once(responseEventName, (response: IncomingMessageWithTimings) => { void this._onResponse(response); }); request.once('error', (error: Error) => { this._aborted = true; // Force clean-up, because some packages (e.g. nock) don't do this. request.destroy(); error = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings!, this) : new RequestError(error.message, error, this); this._beforeError(error); }); this._unproxyEvents = proxyEvents(request, this, proxiedRequestEvents); this._request = request; this.emit('uploadProgress', this.uploadProgress); this._sendBody(); this.emit('request', request); } private async _asyncWrite(chunk: any): Promise<void> { return new Promise((resolve, reject) => { super.write(chunk, error => { if (error) { reject(error); return; } resolve(); }); }); } private _sendBody() { // Send body const {body} = this.options; const currentRequest = this.redirectUrls.length === 0 ? this : this._request ?? this; if (is.nodeStream(body)) { body.pipe(currentRequest); } else if (is.generator(body) || is.asyncGenerator(body)) { (async () => { try { for await (const chunk of body) { await this._asyncWrite(chunk); } super.end(); } catch (error: any) { this._beforeError(error); } })(); } else if (!is.undefined(body)) { this._writeRequest(body, undefined, () => {}); currentRequest.end(); } else if (this._cannotHaveBody || this._noPipe) { currentRequest.end(); } } private _prepareCache(cache: string | StorageAdapter) { if (!cacheableStore.has(cache)) { const cacheableRequest = new CacheableRequest( ((requestOptions: RequestOptions, handler?: (response: IncomingMessageWithTimings) => void): ClientRequest => { const result = (requestOptions as any)._request(requestOptions, handler); // TODO: remove this when `cacheable-request` supports async request functions. if (is.promise(result)) { // We only need to implement the error handler in order to support HTTP2 caching. // The result will be a promise anyway. // @ts-expect-error ignore result.once = (event: string, handler: (reason: unknown) => void) => { if (event === 'error') { (async () => { try { await result; } catch (error) { handler(error); } })(); } else if (event === 'abort') { // The empty catch is needed here in case when // it rejects before it's `await`ed in `_makeRequest`. (async () => { try { const request = (await result) as ClientRequest; request.once('abort', handler); } catch {} })(); } else { /* istanbul ignore next: safety check */ throw new Error(`Unknown HTTP2 promise event: ${event}`); } return result; }; } return result; }) as typeof http.request, cache as StorageAdapter, ); cacheableStore.set(cache, cacheableRequest.request()); } } private async _createCacheableRequest(url: URL, options: RequestOptions): Promise<ClientRequest | ResponseLike> { return new Promise<ClientRequest | ResponseLike>((resolve, reject) => { // TODO: Remove `utils/url-to-options.ts` when `cacheable-request` is fixed Object.assign(options, urlToOptions(url)); let request: ClientRequest | Promise<ClientRequest>; // TODO: Fix `cacheable-response`. This is ugly. const cacheRequest = cacheableStore.get((options as any).cache)!(options as CacheableOptions, async (response: any) => { response._readableState.autoDestroy = false; if (request) { const fix = () => { if (response.req) { response.complete = response.req.res.complete; } }; response.prependOnceListener('end', fix); fix(); (await request).emit('cacheableResponse', response); } resolve(response); }); cacheRequest.once('error', reject); cacheRequest.once('request', async (requestOrPromise: ClientRequest | Promise<ClientRequest>) => { request = requestOrPromise; resolve(request); }); }); } private async _makeRequest(): Promise<void> { const {options} = this; const {headers, username, password} = options; const cookieJar = options.cookieJar as PromiseCookieJar | undefined; for (const key in headers) { if (is.undefined(headers[key])) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete headers[key]; } else if (is.null_(headers[key])) { throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`); } } if (options.decompress && is.undefined(headers['accept-encoding'])) { headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate'; } if (username || password) { const credentials = Buffer.from(`${username}:${password}`).toString('base64'); headers.authorization = `Basic ${credentials}`; } // Set cookies if (cookieJar) { const cookieString: string = await cookieJar.getCookieString(options.url!.toString()); if (is.nonEmptyString(cookieString)) { headers.cookie = cookieString; } } // Reset `prefixUrl` options.prefixUrl = ''; let request: ReturnType<Options['getRequestFunction']> | undefined; for (const hook of options.hooks.beforeRequest) { // eslint-disable-next-line no-await-in-loop const result = await hook(options); if (!is.undefined(result)) { // @ts-expect-error Skip the type mismatch to support abstract responses request = () => result; break; } } if (!request) { request = options.getRequestFunction(); } const url = options.url as URL; this._requestOptions = options.createNativeRequestOptions() as NativeRequestOptions; if (options.cache) { (this._requestOptions as any)._request = request; (this._requestOptions as any).cache = options.cache; (this._requestOptions as any).body = options.body; this._prepareCache(options.cache as StorageAdapter); } // Cache support const fn = options.cache ? this._createCacheableRequest : request; try { // We can't do `await fn(...)`, // because stream `error` event can be emitted before `Promise.resolve()`. let requestOrResponse = fn!(url, this._requestOptions); if (is.promise(requestOrResponse)) { requestOrResponse = await requestOrResponse; } // Fallback if (is.undefined(requestOrResponse)) { requestOrResponse = options.getFallbackRequestFunction()!(url, this._requestOptions); if (is.promise(requestOrResponse)) { requestOrResponse = await requestOrResponse; } } if (isClientRequest(requestOrResponse!)) { this._onRequest(requestOrResponse); } else if (this.writable) { this.once('finish', () => { void this._onResponse(requestOrResponse as IncomingMessageWithTimings); }); this._sendBody(); } else { void this._onResponse(requestOrResponse as IncomingMessageWithTimings); } } catch (error) { if (error instanceof CacheableCacheError) { throw new CacheError(error, this); } throw error; } } private async _error(error: RequestError): Promise<void> { try { if (error instanceof HTTPError && !this.options.throwHttpErrors) { // This branch can be reached only when using the Promise API // Skip calling the hooks on purpose. // See https://github.com/sindresorhus/got/issues/2103 } else { for (const hook of this.options.hooks.beforeError) { // eslint-disable-next-line no-await-in-loop error = await hook(error); } } } catch (error_: any) { error = new RequestError(error_.message, error_, this); } this.destroy(error); } private _writeRequest(chunk: any, encoding: BufferEncoding | undefined, callback: (error?: Error | null) => void): void { // eslint-disable-line @typescript-eslint/ban-types if (!this._request || this._request.destroyed) { // Probably the `ClientRequest` instance will throw return; } this._request.write(chunk, encoding!, (error?: Error | null) => { // eslint-disable-line @typescript-eslint/ban-types // The `!destroyed` check is required to prevent `uploadProgress` being emitted after the stream was destroyed if (!error && !this._request!.destroyed) { this._uploadedSize += Buffer.byteLength(chunk, encoding); const progress = this.uploadProgress; if (progress.percent < 1) { this.emit('uploadProgress', progress); } } callback(error); }); } /** The remote IP address. */ get ip(): string | undefined { return this.socket?.remoteAddress; } /** Indicates whether the request has been aborted or not. */ get isAborted(): boolean { return this._aborted; } get socket(): Socket | undefined { return this._request?.socket ?? undefined; } /** Progress event for downloading (receiving a response). */ get downloadProgress(): Progress { let percent; if (this._responseSize) { percent = this._downloadedSize / this._responseSize; } else if (this._responseSize === this._downloadedSize) { percent = 1; } else { percent = 0; } return { percent, transferred: this._downloadedSize, total: this._responseSize, }; } /** Progress event for uploading (sending a request). */ get uploadProgress(): Progress { let percent; if (this._bodySize) { percent = this._uploadedSize / this._bodySize; } else if (this._bodySize === this._uploadedSize) { percent = 1; } else { percent = 0; } return { percent, transferred: this._uploadedSize, total: this._bodySize, }; } /** The object contains the following properties: - `start` - Time when the request started. - `socket` - Time when a socket was assigned to the request. - `lookup` - Time when the DNS lookup finished. - `connect` - Time when the socket successfully connected. - `secureConnect` - Time when the socket securely connected. - `upload` - Time when the request finished uploading. - `response` - Time when the request fired `response` event. - `end` - Time when the response fired `end` event. - `error` - Time when the request fired `error` event. - `abort` - Time when the request fired `abort` event. - `phases` - `wait` - `timings.socket - timings.start` - `dns` - `timings.lookup - timings.socket` - `tcp` - `timings.connect - timings.lookup` - `tls` - `timings.secureConnect - timings.connect` - `request` - `timings.upload - (timings.secureConnect || timings.connect)` - `firstByte` - `timings.response - timings.upload` - `download` - `timings.end - timings.response` - `total` - `(timings.end || timings.error || timings.abort) - timings.start` If something has not been measured yet, it will be `undefined`. __Note__: The time is a `number` representing the milliseconds elapsed since the UNIX epoch. */ get timings(): Timings | undefined { return (this._request as ClientRequestWithTimings)?.timings; } /** Whether the response was retrieved from the cache. */ get isFromCache(): boolean | undefined { return this._isFromCache; } get reusedSocket(): boolean | undefined { return this._request?.reusedSocket; } }