renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
370 lines • 13.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpBase = void 0;
exports.applyDefaultHeaders = applyDefaultHeaders;
const tslib_1 = require("tslib");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const deepmerge_1 = tslib_1.__importDefault(require("deepmerge"));
const zod_1 = require("zod");
const global_1 = require("../../config/global");
const error_messages_1 = require("../../constants/error-messages");
const expose_cjs_1 = require("../../expose.cjs");
const logger_1 = require("../../logger");
const external_host_error_1 = require("../../types/errors/external-host-error");
const memCache = tslib_1.__importStar(require("../cache/memory"));
const env_1 = require("../env");
const hash_1 = require("../hash");
const result_1 = require("../result");
const schema_utils_1 = require("../schema-utils");
const stats_1 = require("../stats");
const url_1 = require("../url");
const yaml_1 = require("../yaml");
const auth_1 = require("./auth");
const got_1 = require("./got");
const host_rules_1 = require("./host-rules");
const queue_1 = require("./queue");
const retry_after_1 = require("./retry-after");
const throttle_1 = require("./throttle");
const util_1 = require("./util");
function applyDefaultHeaders(options) {
const renovateVersion = expose_cjs_1.pkg.version;
options.headers = {
...options.headers,
'user-agent': global_1.GlobalConfig.get('userAgent') ??
`RenovateBot/${renovateVersion} (https://github.com/renovatebot/renovate)`,
};
}
class HttpBase {
hostType;
options;
get baseUrl() {
return undefined;
}
constructor(hostType, options = {}) {
this.hostType = hostType;
const retryLimit = (0, env_1.getEnv)().NODE_ENV === 'test' ? 0 : 2;
this.options = (0, deepmerge_1.default)(options, {
method: 'get',
context: { hostType },
retry: {
calculateDelay: (retryObject) => this.calculateRetryDelay(retryObject),
limit: retryLimit,
maxRetryAfter: 0, // Don't rely on `got` retry-after handling, just let it fail and then we'll handle it
},
}, { isMergeableObject: is_1.default.plainObject });
}
async request(requestUrl, httpOptions) {
const resolvedUrl = this.resolveUrl(requestUrl, httpOptions);
const url = resolvedUrl.toString();
this.processOptions(resolvedUrl, httpOptions);
let options = (0, deepmerge_1.default)({
...this.options,
hostType: this.hostType,
}, httpOptions, { isMergeableObject: is_1.default.plainObject });
logger_1.logger.trace(`HTTP request: ${options.method.toUpperCase()} ${url}`);
options.hooks = {
beforeRedirect: [auth_1.removeAuthorization],
};
applyDefaultHeaders(options);
if (is_1.default.undefined(options.readOnly) &&
['head', 'get'].includes(options.method)) {
options.readOnly = true;
}
const hostRule = (0, host_rules_1.findMatchingRule)(url, options);
options = (0, host_rules_1.applyHostRule)(url, options, hostRule);
if (options.enabled === false) {
logger_1.logger.debug(`Host is disabled - rejecting request. HostUrl: ${url}`);
throw new Error(error_messages_1.HOST_DISABLED);
}
options = (0, auth_1.applyAuthorization)(options);
options.timeout ??= 60000;
const { cacheProvider } = options;
const memCacheKey = !process.env.RENOVATE_X_DISABLE_HTTP_MEMCACHE &&
!cacheProvider &&
options.memCache !== false &&
(options.method === 'get' || options.method === 'head')
? (0, hash_1.hash)(`got-${JSON.stringify({
url,
headers: options.headers,
method: options.method,
})}`)
: null;
const cachedResponse = await cacheProvider?.bypassServer(url);
if (cachedResponse) {
return cachedResponse;
}
let resPromise = null;
// Cache GET requests unless memCache=false
if (memCacheKey) {
resPromise = memCache.get(memCacheKey);
/* v8 ignore start: temporary code */
if (resPromise && !cacheProvider) {
stats_1.ObsoleteCacheHitLogger.write(url);
}
/* v8 ignore stop: temporary code */
}
if (!resPromise) {
if (cacheProvider) {
await cacheProvider.setCacheHeaders(url, options);
}
const startTime = Date.now();
const httpTask = () => {
const queueMs = Date.now() - startTime;
return (0, got_1.fetch)(url, options, { queueMs });
};
const throttle = (0, throttle_1.getThrottle)(url);
const throttledTask = throttle ? () => throttle.add(httpTask) : httpTask;
const queue = (0, queue_1.getQueue)(url);
const queuedTask = queue
? () => queue.add(throttledTask, { throwOnTimeout: true })
: throttledTask;
const { maxRetryAfter = 60 } = hostRule;
resPromise = (0, retry_after_1.wrapWithRetry)(queuedTask, url, retry_after_1.getRetryAfter, maxRetryAfter);
if (memCacheKey) {
memCache.set(memCacheKey, resPromise);
}
}
try {
const res = await resPromise;
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
const resCopy = (0, util_1.copyResponse)(res, deepCopyNeeded);
resCopy.authorization = !!options?.headers?.authorization;
if (cacheProvider) {
return await cacheProvider.wrapServerResponse(url, resCopy);
}
return resCopy;
}
catch (err) {
const { abortOnError, abortIgnoreStatusCodes } = options;
if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) {
throw new external_host_error_1.ExternalHostError(err);
}
const staleResponse = await cacheProvider?.bypassServer(url, true);
if (staleResponse) {
logger_1.logger.debug({ err }, `Request error: returning stale cache instead for ${url}`);
return staleResponse;
}
this.handleError(requestUrl, httpOptions, err);
}
}
processOptions(_url, _options) {
// noop
}
handleError(_url, _httpOptions, err) {
throw err;
}
resolveUrl(requestUrl, options) {
let url = requestUrl;
if (url instanceof URL) {
// already a aboslute URL
return url;
}
const baseUrl = options?.baseUrl ?? this.baseUrl;
if (baseUrl) {
url = (0, url_1.resolveBaseUrl)(baseUrl, url);
}
const parsedUrl = (0, url_1.parseUrl)(url);
if (!parsedUrl || !(0, url_1.isHttpUrl)(parsedUrl)) {
logger_1.logger.error({ url: requestUrl, baseUrl, resolvedUrl: url }, 'Request Error: cannot parse url');
throw new Error('Invalid URL');
}
return parsedUrl;
}
calculateRetryDelay({ computedValue }) {
return computedValue;
}
get(url, options = {}) {
return this.request(url, options);
}
head(url, options = {}) {
// to complex to validate
return this.request(url, {
...options,
responseType: 'text',
method: 'head',
});
}
getText(url, options = {}) {
return this.request(url, { ...options, responseType: 'text' });
}
getBuffer(url, options = {}) {
return this.request(url, { ...options, responseType: 'buffer' });
}
requestJsonUnsafe(method, { url, httpOptions: requestOptions }) {
const { body: json, ...httpOptions } = { ...requestOptions };
const opts = {
...httpOptions,
method,
};
// signal that we expect a json response
opts.headers = {
accept: 'application/json',
...opts.headers,
};
if (json) {
opts.json = json;
}
return this.request(url, { ...opts, responseType: 'json' });
}
async requestJson(method, options) {
const res = await this.requestJsonUnsafe(method, options);
if (options.schema) {
res.body = await options.schema.parseAsync(res.body);
}
return res;
}
resolveArgs(arg1, arg2, arg3) {
const res = { url: arg1 };
if (arg2 instanceof zod_1.ZodType) {
res.schema = arg2;
}
else if (arg2) {
res.httpOptions = arg2;
}
if (arg3) {
res.schema = arg3;
}
return res;
}
async getPlain(url, options) {
const opt = options ?? {};
return await this.getText(url, {
headers: {
Accept: 'text/plain',
},
...opt,
});
}
/**
* @deprecated use `getYaml` instead
*/
async getYamlUnchecked(url, options) {
const res = await this.getText(url, options);
const body = (0, yaml_1.parseSingleYaml)(res.body);
return { ...res, body };
}
async getYaml(arg1, arg2, arg3) {
const url = arg1;
let schema;
let httpOptions;
if (arg3) {
schema = arg3;
httpOptions = arg2;
}
else {
schema = arg2;
}
const opts = {
...httpOptions,
method: 'get',
};
const res = await this.getText(url, opts);
const body = await schema.parseAsync((0, yaml_1.parseSingleYaml)(res.body));
return { ...res, body };
}
getYamlSafe(arg1, arg2, arg3) {
const url = arg1;
let schema;
let httpOptions;
if (arg3) {
schema = arg3;
httpOptions = arg2;
}
else {
schema = arg2;
}
let res;
if (httpOptions) {
res = result_1.Result.wrap(this.getYaml(url, httpOptions, schema));
}
else {
res = result_1.Result.wrap(this.getYaml(url, schema));
}
return res.transform((response) => result_1.Result.ok(response.body));
}
/**
* Request JSON and return the response without any validation.
*
* The usage of this method is discouraged, please use `getJson` instead.
*
* If you're new to Zod schema validation library:
* - consult the [documentation of Zod library](https://github.com/colinhacks/zod?tab=readme-ov-file#basic-usage)
* - search the Renovate codebase for 'zod' module usage
* - take a look at the `schema-utils.ts` file for Renovate-specific schemas and utilities
*/
getJsonUnchecked(url, options) {
return this.requestJson('get', { url, httpOptions: options });
}
getJson(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return this.requestJson('get', args);
}
getJsonSafe(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return result_1.Result.wrap(this.requestJson('get', args)).transform((response) => result_1.Result.ok(response.body));
}
/**
* @deprecated use `head` instead
*/
headJson(url, httpOptions) {
return this.requestJson('head', { url, httpOptions });
}
postJson(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return this.requestJson('post', args);
}
putJson(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return this.requestJson('put', args);
}
patchJson(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return this.requestJson('patch', args);
}
deleteJson(arg1, arg2, arg3) {
const args = this.resolveArgs(arg1, arg2, arg3);
return this.requestJson('delete', args);
}
stream(url, options) {
let combinedOptions = {
...this.options,
hostType: this.hostType,
...options,
method: 'get',
};
const resolvedUrl = this.resolveUrl(url, options).toString();
applyDefaultHeaders(combinedOptions);
if (is_1.default.undefined(combinedOptions.readOnly) &&
['head', 'get'].includes(combinedOptions.method)) {
combinedOptions.readOnly = true;
}
const hostRule = (0, host_rules_1.findMatchingRule)(url, combinedOptions);
combinedOptions = (0, host_rules_1.applyHostRule)(resolvedUrl, combinedOptions, hostRule);
if (combinedOptions.enabled === false) {
throw new Error(error_messages_1.HOST_DISABLED);
}
combinedOptions = (0, auth_1.applyAuthorization)(combinedOptions);
return (0, got_1.stream)(resolvedUrl, combinedOptions);
}
async getToml(arg1, arg2, arg3) {
const { url, schema, httpOptions } = this.resolveArgs(arg1, arg2, arg3);
const opts = {
...httpOptions,
method: 'get',
headers: {
'Content-Type': 'application/toml',
...httpOptions?.headers,
},
};
const res = await this.getText(url, opts);
if (schema) {
res.body = await schema_utils_1.Toml.pipe(schema).parseAsync(res.body);
}
else {
res.body = (await schema_utils_1.Toml.parseAsync(res.body));
}
return res;
}
}
exports.HttpBase = HttpBase;
//# sourceMappingURL=http.js.map