trouble-in-terror-town/node_modules/got/source/normalize-arguments.js
Mikolaj 2bbacbea09 did some more work on networking and removed EOS in favor of LRM
did some more work on networking and removed EOS in favor of Light Reflective Mirror
2022-05-31 15:04:31 +02:00

265 lines
7.4 KiB
JavaScript

'use strict';
const {URL, URLSearchParams} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10
const urlLib = require('url');
const is = require('@sindresorhus/is');
const urlParseLax = require('url-parse-lax');
const lowercaseKeys = require('lowercase-keys');
const urlToOptions = require('./utils/url-to-options');
const isFormData = require('./utils/is-form-data');
const merge = require('./merge');
const knownHookEvents = require('./known-hook-events');
const retryAfterStatusCodes = new Set([413, 429, 503]);
// `preNormalize` handles static options (e.g. headers).
// For example, when you create a custom instance and make a request
// with no static changes, they won't be normalized again.
//
// `normalize` operates on dynamic options - they cannot be saved.
// For example, `body` is everytime different per request.
// When it's done normalizing the new options, it performs merge()
// on the prenormalized options and the normalized ones.
const preNormalize = (options, defaults) => {
if (is.nullOrUndefined(options.headers)) {
options.headers = {};
} else {
options.headers = lowercaseKeys(options.headers);
}
if (options.baseUrl && !options.baseUrl.toString().endsWith('/')) {
options.baseUrl += '/';
}
if (options.stream) {
options.json = false;
}
if (is.nullOrUndefined(options.hooks)) {
options.hooks = {};
} else if (!is.object(options.hooks)) {
throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
}
for (const event of knownHookEvents) {
if (is.nullOrUndefined(options.hooks[event])) {
if (defaults) {
options.hooks[event] = [...defaults.hooks[event]];
} else {
options.hooks[event] = [];
}
}
}
if (is.number(options.timeout)) {
options.gotTimeout = {request: options.timeout};
} else if (is.object(options.timeout)) {
options.gotTimeout = options.timeout;
}
delete options.timeout;
const {retry} = options;
options.retry = {
retries: 0,
methods: [],
statusCodes: [],
errorCodes: []
};
if (is.nonEmptyObject(defaults) && retry !== false) {
options.retry = {...defaults.retry};
}
if (retry !== false) {
if (is.number(retry)) {
options.retry.retries = retry;
} else {
options.retry = {...options.retry, ...retry};
}
}
if (options.gotTimeout) {
options.retry.maxRetryAfter = Math.min(...[options.gotTimeout.request, options.gotTimeout.connection].filter(n => !is.nullOrUndefined(n)));
}
if (is.array(options.retry.methods)) {
options.retry.methods = new Set(options.retry.methods.map(method => method.toUpperCase()));
}
if (is.array(options.retry.statusCodes)) {
options.retry.statusCodes = new Set(options.retry.statusCodes);
}
if (is.array(options.retry.errorCodes)) {
options.retry.errorCodes = new Set(options.retry.errorCodes);
}
return options;
};
const normalize = (url, options, defaults) => {
if (is.plainObject(url)) {
options = {...url, ...options};
url = options.url || {};
delete options.url;
}
if (defaults) {
options = merge({}, defaults.options, options ? preNormalize(options, defaults.options) : {});
} else {
options = merge({}, preNormalize(options));
}
if (!is.string(url) && !is.object(url)) {
throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`);
}
if (is.string(url)) {
if (options.baseUrl) {
if (url.toString().startsWith('/')) {
url = url.toString().slice(1);
}
url = urlToOptions(new URL(url, options.baseUrl));
} else {
url = url.replace(/^unix:/, 'http://$&');
url = urlParseLax(url);
}
} else if (is(url) === 'URL') {
url = urlToOptions(url);
}
// Override both null/undefined with default protocol
options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options);
for (const hook of options.hooks.init) {
const called = hook(options);
if (is.promise(called)) {
throw new TypeError('The `init` hook must be a synchronous function');
}
}
const {baseUrl} = options;
Object.defineProperty(options, 'baseUrl', {
set: () => {
throw new Error('Failed to set baseUrl. Options are normalized already.');
},
get: () => baseUrl
});
const {query} = options;
if (is.nonEmptyString(query) || is.nonEmptyObject(query) || query instanceof URLSearchParams) {
if (!is.string(query)) {
options.query = (new URLSearchParams(query)).toString();
}
options.path = `${options.path.split('?')[0]}?${options.query}`;
delete options.query;
}
if (options.hostname === 'unix') {
const matches = /(.+?):(.+)/.exec(options.path);
if (matches) {
const [, socketPath, path] = matches;
options = {
...options,
socketPath,
path,
host: null
};
}
}
const {headers} = options;
for (const [key, value] of Object.entries(headers)) {
if (is.nullOrUndefined(value)) {
delete headers[key];
}
}
if (options.json && is.undefined(headers.accept)) {
headers.accept = 'application/json';
}
if (options.decompress && is.undefined(headers['accept-encoding'])) {
headers['accept-encoding'] = 'gzip, deflate';
}
const {body} = options;
if (is.nullOrUndefined(body)) {
options.method = options.method ? options.method.toUpperCase() : 'GET';
} else {
const isObject = is.object(body) && !is.buffer(body) && !is.nodeStream(body);
if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(options.form || options.json)) {
throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
}
if (options.json && !(isObject || is.array(body))) {
throw new TypeError('The `body` option must be an Object or Array when the `json` option is used');
}
if (options.form && !isObject) {
throw new TypeError('The `body` option must be an Object when the `form` option is used');
}
if (isFormData(body)) {
// Special case for https://github.com/form-data/form-data
headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`;
} else if (options.form) {
headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded';
options.body = (new URLSearchParams(body)).toString();
} else if (options.json) {
headers['content-type'] = headers['content-type'] || 'application/json';
options.body = JSON.stringify(body);
}
options.method = options.method ? options.method.toUpperCase() : 'POST';
}
if (!is.function(options.retry.retries)) {
const {retries} = options.retry;
options.retry.retries = (iteration, error) => {
if (iteration > retries) {
return 0;
}
if ((!error || !options.retry.errorCodes.has(error.code)) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
return 0;
}
if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
let after = Number(error.headers['retry-after']);
if (is.nan(after)) {
after = Date.parse(error.headers['retry-after']) - Date.now();
} else {
after *= 1000;
}
if (after > options.retry.maxRetryAfter) {
return 0;
}
return after;
}
if (error.statusCode === 413) {
return 0;
}
const noise = Math.random() * 100;
return ((2 ** (iteration - 1)) * 1000) + noise;
};
}
return options;
};
const reNormalize = options => normalize(urlLib.format(options), options);
module.exports = normalize;
module.exports.preNormalize = preNormalize;
module.exports.reNormalize = reNormalize;