1492 lines
62 KiB
Plaintext
1492 lines
62 KiB
Plaintext
"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");
|
|
// @ts-expect-error Missing types
|
|
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'
|
|
];
|
|
exports.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);
|
|
};
|
|
/**
|
|
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;
|
|
super(message);
|
|
Error.captureStackTrace(this, this.constructor);
|
|
this.name = 'RequestError';
|
|
this.code = error.code;
|
|
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 = (_a = this.request) === null || _a === void 0 ? void 0 : _a.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';
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
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.
|
|
for (const job of this[kJobs]) {
|
|
job();
|
|
}
|
|
// Prevent memory leak
|
|
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) {
|
|
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 = options_to_url_1.default(options.prefixUrl + options.url, options);
|
|
}
|
|
else if ((is_1.default.undefined(options.url) && options.prefixUrl !== '') || options.protocol) {
|
|
options.url = 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 = util_1.promisify(setCookie.bind(options.cookieJar));
|
|
getCookieString = 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 ${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 ${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) {
|
|
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) {
|
|
deprecation_warning_1.default('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"');
|
|
}
|
|
if ('checkServerIdentity' in options) {
|
|
deprecation_warning_1.default('"options.checkServerIdentity" was never documented, please use "options.https.checkServerIdentity"');
|
|
}
|
|
if ('ca' in options) {
|
|
deprecation_warning_1.default('"options.ca" was never documented, please use "options.https.certificateAuthority"');
|
|
}
|
|
if ('key' in options) {
|
|
deprecation_warning_1.default('"options.key" was never documented, please use "options.https.key"');
|
|
}
|
|
if ('cert' in options) {
|
|
deprecation_warning_1.default('"options.cert" was never documented, please use "options.https.certificate"');
|
|
}
|
|
if ('passphrase' in options) {
|
|
deprecation_warning_1.default('"options.passphrase" was never documented, please use "options.https.passphrase"');
|
|
}
|
|
if ('pfx' in options) {
|
|
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
|
|
exports.setNonEnumerableProperties([defaults, rawOptions], options);
|
|
return 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) &&
|
|
!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 (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 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) {
|
|
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);
|
|
// 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) {
|
|
this._beforeError(error);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
if (options.isStream && options.throwHttpErrors && !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 */
|
|
this._beforeError(error);
|
|
}
|
|
}
|
|
_onRequest(request) {
|
|
const { options } = this;
|
|
const { timeout, url } = options;
|
|
http_timer_1.default(request);
|
|
this[kCancelTimeouts] = 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] = 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, 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) {
|
|
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 = 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);
|
|
}
|
|
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_) {
|
|
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 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: calculate_retry_delay_1.default({
|
|
attemptCount: retryCount,
|
|
retryOptions: options.retry,
|
|
error: typedError,
|
|
retryAfter,
|
|
computedValue: 0
|
|
})
|
|
});
|
|
}
|
|
catch (error_) {
|
|
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_) {
|
|
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;
|