442 lines
19 KiB
JavaScript
442 lines
19 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const url_1 = require("url");
|
|
const util_1 = require("util");
|
|
const CacheableRequest = require("cacheable-request");
|
|
const http = require("http");
|
|
const https = require("https");
|
|
const lowercaseKeys = require("lowercase-keys");
|
|
const toReadableStream = require("to-readable-stream");
|
|
const is_1 = require("@sindresorhus/is");
|
|
const cacheable_lookup_1 = require("cacheable-lookup");
|
|
const errors_1 = require("./errors");
|
|
const known_hook_events_1 = require("./known-hook-events");
|
|
const dynamic_require_1 = require("./utils/dynamic-require");
|
|
const get_body_size_1 = require("./utils/get-body-size");
|
|
const is_form_data_1 = require("./utils/is-form-data");
|
|
const merge_1 = require("./utils/merge");
|
|
const options_to_url_1 = require("./utils/options-to-url");
|
|
const supports_brotli_1 = require("./utils/supports-brotli");
|
|
const types_1 = require("./types");
|
|
const nonEnumerableProperties = [
|
|
'context',
|
|
'body',
|
|
'json',
|
|
'form'
|
|
];
|
|
const isAgentByProtocol = (agent) => is_1.default.object(agent);
|
|
// TODO: `preNormalizeArguments` should merge `options` & `defaults`
|
|
exports.preNormalizeArguments = (options, defaults) => {
|
|
var _a, _b, _c, _d, _e, _f;
|
|
// `options.headers`
|
|
if (is_1.default.undefined(options.headers)) {
|
|
options.headers = {};
|
|
}
|
|
else {
|
|
options.headers = lowercaseKeys(options.headers);
|
|
}
|
|
for (const [key, value] of Object.entries(options.headers)) {
|
|
if (is_1.default.null_(value)) {
|
|
throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
|
|
}
|
|
}
|
|
// `options.prefixUrl`
|
|
if (is_1.default.urlInstance(options.prefixUrl) || is_1.default.string(options.prefixUrl)) {
|
|
options.prefixUrl = options.prefixUrl.toString();
|
|
if (options.prefixUrl.length !== 0 && !options.prefixUrl.endsWith('/')) {
|
|
options.prefixUrl += '/';
|
|
}
|
|
}
|
|
else {
|
|
options.prefixUrl = defaults ? defaults.prefixUrl : '';
|
|
}
|
|
// `options.hooks`
|
|
if (is_1.default.undefined(options.hooks)) {
|
|
options.hooks = {};
|
|
}
|
|
if (is_1.default.object(options.hooks)) {
|
|
for (const event of known_hook_events_1.default) {
|
|
if (Reflect.has(options.hooks, event)) {
|
|
if (!is_1.default.array(options.hooks[event])) {
|
|
throw new TypeError(`Parameter \`${event}\` must be an Array, not ${is_1.default(options.hooks[event])}`);
|
|
}
|
|
}
|
|
else {
|
|
options.hooks[event] = [];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
throw new TypeError(`Parameter \`hooks\` must be an Object, not ${is_1.default(options.hooks)}`);
|
|
}
|
|
if (defaults) {
|
|
for (const event of known_hook_events_1.default) {
|
|
if (!(Reflect.has(options.hooks, event) && is_1.default.undefined(options.hooks[event]))) {
|
|
// @ts-ignore Union type array is not assignable to union array type
|
|
options.hooks[event] = [
|
|
...defaults.hooks[event],
|
|
...options.hooks[event]
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// `options.timeout`
|
|
if (is_1.default.number(options.timeout)) {
|
|
options.timeout = { request: options.timeout };
|
|
}
|
|
else if (!is_1.default.object(options.timeout)) {
|
|
options.timeout = {};
|
|
}
|
|
// `options.retry`
|
|
const { retry } = options;
|
|
if (defaults) {
|
|
options.retry = { ...defaults.retry };
|
|
}
|
|
else {
|
|
options.retry = {
|
|
calculateDelay: retryObject => retryObject.computedValue,
|
|
limit: 0,
|
|
methods: [],
|
|
statusCodes: [],
|
|
errorCodes: [],
|
|
maxRetryAfter: undefined
|
|
};
|
|
}
|
|
if (is_1.default.object(retry)) {
|
|
options.retry = {
|
|
...options.retry,
|
|
...retry
|
|
};
|
|
}
|
|
else if (is_1.default.number(retry)) {
|
|
options.retry.limit = retry;
|
|
}
|
|
if (options.retry.maxRetryAfter === undefined) {
|
|
options.retry.maxRetryAfter = Math.min(...[options.timeout.request, options.timeout.connect].filter((n) => !is_1.default.nullOrUndefined(n)));
|
|
}
|
|
options.retry.methods = [...new Set(options.retry.methods.map(method => method.toUpperCase()))];
|
|
options.retry.statusCodes = [...new Set(options.retry.statusCodes)];
|
|
options.retry.errorCodes = [...new Set(options.retry.errorCodes)];
|
|
// `options.dnsCache`
|
|
if (options.dnsCache && !(options.dnsCache instanceof cacheable_lookup_1.default)) {
|
|
options.dnsCache = new cacheable_lookup_1.default({ cacheAdapter: options.dnsCache });
|
|
}
|
|
// `options.method`
|
|
if (is_1.default.string(options.method)) {
|
|
options.method = options.method.toUpperCase();
|
|
}
|
|
else {
|
|
options.method = (_b = (_a = defaults) === null || _a === void 0 ? void 0 : _a.method, (_b !== null && _b !== void 0 ? _b : 'GET'));
|
|
}
|
|
// Better memory management, so we don't have to generate a new object every time
|
|
if (options.cache) {
|
|
options.cacheableRequest = new CacheableRequest(
|
|
// @ts-ignore Cannot properly type a function with multiple definitions yet
|
|
(requestOptions, handler) => requestOptions[types_1.requestSymbol](requestOptions, handler), options.cache);
|
|
}
|
|
// `options.cookieJar`
|
|
if (is_1.default.object(options.cookieJar)) {
|
|
let { setCookie, getCookieString } = options.cookieJar;
|
|
// Horrible `tough-cookie` check
|
|
if (setCookie.length === 4 && getCookieString.length === 0) {
|
|
if (!Reflect.has(setCookie, util_1.promisify.custom)) {
|
|
// @ts-ignore TS is dumb - it says `setCookie` is `never`.
|
|
setCookie = util_1.promisify(setCookie.bind(options.cookieJar));
|
|
getCookieString = util_1.promisify(getCookieString.bind(options.cookieJar));
|
|
}
|
|
}
|
|
else if (setCookie.length !== 2) {
|
|
throw new TypeError('`options.cookieJar.setCookie` needs to be an async function with 2 arguments');
|
|
}
|
|
else if (getCookieString.length !== 1) {
|
|
throw new TypeError('`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
|
|
}
|
|
options.cookieJar = { setCookie, getCookieString };
|
|
}
|
|
// `options.encoding`
|
|
if (is_1.default.null_(options.encoding)) {
|
|
throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
|
|
}
|
|
// `options.maxRedirects`
|
|
if (!Reflect.has(options, 'maxRedirects') && !(defaults && Reflect.has(defaults, 'maxRedirects'))) {
|
|
options.maxRedirects = 0;
|
|
}
|
|
// Merge defaults
|
|
if (defaults) {
|
|
options = merge_1.default({}, defaults, options);
|
|
}
|
|
// `options._pagination`
|
|
if (is_1.default.object(options._pagination)) {
|
|
const { _pagination: pagination } = options;
|
|
if (!is_1.default.function_(pagination.transform)) {
|
|
throw new TypeError('`options._pagination.transform` must be implemented');
|
|
}
|
|
if (!is_1.default.function_(pagination.shouldContinue)) {
|
|
throw new TypeError('`options._pagination.shouldContinue` must be implemented');
|
|
}
|
|
if (!is_1.default.function_(pagination.filter)) {
|
|
throw new TypeError('`options._pagination.filter` must be implemented');
|
|
}
|
|
if (!is_1.default.function_(pagination.paginate)) {
|
|
throw new TypeError('`options._pagination.paginate` must be implemented');
|
|
}
|
|
}
|
|
// Other values
|
|
options.decompress = Boolean(options.decompress);
|
|
options.isStream = Boolean(options.isStream);
|
|
options.throwHttpErrors = Boolean(options.throwHttpErrors);
|
|
options.ignoreInvalidCookies = Boolean(options.ignoreInvalidCookies);
|
|
options.cache = (_c = options.cache, (_c !== null && _c !== void 0 ? _c : false));
|
|
options.responseType = (_d = options.responseType, (_d !== null && _d !== void 0 ? _d : 'text'));
|
|
options.resolveBodyOnly = Boolean(options.resolveBodyOnly);
|
|
options.followRedirect = Boolean(options.followRedirect);
|
|
options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false));
|
|
options.useElectronNet = Boolean(options.useElectronNet);
|
|
options.methodRewriting = Boolean(options.methodRewriting);
|
|
options.allowGetBody = Boolean(options.allowGetBody);
|
|
options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {}));
|
|
return options;
|
|
};
|
|
exports.mergeOptions = (...sources) => {
|
|
let mergedOptions = exports.preNormalizeArguments({});
|
|
// Non enumerable properties shall not be merged
|
|
const properties = {};
|
|
for (const source of sources) {
|
|
mergedOptions = exports.preNormalizeArguments(merge_1.default({}, source), mergedOptions);
|
|
for (const name of nonEnumerableProperties) {
|
|
if (!Reflect.has(source, name)) {
|
|
continue;
|
|
}
|
|
properties[name] = {
|
|
writable: true,
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source[name]
|
|
};
|
|
}
|
|
}
|
|
Object.defineProperties(mergedOptions, properties);
|
|
return mergedOptions;
|
|
};
|
|
exports.normalizeArguments = (url, options, defaults) => {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
// Merge options
|
|
if (typeof url === 'undefined') {
|
|
throw new TypeError('Missing `url` argument');
|
|
}
|
|
const runInitHooks = (hooks, options) => {
|
|
if (hooks && options) {
|
|
for (const hook of hooks) {
|
|
const result = hook(options);
|
|
if (is_1.default.promise(result)) {
|
|
throw new TypeError('The `init` hook must be a synchronous function');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const hasUrl = is_1.default.urlInstance(url) || is_1.default.string(url);
|
|
if (hasUrl) {
|
|
if (options) {
|
|
if (Reflect.has(options, 'url')) {
|
|
throw new TypeError('The `url` option cannot be used if the input is a valid URL.');
|
|
}
|
|
}
|
|
else {
|
|
options = {};
|
|
}
|
|
// @ts-ignore URL is not URL
|
|
options.url = url;
|
|
runInitHooks((_a = defaults) === null || _a === void 0 ? void 0 : _a.options.hooks.init, options);
|
|
runInitHooks((_b = options.hooks) === null || _b === void 0 ? void 0 : _b.init, options);
|
|
}
|
|
else if (Reflect.has(url, 'resolve')) {
|
|
throw new Error('The legacy `url.Url` is deprecated. Use `URL` instead.');
|
|
}
|
|
else {
|
|
runInitHooks((_c = defaults) === null || _c === void 0 ? void 0 : _c.options.hooks.init, url);
|
|
runInitHooks((_d = url.hooks) === null || _d === void 0 ? void 0 : _d.init, url);
|
|
if (options) {
|
|
runInitHooks((_e = defaults) === null || _e === void 0 ? void 0 : _e.options.hooks.init, options);
|
|
runInitHooks((_f = options.hooks) === null || _f === void 0 ? void 0 : _f.init, options);
|
|
}
|
|
}
|
|
if (hasUrl) {
|
|
options = exports.mergeOptions((_h = (_g = defaults) === null || _g === void 0 ? void 0 : _g.options, (_h !== null && _h !== void 0 ? _h : {})), (options !== null && options !== void 0 ? options : {}));
|
|
}
|
|
else {
|
|
options = exports.mergeOptions((_k = (_j = defaults) === null || _j === void 0 ? void 0 : _j.options, (_k !== null && _k !== void 0 ? _k : {})), url, (options !== null && options !== void 0 ? options : {}));
|
|
}
|
|
// Normalize URL
|
|
// TODO: drop `optionsToUrl` in Got 12
|
|
if (is_1.default.string(options.url)) {
|
|
options.url = options.prefixUrl + options.url;
|
|
options.url = options.url.replace(/^unix:/, 'http://$&');
|
|
if (options.searchParams || options.search) {
|
|
options.url = options.url.split('?')[0];
|
|
}
|
|
// @ts-ignore URL is not URL
|
|
options.url = options_to_url_1.default({
|
|
origin: options.url,
|
|
...options
|
|
});
|
|
}
|
|
else if (!is_1.default.urlInstance(options.url)) {
|
|
// @ts-ignore URL is not URL
|
|
options.url = options_to_url_1.default({ origin: options.prefixUrl, ...options });
|
|
}
|
|
const normalizedOptions = options;
|
|
// Make it possible to change `options.prefixUrl`
|
|
let prefixUrl = options.prefixUrl;
|
|
Object.defineProperty(normalizedOptions, 'prefixUrl', {
|
|
set: (value) => {
|
|
if (!normalizedOptions.url.href.startsWith(value)) {
|
|
throw new Error(`Cannot change \`prefixUrl\` from ${prefixUrl} to ${value}: ${normalizedOptions.url.href}`);
|
|
}
|
|
normalizedOptions.url = new url_1.URL(value + normalizedOptions.url.href.slice(prefixUrl.length));
|
|
prefixUrl = value;
|
|
},
|
|
get: () => prefixUrl
|
|
});
|
|
// Make it possible to remove default headers
|
|
for (const [key, value] of Object.entries(normalizedOptions.headers)) {
|
|
if (is_1.default.undefined(value)) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete normalizedOptions.headers[key];
|
|
}
|
|
}
|
|
return normalizedOptions;
|
|
};
|
|
const withoutBody = new Set(['HEAD']);
|
|
const withoutBodyUnlessSpecified = 'GET';
|
|
exports.normalizeRequestArguments = async (options) => {
|
|
var _a, _b, _c;
|
|
options = exports.mergeOptions(options);
|
|
// Serialize body
|
|
const { headers } = options;
|
|
const hasNoContentType = is_1.default.undefined(headers['content-type']);
|
|
{
|
|
// TODO: these checks should be moved to `preNormalizeArguments`
|
|
const isForm = !is_1.default.undefined(options.form);
|
|
const isJson = !is_1.default.undefined(options.json);
|
|
const isBody = !is_1.default.undefined(options.body);
|
|
if ((isBody || isForm || isJson) && withoutBody.has(options.method)) {
|
|
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
|
|
}
|
|
if (!options.allowGetBody && (isBody || isForm || isJson) && withoutBodyUnlessSpecified === options.method) {
|
|
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 &&
|
|
!is_1.default.nodeStream(options.body) &&
|
|
!is_1.default.string(options.body) &&
|
|
!is_1.default.buffer(options.body) &&
|
|
!(is_1.default.object(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');
|
|
}
|
|
}
|
|
if (options.body) {
|
|
// Special case for https://github.com/form-data/form-data
|
|
if (is_1.default.object(options.body) && is_form_data_1.default(options.body) && hasNoContentType) {
|
|
headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
|
|
}
|
|
}
|
|
else if (options.form) {
|
|
if (hasNoContentType) {
|
|
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
}
|
|
options.body = (new url_1.URLSearchParams(options.form)).toString();
|
|
}
|
|
else if (options.json) {
|
|
if (hasNoContentType) {
|
|
headers['content-type'] = 'application/json';
|
|
}
|
|
options.body = JSON.stringify(options.json);
|
|
}
|
|
const uploadBodySize = await get_body_size_1.default(options);
|
|
if (!is_1.default.nodeStream(options.body)) {
|
|
options.body = toReadableStream(options.body);
|
|
}
|
|
// 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 ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE' || (options.allowGetBody && options.method === 'GET')) &&
|
|
!is_1.default.undefined(uploadBodySize)) {
|
|
// @ts-ignore We assign if it is undefined, so this IS correct
|
|
headers['content-length'] = String(uploadBodySize);
|
|
}
|
|
}
|
|
if (!options.isStream && options.responseType === 'json' && is_1.default.undefined(headers.accept)) {
|
|
headers.accept = 'application/json';
|
|
}
|
|
if (options.decompress && is_1.default.undefined(headers['accept-encoding'])) {
|
|
headers['accept-encoding'] = supports_brotli_1.default ? 'gzip, deflate, br' : 'gzip, deflate';
|
|
}
|
|
// Validate URL
|
|
if (options.url.protocol !== 'http:' && options.url.protocol !== 'https:') {
|
|
throw new errors_1.UnsupportedProtocolError(options);
|
|
}
|
|
decodeURI(options.url.toString());
|
|
// Normalize request function
|
|
if (is_1.default.function_(options.request)) {
|
|
options[types_1.requestSymbol] = options.request;
|
|
delete options.request;
|
|
}
|
|
else {
|
|
options[types_1.requestSymbol] = options.url.protocol === 'https:' ? https.request : http.request;
|
|
}
|
|
// UNIX sockets
|
|
if (options.url.hostname === 'unix') {
|
|
const matches = /(?<socketPath>.+?):(?<path>.+)/.exec(options.url.pathname);
|
|
if ((_a = matches) === null || _a === void 0 ? void 0 : _a.groups) {
|
|
const { socketPath, path } = matches.groups;
|
|
options = {
|
|
...options,
|
|
socketPath,
|
|
path,
|
|
host: ''
|
|
};
|
|
}
|
|
}
|
|
if (isAgentByProtocol(options.agent)) {
|
|
options.agent = (_b = options.agent[options.url.protocol.slice(0, -1)], (_b !== null && _b !== void 0 ? _b : options.agent));
|
|
}
|
|
if (options.dnsCache) {
|
|
options.lookup = options.dnsCache.lookup;
|
|
}
|
|
/* istanbul ignore next: electron.net is broken */
|
|
// No point in typing process.versions correctly, as
|
|
// `process.version.electron` is used only once, right here.
|
|
if (options.useElectronNet && process.versions.electron) {
|
|
const electron = dynamic_require_1.default(module, 'electron'); // Trick webpack
|
|
options.request = util_1.deprecate((_c = electron.net.request, (_c !== null && _c !== void 0 ? _c : electron.remote.net.request)), 'Electron support has been deprecated and will be removed in Got 11.\n' +
|
|
'See https://github.com/sindresorhus/got/issues/899 for further information.', 'GOT_ELECTRON');
|
|
}
|
|
// Got's `timeout` is an object, http's `timeout` is a number, so they're not compatible.
|
|
delete options.timeout;
|
|
// Set cookies
|
|
if (options.cookieJar) {
|
|
const cookieString = await options.cookieJar.getCookieString(options.url.toString());
|
|
if (is_1.default.nonEmptyString(cookieString)) {
|
|
options.headers.cookie = cookieString;
|
|
}
|
|
else {
|
|
delete options.headers.cookie;
|
|
}
|
|
}
|
|
// `http-cache-semantics` checks this
|
|
delete options.url;
|
|
return options;
|
|
};
|