"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnsupportedProtocolError = exports.ReadError = exports.TimeoutError = exports.UploadError = exports.CacheError = exports.HTTPError = exports.MaxRedirectsError = exports.RequestError = exports.setNonEnumerableProperties = exports.knownHookEvents = exports.withoutBody = exports.kIsNormalizedAlready = void 0;
const util_1 = require("util");
const stream_1 = require("stream");
const fs_1 = require("fs");
const url_1 = require("url");
const http = require("http");
const http_1 = require("http");
const https = require("https");
const http_timer_1 = require("@szmarczak/http-timer");
const cacheable_lookup_1 = require("cacheable-lookup");
const CacheableRequest = require("cacheable-request");
const decompressResponse = require("decompress-response");
const http2wrapper = require("http2-wrapper");
const lowercaseKeys = require("lowercase-keys");
const is_1 = require("@sindresorhus/is");
const get_body_size_1 = require("./utils/get-body-size");
const is_form_data_1 = require("./utils/is-form-data");
const proxy_events_1 = require("./utils/proxy-events");
const timed_out_1 = require("./utils/timed-out");
const url_to_options_1 = require("./utils/url-to-options");
const options_to_url_1 = require("./utils/options-to-url");
const weakable_map_1 = require("./utils/weakable-map");
const get_buffer_1 = require("./utils/get-buffer");
const dns_ip_version_1 = require("./utils/dns-ip-version");
const is_response_ok_1 = require("./utils/is-response-ok");
const deprecation_warning_1 = require("../utils/deprecation-warning");
const normalize_arguments_1 = require("../as-promise/normalize-arguments");
const calculate_retry_delay_1 = require("./calculate-retry-delay");
let globalDnsCache;
const kRequest = Symbol('request');
const kResponse = Symbol('response');
const kResponseSize = Symbol('responseSize');
const kDownloadedSize = Symbol('downloadedSize');
const kBodySize = Symbol('bodySize');
const kUploadedSize = Symbol('uploadedSize');
const kServerResponsesPiped = Symbol('serverResponsesPiped');
const kUnproxyEvents = Symbol('unproxyEvents');
const kIsFromCache = Symbol('isFromCache');
const kCancelTimeouts = Symbol('cancelTimeouts');
const kStartedReading = Symbol('startedReading');
const kStopReading = Symbol('stopReading');
const kTriggerRead = Symbol('triggerRead');
const kBody = Symbol('body');
const kJobs = Symbol('jobs');
const kOriginalResponse = Symbol('originalResponse');
const kRetryTimeout = Symbol('retryTimeout');
exports.kIsNormalizedAlready = Symbol('isNormalizedAlready');
const supportsBrotli = is_1.default.string(process.versions.brotli);
exports.withoutBody = new Set(['GET', 'HEAD']);
exports.knownHookEvents = [
'init',
'beforeRequest',
'beforeRedirect',
'beforeError',
'beforeRetry',
// Promise-Only
'afterResponse'
];
function validateSearchParameters(searchParameters) {
// eslint-disable-next-line guard-for-in
for (const key in searchParameters) {
const value = searchParameters[key];
if (!is_1.default.string(value) && !is_1.default.number(value) && !is_1.default.boolean(value) && !is_1.default.null_(value) && !is_1.default.undefined(value)) {
throw new TypeError(`The \`searchParams\` value '${String(value)}' must be a string, number, boolean or null`);
}
}
}
function isClientRequest(clientRequest) {
return is_1.default.object(clientRequest) && !('statusCode' in clientRequest);
}
const cacheableStore = new weakable_map_1.default();
const waitForOpenFile = async (file) => new Promise((resolve, reject) => {
const onError = (error) => {
reject(error);
};
// Node.js 12 has incomplete types
if (!file.pending) {
resolve();
}
file.once('error', onError);
file.once('ready', () => {
file.off('error', onError);
resolve();
});
});
const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]);
const nonEnumerableProperties = [
'context',
'body',
'json',
'form'
];
const setNonEnumerableProperties = (sources, to) => {
// Non enumerable properties shall not be merged
const properties = {};
for (const source of sources) {
if (!source) {
continue;
}
for (const name of nonEnumerableProperties) {
if (!(name in source)) {
continue;
}
properties[name] = {
writable: true,
configurable: true,
enumerable: false,
// @ts-expect-error TS doesn't see the check above
value: source[name]
};
}
}
Object.defineProperties(to, properties);
};
exports.setNonEnumerableProperties = setNonEnumerableProperties;
/**
An error to be thrown when a request fails.
Contains a `code` property with error class code, like `ECONNREFUSED`.
*/
class RequestError extends Error {
constructor(message, error, self) {
var _a, _b;
super(message);
Error.captureStackTrace(this, this.constructor);
this.name = 'RequestError';
this.code = (_a = error.code) !== null && _a !== void 0 ? _a : 'ERR_GOT_REQUEST_ERROR';
if (self instanceof Request) {
Object.defineProperty(this, 'request', {
enumerable: false,
value: self
});
Object.defineProperty(this, 'response', {
enumerable: false,
value: self[kResponse]
});
Object.defineProperty(this, 'options', {
// This fails because of TS 3.7.2 useDefineForClassFields
// Ref: https://github.com/microsoft/TypeScript/issues/34972
enumerable: false,
value: self.options
});
}
else {
Object.defineProperty(this, 'options', {
// This fails because of TS 3.7.2 useDefineForClassFields
// Ref: https://github.com/microsoft/TypeScript/issues/34972
enumerable: false,
value: self
});
}
this.timings = (_b = this.request) === null || _b === void 0 ? void 0 : _b.timings;
// Recover the original stacktrace
if (is_1.default.string(error.stack) && is_1.default.string(this.stack)) {
const indexOfMessage = this.stack.indexOf(this.message) + this.message.length;
const thisStackTrace = this.stack.slice(indexOfMessage).split('\n').reverse();
const errorStackTrace = error.stack.slice(error.stack.indexOf(error.message) + error.message.length).split('\n').reverse();
// Remove duplicated traces
while (errorStackTrace.length !== 0 && errorStackTrace[0] === thisStackTrace[0]) {
thisStackTrace.shift();
}
this.stack = `${this.stack.slice(0, indexOfMessage)}${thisStackTrace.reverse().join('\n')}${errorStackTrace.reverse().join('\n')}`;
}
}
}
exports.RequestError = RequestError;
/**
An error to be thrown when the server redirects you more than ten times.
Includes a `response` property.
*/
class MaxRedirectsError extends RequestError {
constructor(request) {
super(`Redirected ${request.options.maxRedirects} times. Aborting.`, {}, request);
this.name = 'MaxRedirectsError';
this.code = 'ERR_TOO_MANY_REDIRECTS';
}
}
exports.MaxRedirectsError = MaxRedirectsError;
/**
An error to be thrown when the server response code is not 2xx nor 3xx if `options.followRedirect` is `true`, but always except for 304.
Includes a `response` property.
*/
class HTTPError extends RequestError {
constructor(response) {
super(`Response code ${response.statusCode} (${response.statusMessage})`, {}, response.request);
this.name = 'HTTPError';
this.code = 'ERR_NON_2XX_3XX_RESPONSE';
}
}
exports.HTTPError = HTTPError;
/**
An error to be thrown when a cache method fails.
For example, if the database goes down or there's a filesystem error.
*/
class CacheError extends RequestError {
constructor(error, request) {
super(error.message, error, request);
this.name = 'CacheError';
this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_CACHE_ACCESS' : this.code;
}
}
exports.CacheError = CacheError;
/**
An error to be thrown when the request body is a stream and an error occurs while reading from that stream.
*/
class UploadError extends RequestError {
constructor(error, request) {
super(error.message, error, request);
this.name = 'UploadError';
this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_UPLOAD' : this.code;
}
}
exports.UploadError = UploadError;
/**
An error to be thrown when the request is aborted due to a timeout.
Includes an `event` and `timings` property.
*/
class TimeoutError extends RequestError {
constructor(error, timings, request) {
super(error.message, error, request);
this.name = 'TimeoutError';
this.event = error.event;
this.timings = timings;
}
}
exports.TimeoutError = TimeoutError;
/**
An error to be thrown when reading from response stream fails.
*/
class ReadError extends RequestError {
constructor(error, request) {
super(error.message, error, request);
this.name = 'ReadError';
this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_READING_RESPONSE_STREAM' : this.code;
}
}
exports.ReadError = ReadError;
/**
An error to be thrown when given an unsupported protocol.
*/
class UnsupportedProtocolError extends RequestError {
constructor(options) {
super(`Unsupported protocol "${options.url.protocol}"`, {}, options);
this.name = 'UnsupportedProtocolError';
this.code = 'ERR_UNSUPPORTED_PROTOCOL';
}
}
exports.UnsupportedProtocolError = UnsupportedProtocolError;
const proxiedRequestEvents = [
'socket',
'connect',
'continue',
'information',
'upgrade',
'timeout'
];
class Request extends stream_1.Duplex {
constructor(url, options = {}, defaults) {
super({
// This must be false, to enable throwing after destroy
// It is used for retry logic in Promise API
autoDestroy: false,
// It needs to be zero because we're just proxying the data to another stream
highWaterMark: 0
});
this[kDownloadedSize] = 0;
this[kUploadedSize] = 0;
this.requestInitialized = false;
this[kServerResponsesPiped] = new Set();
this.redirects = [];
this[kStopReading] = false;
this[kTriggerRead] = false;
this[kJobs] = [];
this.retryCount = 0;
// TODO: Remove this when targeting Node.js >= 12
this._progressCallbacks = [];
const unlockWrite = () => this._unlockWrite();
const lockWrite = () => this._lockWrite();
this.on('pipe', (source) => {
source.prependListener('data', unlockWrite);
source.on('data', lockWrite);
source.prependListener('end', unlockWrite);
source.on('end', lockWrite);
});
this.on('unpipe', (source) => {
source.off('data', unlockWrite);
source.off('data', lockWrite);
source.off('end', unlockWrite);
source.off('end', lockWrite);
});
this.on('pipe', source => {
if (source instanceof http_1.IncomingMessage) {
this.options.headers = {
...source.headers,
...this.options.headers
};
}
});
const { json, body, form } = options;
if (json || body || form) {
this._lockWrite();
}
if (exports.kIsNormalizedAlready in options) {
this.options = options;
}
else {
try {
// @ts-expect-error Common TypeScript bug saying that `this.constructor` is not accessible
this.options = this.constructor.normalizeArguments(url, options, defaults);
}
catch (error) {
// TODO: Move this to `_destroy()`
if (is_1.default.nodeStream(options.body)) {
options.body.destroy();
}
// @ts-ignore
this.destroy(error);
return;
}
}
(async () => {
var _a;
try {
if (this.options.body instanceof fs_1.ReadStream) {
await waitForOpenFile(this.options.body);
}
const { url: normalizedURL } = this.options;
if (!normalizedURL) {
throw new TypeError('Missing `url` property');
}
this.requestUrl = normalizedURL.toString();
decodeURI(this.requestUrl);
await this._finalizeBody();
await this._makeRequest();
if (this.destroyed) {
(_a = this[kRequest]) === null || _a === void 0 ? void 0 : _a.destroy();
return;
}
// Queued writes etc.
// @ts-ignore
for (const job of this[kJobs]) {
job();
}
// Prevent memory leak
// @ts-ignore
this[kJobs].length = 0;
this.requestInitialized = true;
}
catch (error) {
if (error instanceof RequestError) {
this._beforeError(error);
return;
}
// This is a workaround for https://github.com/nodejs/node/issues/33335
if (!this.destroyed) {
// @ts-ignore
this.destroy(error);
}
}
})();
}
static normalizeArguments(url, options, defaults) {
var _a, _b, _c, _d, _e;
const rawOptions = options;
if (is_1.default.object(url) && !is_1.default.urlInstance(url)) {
options = { ...defaults, ...url, ...options };
}
else {
if (url && options && options.url !== undefined) {
throw new TypeError('The `url` option is mutually exclusive with the `input` argument');
}
options = { ...defaults, ...options };
if (url !== undefined) {
options.url = url;
}
if (is_1.default.urlInstance(options.url)) {
options.url = new url_1.URL(options.url.toString());
}
}
// TODO: Deprecate URL options in Got 12.
// Support extend-specific options
if (options.cache === false) {
options.cache = undefined;
}
if (options.dnsCache === false) {
options.dnsCache = undefined;
}
// Nice type assertions
is_1.assert.any([is_1.default.string, is_1.default.undefined], options.method);
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.headers);
is_1.assert.any([is_1.default.string, is_1.default.urlInstance, is_1.default.undefined], options.prefixUrl);
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.cookieJar);
is_1.assert.any([is_1.default.object, is_1.default.string, is_1.default.undefined], options.searchParams);
is_1.assert.any([is_1.default.object, is_1.default.string, is_1.default.undefined], options.cache);
is_1.assert.any([is_1.default.object, is_1.default.number, is_1.default.undefined], options.timeout);
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.context);
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.hooks);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.decompress);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.ignoreInvalidCookies);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.followRedirect);
is_1.assert.any([is_1.default.number, is_1.default.undefined], options.maxRedirects);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.throwHttpErrors);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.http2);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.allowGetBody);
is_1.assert.any([is_1.default.string, is_1.default.undefined], options.localAddress);
is_1.assert.any([dns_ip_version_1.isDnsLookupIpVersion, is_1.default.undefined], options.dnsLookupIpVersion);
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.https);
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.rejectUnauthorized);
if (options.https) {
is_1.assert.any([is_1.default.boolean, is_1.default.undefined], options.https.rejectUnauthorized);
is_1.assert.any([is_1.default.function_, is_1.default.undefined], options.https.checkServerIdentity);
is_1.assert.any([is_1.default.string, is_1.default.object, is_1.default.array, is_1.default.undefined], options.https.certificateAuthority);
is_1.assert.any([is_1.default.string, is_1.default.object, is_1.default.array, is_1.default.undefined], options.https.key);
is_1.assert.any([is_1.default.string, is_1.default.object, is_1.default.array, is_1.default.undefined], options.https.certificate);
is_1.assert.any([is_1.default.string, is_1.default.undefined], options.https.passphrase);
is_1.assert.any([is_1.default.string, is_1.default.buffer, is_1.default.array, is_1.default.undefined], options.https.pfx);
}
is_1.assert.any([is_1.default.object, is_1.default.undefined], options.cacheOptions);
// `options.method`
if (is_1.default.string(options.method)) {
options.method = options.method.toUpperCase();
}
else {
options.method = 'GET';
}
// `options.headers`
if (options.headers === (defaults === null || defaults === void 0 ? void 0 : defaults.headers)) {
options.headers = { ...options.headers };
}
else {
options.headers = lowercaseKeys({ ...(defaults === null || defaults === void 0 ? void 0 : defaults.headers), ...options.headers });
}
// Disallow legacy `url.Url`
if ('slashes' in options) {
throw new TypeError('The legacy `url.Url` has been deprecated. Use `URL` instead.');
}
// `options.auth`
if ('auth' in options) {
throw new TypeError('Parameter `auth` is deprecated. Use `username` / `password` instead.');
}
// `options.searchParams`
if ('searchParams' in options) {
if (options.searchParams && options.searchParams !== (defaults === null || defaults === void 0 ? void 0 : defaults.searchParams)) {
let searchParameters;
if (is_1.default.string(options.searchParams) || (options.searchParams instanceof url_1.URLSearchParams)) {
searchParameters = new url_1.URLSearchParams(options.searchParams);
}
else {
validateSearchParameters(options.searchParams);
searchParameters = new url_1.URLSearchParams();
// eslint-disable-next-line guard-for-in
for (const key in options.searchParams) {
const value = options.searchParams[key];
if (value === null) {
searchParameters.append(key, '');
}
else if (value !== undefined) {
searchParameters.append(key, value);
}
}
}
// `normalizeArguments()` is also used to merge options
(_a = defaults === null || defaults === void 0 ? void 0 : defaults.searchParams) === null || _a === void 0 ? void 0 : _a.forEach((value, key) => {
// Only use default if one isn't already defined
if (!searchParameters.has(key)) {
searchParameters.append(key, value);
}
});
options.searchParams = searchParameters;
}
}
// `options.username` & `options.password`
options.username = (_b = options.username) !== null && _b !== void 0 ? _b : '';
options.password = (_c = options.password) !== null && _c !== void 0 ? _c : '';
// `options.prefixUrl` & `options.url`
if (is_1.default.undefined(options.prefixUrl)) {
options.prefixUrl = (_d = defaults === null || defaults === void 0 ? void 0 : defaults.prefixUrl) !== null && _d !== void 0 ? _d : '';
}
else {
options.prefixUrl = options.prefixUrl.toString();
if (options.prefixUrl !== '' && !options.prefixUrl.endsWith('/')) {
options.prefixUrl += '/';
}
}
if (is_1.default.string(options.url)) {
if (options.url.startsWith('/')) {
throw new Error('`input` must not start with a slash when using `prefixUrl`');
}
options.url = (0, options_to_url_1.default)(options.prefixUrl + options.url, options);
}
else if ((is_1.default.undefined(options.url) && options.prefixUrl !== '') || options.protocol) {
options.url = (0, options_to_url_1.default)(options.prefixUrl, options);
}
if (options.url) {
if ('port' in options) {
delete options.port;
}
// Make it possible to change `options.prefixUrl`
let { prefixUrl } = options;
Object.defineProperty(options, 'prefixUrl', {
set: (value) => {
const url = options.url;
if (!url.href.startsWith(value)) {
throw new Error(`Cannot change \`prefixUrl\` from ${prefixUrl} to ${value}: ${url.href}`);
}
options.url = new url_1.URL(value + url.href.slice(prefixUrl.length));
prefixUrl = value;
},
get: () => prefixUrl
});
// Support UNIX sockets
let { protocol } = options.url;
if (protocol === 'unix:') {
protocol = 'http:';
options.url = new url_1.URL(`http://unix${options.url.pathname}${options.url.search}`);
}
// Set search params
if (options.searchParams) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
options.url.search = options.searchParams.toString();
}
// Protocol check
if (protocol !== 'http:' && protocol !== 'https:') {
throw new UnsupportedProtocolError(options);
}
// Update `username`
if (options.username === '') {
options.username = options.url.username;
}
else {
options.url.username = options.username;
}
// Update `password`
if (options.password === '') {
options.password = options.url.password;
}
else {
options.url.password = options.password;
}
}
// `options.cookieJar`
const { cookieJar } = options;
if (cookieJar) {
let { setCookie, getCookieString } = cookieJar;
is_1.assert.function_(setCookie);
is_1.assert.function_(getCookieString);
/* istanbul ignore next: Horrible `tough-cookie` v3 check */
if (setCookie.length === 4 && getCookieString.length === 0) {
setCookie = (0, util_1.promisify)(setCookie.bind(options.cookieJar));
getCookieString = (0, util_1.promisify)(getCookieString.bind(options.cookieJar));
options.cookieJar = {
setCookie,
getCookieString: getCookieString
};
}
}
// `options.cache`
const { cache } = options;
if (cache) {
if (!cacheableStore.has(cache)) {
cacheableStore.set(cache, new CacheableRequest(((requestOptions, handler) => {
const result = requestOptions[kRequest](requestOptions, handler);
// TODO: remove this when `cacheable-request` supports async request functions.
if (is_1.default.promise(result)) {
// @ts-expect-error
// We only need to implement the error handler in order to support HTTP2 caching.
// The result will be a promise anyway.
result.once = (event, handler) => {
if (event === 'error') {
result.catch(handler);
}
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);
request.once('abort', handler);
}
catch (_a) { }
})();
}
else {
/* istanbul ignore next: safety check */
throw new Error(`Unknown HTTP2 promise event: ${event}`);
}
return result;
};
}
return result;
}), cache));
}
}
// `options.cacheOptions`
options.cacheOptions = { ...options.cacheOptions };
// `options.dnsCache`
if (options.dnsCache === true) {
if (!globalDnsCache) {
globalDnsCache = new cacheable_lookup_1.default();
}
options.dnsCache = globalDnsCache;
}
else if (!is_1.default.undefined(options.dnsCache) && !options.dnsCache.lookup) {
throw new TypeError(`Parameter \`dnsCache\` must be a CacheableLookup instance or a boolean, got ${(0, is_1.default)(options.dnsCache)}`);
}
// `options.timeout`
if (is_1.default.number(options.timeout)) {
options.timeout = { request: options.timeout };
}
else if (defaults && options.timeout !== defaults.timeout) {
options.timeout = {
...defaults.timeout,
...options.timeout
};
}
else {
options.timeout = { ...options.timeout };
}
// `options.context`
if (!options.context) {
options.context = {};
}
// `options.hooks`
const areHooksDefault = options.hooks === (defaults === null || defaults === void 0 ? void 0 : defaults.hooks);
options.hooks = { ...options.hooks };
for (const event of exports.knownHookEvents) {
if (event in options.hooks) {
if (is_1.default.array(options.hooks[event])) {
// See https://github.com/microsoft/TypeScript/issues/31445#issuecomment-576929044
options.hooks[event] = [...options.hooks[event]];
}
else {
throw new TypeError(`Parameter \`${event}\` must be an Array, got ${(0, is_1.default)(options.hooks[event])}`);
}
}
else {
options.hooks[event] = [];
}
}
if (defaults && !areHooksDefault) {
for (const event of exports.knownHookEvents) {
const defaultHooks = defaults.hooks[event];
if (defaultHooks.length > 0) {
// See https://github.com/microsoft/TypeScript/issues/31445#issuecomment-576929044
options.hooks[event] = [
...defaults.hooks[event],
...options.hooks[event]
];
}
}
}
// DNS options
if ('family' in options) {
(0, deprecation_warning_1.default)('"options.family" was never documented, please use "options.dnsLookupIpVersion"');
}
// HTTPS options
if (defaults === null || defaults === void 0 ? void 0 : defaults.https) {
options.https = { ...defaults.https, ...options.https };
}
if ('rejectUnauthorized' in options) {
(0, deprecation_warning_1.default)('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"');
}
if ('checkServerIdentity' in options) {
(0, deprecation_warning_1.default)('"options.checkServerIdentity" was never documented, please use "options.https.checkServerIdentity"');
}
if ('ca' in options) {
(0, deprecation_warning_1.default)('"options.ca" was never documented, please use "options.https.certificateAuthority"');
}
if ('key' in options) {
(0, deprecation_warning_1.default)('"options.key" was never documented, please use "options.https.key"');
}
if ('cert' in options) {
(0, deprecation_warning_1.default)('"options.cert" was never documented, please use "options.https.certificate"');
}
if ('passphrase' in options) {
(0, deprecation_warning_1.default)('"options.passphrase" was never documented, please use "options.https.passphrase"');
}
if ('pfx' in options) {
(0, deprecation_warning_1.default)('"options.pfx" was never documented, please use "options.https.pfx"');
}
// Other options
if ('followRedirects' in options) {
throw new TypeError('The `followRedirects` option does not exist. Use `followRedirect` instead.');
}
if (options.agent) {
for (const key in options.agent) {
if (key !== 'http' && key !== 'https' && key !== 'http2') {
throw new TypeError(`Expected the \`options.agent\` properties to be \`http\`, \`https\` or \`http2\`, got \`${key}\``);
}
}
}
options.maxRedirects = (_e = options.maxRedirects) !== null && _e !== void 0 ? _e : 0;
// Set non-enumerable properties
(0, exports.setNonEnumerableProperties)([defaults, rawOptions], options);
return (0, normalize_arguments_1.default)(options, defaults);
}
_lockWrite() {
const onLockedWrite = () => {
throw new TypeError('The payload has been already provided');
};
this.write = onLockedWrite;
this.end = onLockedWrite;
}
_unlockWrite() {
this.write = super.write;
this.end = super.end;
}
async _finalizeBody() {
const { options } = this;
const { headers } = options;
const isForm = !is_1.default.undefined(options.form);
const isJSON = !is_1.default.undefined(options.json);
const isBody = !is_1.default.undefined(options.body);
const hasPayload = isForm || isJSON || isBody;
const cannotHaveBody = exports.withoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
this._cannotHaveBody = cannotHaveBody;
if (hasPayload) {
if (cannotHaveBody) {
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
}
if ([isBody, isForm, isJSON].filter(isTrue => isTrue).length > 1) {
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
}
if (isBody &&
!(options.body instanceof stream_1.Readable) &&
!is_1.default.string(options.body) &&
!is_1.default.buffer(options.body) &&
!(0, is_form_data_1.default)(options.body)) {
throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
}
if (isForm && !is_1.default.object(options.form)) {
throw new TypeError('The `form` option must be an Object');
}
{
// Serialize body
const noContentType = !is_1.default.string(headers['content-type']);
if (isBody) {
// Special case for https://github.com/form-data/form-data
if ((0, is_form_data_1.default)(options.body) && noContentType) {
headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
}
this[kBody] = options.body;
}
else if (isForm) {
if (noContentType) {
headers['content-type'] = 'application/x-www-form-urlencoded';
}
this[kBody] = (new url_1.URLSearchParams(options.form)).toString();
}
else {
if (noContentType) {
headers['content-type'] = 'application/json';
}
this[kBody] = options.stringifyJson(options.json);
}
const uploadBodySize = await (0, get_body_size_1.default)(this[kBody], 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_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) {
if (!cannotHaveBody && !is_1.default.undefined(uploadBodySize)) {
headers['content-length'] = String(uploadBodySize);
}
}
}
}
else if (cannotHaveBody) {
this._lockWrite();
}
else {
this._unlockWrite();
}
this[kBodySize] = Number(headers['content-length']) || undefined;
}
async _onResponseBase(response) {
const { options } = this;
const { url } = options;
this[kOriginalResponse] = response;
if (options.decompress) {
response = decompressResponse(response);
}
const statusCode = response.statusCode;
const typedResponse = response;
typedResponse.statusMessage = typedResponse.statusMessage ? typedResponse.statusMessage : http.STATUS_CODES[statusCode];
typedResponse.url = options.url.toString();
typedResponse.requestUrl = this.requestUrl;
typedResponse.redirectUrls = this.redirects;
typedResponse.request = this;
typedResponse.isFromCache = response.fromCache || false;
typedResponse.ip = this.ip;
typedResponse.retryCount = this.retryCount;
this[kIsFromCache] = typedResponse.isFromCache;
this[kResponseSize] = Number(response.headers['content-length']) || undefined;
this[kResponse] = response;
response.once('end', () => {
this[kResponseSize] = this[kDownloadedSize];
this.emit('downloadProgress', this.downloadProgress);
});
response.once('error', (error) => {
// 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._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_1.default.object(options.cookieJar) && rawCookies) {
let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
if (options.ignoreInvalidCookies) {
promises = promises.map(async (p) => p.catch(() => { }));
}
try {
await Promise.all(promises);
}
catch (error) {
// @ts-ignore
this._beforeError(error);
return;
}
}
if (options.followRedirect && 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.
response.resume();
if (this[kRequest]) {
this[kCancelTimeouts]();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this[kRequest];
this[kUnproxyEvents]();
}
const shouldBeGet = statusCode === 303 && options.method !== 'GET' && options.method !== 'HEAD';
if (shouldBeGet || !options.methodRewriting) {
// Server responded with "see other", indicating that the resource exists at another location,
// and the client should request it from that location via GET or HEAD.
options.method = 'GET';
if ('body' in options) {
delete options.body;
}
if ('json' in options) {
delete options.json;
}
if ('form' in options) {
delete options.form;
}
this[kBody] = undefined;
delete options.headers['content-length'];
}
if (this.redirects.length >= options.maxRedirects) {
this._beforeError(new MaxRedirectsError(this));
return;
}
try {
// Do not remove. See https://github.com/sindresorhus/got/pull/214
const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
// Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
const redirectUrl = new url_1.URL(redirectBuffer, url);
const redirectString = redirectUrl.toString();
decodeURI(redirectString);
// eslint-disable-next-line no-inner-declarations
function isUnixSocketURL(url) {
return url.protocol === 'unix:' || url.hostname === 'unix';
}
if (!isUnixSocketURL(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.hostname || redirectUrl.port !== url.port) {
if ('host' in options.headers) {
delete options.headers.host;
}
if ('cookie' in options.headers) {
delete options.headers.cookie;
}
if ('authorization' in options.headers) {
delete options.headers.authorization;
}
if (options.username || options.password) {
options.username = '';
options.password = '';
}
}
else {
redirectUrl.username = options.username;
redirectUrl.password = options.password;
}
this.redirects.push(redirectString);
options.url = redirectUrl;
for (const hook of options.hooks.beforeRedirect) {
// eslint-disable-next-line no-await-in-loop
await hook(options, typedResponse);
}
this.emit('redirect', typedResponse, options);
await this._makeRequest();
}
catch (error) {
// @ts-ignore
this._beforeError(error);
return;
}
return;
}
if (options.isStream && options.throwHttpErrors && !(0, is_response_ok_1.isResponseOk)(typedResponse)) {
this._beforeError(new HTTPError(typedResponse));
return;
}
response.on('readable', () => {
if (this[kTriggerRead]) {
this._read();
}
});
this.on('resume', () => {
response.resume();
});
this.on('pause', () => {
response.pause();
});
response.once('end', () => {
this.push(null);
});
this.emit('response', response);
for (const destination of this[kServerResponsesPiped]) {
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;
}
}
async _onResponse(response) {
try {
await this._onResponseBase(response);
}
catch (error) {
/* istanbul ignore next: better safe than sorry */
// @ts-ignore
this._beforeError(error);
}
}
_onRequest(request) {
const { options } = this;
const { timeout, url } = options;
(0, http_timer_1.default)(request);
this[kCancelTimeouts] = (0, timed_out_1.default)(request, timeout, url);
const responseEventName = options.cache ? 'cacheableResponse' : 'response';
request.once(responseEventName, (response) => {
void this._onResponse(response);
});
request.once('error', (error) => {
var _a;
// Force clean-up, because some packages (e.g. nock) don't do this.
request.destroy();
// Node.js <= 12.18.2 mistakenly emits the response `end` first.
(_a = request.res) === null || _a === void 0 ? void 0 : _a.removeAllListeners('end');
error = error instanceof timed_out_1.TimeoutError ? new TimeoutError(error, this.timings, this) : new RequestError(error.message, error, this);
this._beforeError(error);
});
this[kUnproxyEvents] = (0, proxy_events_1.default)(request, this, proxiedRequestEvents);
this[kRequest] = request;
this.emit('uploadProgress', this.uploadProgress);
// Send body
const body = this[kBody];
const currentRequest = this.redirects.length === 0 ? this : request;
if (is_1.default.nodeStream(body)) {
body.pipe(currentRequest);
body.once('error', (error) => {
this._beforeError(new UploadError(error, this));
});
}
else {
this._unlockWrite();
if (!is_1.default.undefined(body)) {
this._writeRequest(body, undefined, () => { });
currentRequest.end();
this._lockWrite();
}
else if (this._cannotHaveBody || this._noPipe) {
currentRequest.end();
this._lockWrite();
}
}
this.emit('request', request);
}
async _createCacheableRequest(url, options) {
return new Promise((resolve, reject) => {
// TODO: Remove `utils/url-to-options.ts` when `cacheable-request` is fixed
Object.assign(options, (0, url_to_options_1.default)(url));
// `http-cache-semantics` checks this
// TODO: Fix this ignore.
// @ts-expect-error
delete options.url;
let request;
// This is ugly
const cacheRequest = cacheableStore.get(options.cache)(options, async (response) => {
// TODO: Fix `cacheable-response`
response._readableState.autoDestroy = false;
if (request) {
(await request).emit('cacheableResponse', response);
}
resolve(response);
});
// Restore options
options.url = url;
cacheRequest.once('error', reject);
cacheRequest.once('request', async (requestOrPromise) => {
request = requestOrPromise;
resolve(request);
});
});
}
async _makeRequest() {
var _a, _b, _c, _d, _e;
const { options } = this;
const { headers } = options;
for (const key in headers) {
if (is_1.default.undefined(headers[key])) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete headers[key];
}
else if (is_1.default.null_(headers[key])) {
throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
}
}
if (options.decompress && is_1.default.undefined(headers['accept-encoding'])) {
headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
}
// Set cookies
if (options.cookieJar) {
const cookieString = await options.cookieJar.getCookieString(options.url.toString());
if (is_1.default.nonEmptyString(cookieString)) {
options.headers.cookie = cookieString;
}
}
for (const hook of options.hooks.beforeRequest) {
// eslint-disable-next-line no-await-in-loop
const result = await hook(options);
if (!is_1.default.undefined(result)) {
// @ts-expect-error Skip the type mismatch to support abstract responses
options.request = () => result;
break;
}
}
if (options.body && this[kBody] !== options.body) {
this[kBody] = options.body;
}
const { agent, request, timeout, url } = options;
if (options.dnsCache && !('lookup' in options)) {
options.lookup = options.dnsCache.lookup;
}
// UNIX sockets
if (url.hostname === 'unix') {
const matches = /(?<socketPath>.+?):(?<path>.+)/.exec(`${url.pathname}${url.search}`);
if (matches === null || matches === void 0 ? void 0 : matches.groups) {
const { socketPath, path } = matches.groups;
Object.assign(options, {
socketPath,
path,
host: ''
});
}
}
const isHttps = url.protocol === 'https:';
// Fallback function
let fallbackFn;
if (options.http2) {
// @ts-ignore
fallbackFn = http2wrapper.auto;
}
else {
fallbackFn = isHttps ? https.request : http.request;
}
const realFn = (_a = options.request) !== null && _a !== void 0 ? _a : fallbackFn;
// Cache support
const fn = options.cache ? this._createCacheableRequest : realFn;
// Pass an agent directly when HTTP2 is disabled
if (agent && !options.http2) {
options.agent = agent[isHttps ? 'https' : 'http'];
}
// Prepare plain HTTP request options
options[kRequest] = realFn;
delete options.request;
// TODO: Fix this ignore.
// @ts-expect-error
delete options.timeout;
const requestOptions = options;
requestOptions.shared = (_b = options.cacheOptions) === null || _b === void 0 ? void 0 : _b.shared;
requestOptions.cacheHeuristic = (_c = options.cacheOptions) === null || _c === void 0 ? void 0 : _c.cacheHeuristic;
requestOptions.immutableMinTimeToLive = (_d = options.cacheOptions) === null || _d === void 0 ? void 0 : _d.immutableMinTimeToLive;
requestOptions.ignoreCargoCult = (_e = options.cacheOptions) === null || _e === void 0 ? void 0 : _e.ignoreCargoCult;
// If `dnsLookupIpVersion` is not present do not override `family`
if (options.dnsLookupIpVersion !== undefined) {
try {
requestOptions.family = (0, dns_ip_version_1.dnsLookupIpVersionToFamily)(options.dnsLookupIpVersion);
}
catch (_f) {
throw new Error('Invalid `dnsLookupIpVersion` option value');
}
}
// HTTPS options remapping
if (options.https) {
if ('rejectUnauthorized' in options.https) {
requestOptions.rejectUnauthorized = options.https.rejectUnauthorized;
}
if (options.https.checkServerIdentity) {
requestOptions.checkServerIdentity = options.https.checkServerIdentity;
}
if (options.https.certificateAuthority) {
requestOptions.ca = options.https.certificateAuthority;
}
if (options.https.certificate) {
requestOptions.cert = options.https.certificate;
}
if (options.https.key) {
requestOptions.key = options.https.key;
}
if (options.https.passphrase) {
requestOptions.passphrase = options.https.passphrase;
}
if (options.https.pfx) {
requestOptions.pfx = options.https.pfx;
}
}
try {
let requestOrResponse = await fn(url, requestOptions);
if (is_1.default.undefined(requestOrResponse)) {
requestOrResponse = fallbackFn(url, requestOptions);
}
// Restore options
options.request = request;
options.timeout = timeout;
options.agent = agent;
// HTTPS options restore
if (options.https) {
if ('rejectUnauthorized' in options.https) {
delete requestOptions.rejectUnauthorized;
}
if (options.https.checkServerIdentity) {
// @ts-expect-error - This one will be removed when we remove the alias.
delete requestOptions.checkServerIdentity;
}
if (options.https.certificateAuthority) {
delete requestOptions.ca;
}
if (options.https.certificate) {
delete requestOptions.cert;
}
if (options.https.key) {
delete requestOptions.key;
}
if (options.https.passphrase) {
delete requestOptions.passphrase;
}
if (options.https.pfx) {
delete requestOptions.pfx;
}
}
if (isClientRequest(requestOrResponse)) {
this._onRequest(requestOrResponse);
// Emit the response after the stream has been ended
}
else if (this.writable) {
this.once('finish', () => {
void this._onResponse(requestOrResponse);
});
this._unlockWrite();
this.end();
this._lockWrite();
}
else {
void this._onResponse(requestOrResponse);
}
}
catch (error) {
if (error instanceof CacheableRequest.CacheError) {
throw new CacheError(error, this);
}
// @ts-ignore
throw new RequestError(error.message, error, this);
}
}
async _error(error) {
try {
for (const hook of this.options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
error = await hook(error);
}
}
catch (error_) {
// @ts-ignore
error = new RequestError(error_.message, error_, this);
}
this.destroy(error);
}
_beforeError(error) {
if (this[kStopReading]) {
return;
}
const { options } = this;
const retryCount = this.retryCount + 1;
this[kStopReading] = true;
if (!(error instanceof RequestError)) {
error = new RequestError(error.message, error, this);
}
const typedError = error;
const { response } = typedError;
void (async () => {
if (response && !response.body) {
response.setEncoding(this._readableState.encoding);
try {
response.rawBody = await (0, get_buffer_1.default)(response);
response.body = response.rawBody.toString();
}
catch (_a) { }
}
if (this.listenerCount('retry') !== 0) {
let backoff;
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;
}
}
backoff = await options.retry.calculateDelay({
attemptCount: retryCount,
retryOptions: options.retry,
error: typedError,
retryAfter,
computedValue: (0, calculate_retry_delay_1.default)({
attemptCount: retryCount,
retryOptions: options.retry,
error: typedError,
retryAfter,
computedValue: 0
})
});
}
catch (error_) {
// @ts-ignore
void this._error(new RequestError(error_.message, error_, this));
return;
}
if (backoff) {
const retry = async () => {
try {
for (const hook of this.options.hooks.beforeRetry) {
// eslint-disable-next-line no-await-in-loop
await hook(this.options, typedError, retryCount);
}
}
catch (error_) {
// @ts-ignore
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', retryCount, error);
};
this[kRetryTimeout] = setTimeout(retry, backoff);
return;
}
}
void this._error(typedError);
})();
}
_read() {
this[kTriggerRead] = true;
const response = this[kResponse];
if (response && !this[kStopReading]) {
// We cannot put this in the `if` above
// because `.read()` also triggers the `end` event
if (response.readableLength) {
this[kTriggerRead] = false;
}
let data;
while ((data = response.read()) !== null) {
this[kDownloadedSize] += data.length;
this[kStartedReading] = true;
const progress = this.downloadProgress;
if (progress.percent < 1) {
this.emit('downloadProgress', progress);
}
this.push(data);
}
}
}
// Node.js 12 has incorrect types, so the encoding must be a string
_write(chunk, encoding, callback) {
const write = () => {
this._writeRequest(chunk, encoding, callback);
};
if (this.requestInitialized) {
write();
}
else {
this[kJobs].push(write);
}
}
_writeRequest(chunk, encoding, callback) {
if (this[kRequest].destroyed) {
// Probably the `ClientRequest` instance will throw
return;
}
this._progressCallbacks.push(() => {
this[kUploadedSize] += Buffer.byteLength(chunk, encoding);
const progress = this.uploadProgress;
if (progress.percent < 1) {
this.emit('uploadProgress', progress);
}
});
// TODO: What happens if it's from cache? Then this[kRequest] won't be defined.
this[kRequest].write(chunk, encoding, (error) => {
if (!error && this._progressCallbacks.length > 0) {
this._progressCallbacks.shift()();
}
callback(error);
});
}
_final(callback) {
const endRequest = () => {
// FIX: Node.js 10 calls the write callback AFTER the end callback!
while (this._progressCallbacks.length !== 0) {
this._progressCallbacks.shift()();
}
// We need to check if `this[kRequest]` is present,
// because it isn't when we use cache.
if (!(kRequest in this)) {
callback();
return;
}
if (this[kRequest].destroyed) {
callback();
return;
}
this[kRequest].end((error) => {
if (!error) {
this[kBodySize] = this[kUploadedSize];
this.emit('uploadProgress', this.uploadProgress);
this[kRequest].emit('upload-complete');
}
callback(error);
});
};
if (this.requestInitialized) {
endRequest();
}
else {
this[kJobs].push(endRequest);
}
}
_destroy(error, callback) {
var _a;
this[kStopReading] = true;
// Prevent further retries
clearTimeout(this[kRetryTimeout]);
if (kRequest in this) {
this[kCancelTimeouts]();
// TODO: Remove the next `if` when these get fixed:
// - https://github.com/nodejs/node/issues/32851
if (!((_a = this[kResponse]) === null || _a === void 0 ? void 0 : _a.complete)) {
this[kRequest].destroy();
}
}
if (error !== null && !is_1.default.undefined(error) && !(error instanceof RequestError)) {
error = new RequestError(error.message, error, this);
}
callback(error);
}
get _isAboutToError() {
return this[kStopReading];
}
/**
The remote IP address.
*/
get ip() {
var _a;
return (_a = this.socket) === null || _a === void 0 ? void 0 : _a.remoteAddress;
}
/**
Indicates whether the request has been aborted or not.
*/
get aborted() {
var _a, _b, _c;
return ((_b = (_a = this[kRequest]) === null || _a === void 0 ? void 0 : _a.destroyed) !== null && _b !== void 0 ? _b : this.destroyed) && !((_c = this[kOriginalResponse]) === null || _c === void 0 ? void 0 : _c.complete);
}
get socket() {
var _a, _b;
return (_b = (_a = this[kRequest]) === null || _a === void 0 ? void 0 : _a.socket) !== null && _b !== void 0 ? _b : undefined;
}
/**
Progress event for downloading (receiving a response).
*/
get downloadProgress() {
let percent;
if (this[kResponseSize]) {
percent = this[kDownloadedSize] / this[kResponseSize];
}
else if (this[kResponseSize] === this[kDownloadedSize]) {
percent = 1;
}
else {
percent = 0;
}
return {
percent,
transferred: this[kDownloadedSize],
total: this[kResponseSize]
};
}
/**
Progress event for uploading (sending a request).
*/
get uploadProgress() {
let percent;
if (this[kBodySize]) {
percent = this[kUploadedSize] / this[kBodySize];
}
else if (this[kBodySize] === this[kUploadedSize]) {
percent = 1;
}
else {
percent = 0;
}
return {
percent,
transferred: this[kUploadedSize],
total: this[kBodySize]
};
}
/**
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() {
var _a;
return (_a = this[kRequest]) === null || _a === void 0 ? void 0 : _a.timings;
}
/**
Whether the response was retrieved from the cache.
*/
get isFromCache() {
return this[kIsFromCache];
}
pipe(destination, options) {
if (this[kStartedReading]) {
throw new Error('Failed to pipe. The response has been emitted already.');
}
if (destination instanceof http_1.ServerResponse) {
this[kServerResponsesPiped].add(destination);
}
return super.pipe(destination, options);
}
unpipe(destination) {
if (destination instanceof http_1.ServerResponse) {
this[kServerResponsesPiped].delete(destination);
}
super.unpipe(destination);
return this;
}
}
exports.default = Request;
|