UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

349 lines (348 loc) • 11.2 kB
import { HOST_DISABLED } from "../../constants/error-messages.js"; import { pkg } from "../../expose.js"; import { get, set } from "../cache/memory/index.js"; import { getEnv } from "../env.js"; import { GlobalConfig } from "../../config/global.js"; import { logger } from "../../logger/index.js"; import { isHttpUrl, parseUrl, resolveBaseUrl } from "../url.js"; import { parseSingleYaml } from "../yaml.js"; import { compile } from "../template/index.js"; import { hash } from "../hash.js"; import { ExternalHostError } from "../../types/errors/external-host-error.js"; import { Toml } from "../schema-utils/index.js"; import { acquireLock } from "../mutex.js"; import { ObsoleteCacheHitLogger } from "../stats.js"; import { fetch, normalize, stream } from "./got.js"; import { Result } from "../result.js"; import { applyAuthorization } from "./auth.js"; import { applyHostRule, findMatchingRule } from "./host-rules.js"; import { getQueue } from "./queue.js"; import { getRetryAfter, wrapWithRetry } from "./retry-after.js"; import { getThrottle } from "./throttle.js"; import { copyResponse } from "./util.js"; import { isPlainObject, isUndefined } from "@sindresorhus/is"; import { ZodType } from "zod/v4"; import merge from "deepmerge"; //#region lib/util/http/http.ts function applyDefaultHeaders(options) { const userAgentTemplate = GlobalConfig.get("userAgent"); options.headers = { ...options.headers, "user-agent": compile(userAgentTemplate, { renovateVersion: pkg.version }) }; } var HttpBase = class { options; get baseUrl() {} hostType; constructor(hostType, options = {}) { this.hostType = hostType; const retryLimit = getEnv().NODE_ENV === "test" ? 0 : 2; this.options = merge(options, { method: "get", context: { hostType }, retry: { calculateDelay: (retryObject) => this.calculateRetryDelay(retryObject), limit: retryLimit, maxRetryAfter: 0 } }, { isMergeableObject: isPlainObject }); } async request(requestUrl, httpOptions) { const resolvedUrl = this.resolveUrl(requestUrl, httpOptions); const url = resolvedUrl.toString(); this.processOptions(resolvedUrl, httpOptions); let options = merge({ ...this.options, hostType: this.hostType }, httpOptions, { isMergeableObject: isPlainObject }); const method = options.method.toLowerCase(); const isReadMethod = ["head", "get"].includes(method); logger.trace(`HTTP request: ${method.toUpperCase()} ${url}`); applyDefaultHeaders(options); if (isUndefined(options.readOnly) && isReadMethod) options.readOnly = true; const hostRule = findMatchingRule(url, options); options = applyHostRule(url, options, hostRule); if (options.enabled === false) { logger.debug(`Host is disabled - rejecting request. HostUrl: ${url}`); throw new Error(HOST_DISABLED); } options = applyAuthorization(options); const timeout = options.timeout ?? 6e4; options.timeout = timeout; let cacheProvider; if (isReadMethod && options.cacheProvider) cacheProvider = options.cacheProvider; const memCacheKey = !process.env.RENOVATE_X_DISABLE_HTTP_MEMCACHE && !cacheProvider && options.memCache !== false && isReadMethod ? hash(`got-${JSON.stringify({ url, headers: options.headers, method })}`) : null; let resPromise = null; if (memCacheKey) { resPromise = get(memCacheKey); /* v8 ignore next: temporary code */ if (resPromise && !cacheProvider) ObsoleteCacheHitLogger.write(url); } // v8 ignore else -- TODO: add test #40625 if (!resPromise) { if (cacheProvider) await cacheProvider.setCacheHeaders(method, url, options); const startTime = Date.now(); const httpTask = async () => { let releaseLock; if (isReadMethod) releaseLock = await acquireLock(`${options.method} ${url}`, "http-mutex", timeout * 2); try { const cachedResponse = await cacheProvider?.bypassServer(options.method, url); if (cachedResponse) return cachedResponse; const queueMs = Date.now() - startTime; return fetch(url, this._normalizeOptions(options), { queueMs }); } finally { releaseLock?.(); } }; const throttle = getThrottle(url); const throttledTask = throttle ? () => throttle.add(httpTask) : httpTask; const queue = getQueue(url); const queuedTask = queue ? () => queue.add(throttledTask) : throttledTask; const { maxRetryAfter = 60 } = hostRule; resPromise = wrapWithRetry(queuedTask, url, getRetryAfter, maxRetryAfter); if (memCacheKey) set(memCacheKey, resPromise); } try { const res = await resPromise; const resCopy = copyResponse(res, !!memCacheKey && res.statusCode !== 304); resCopy.authorization = !!options?.headers?.authorization; if (cacheProvider) return await cacheProvider.wrapServerResponse(method, url, resCopy); return resCopy; } catch (err) { const { abortOnError, abortIgnoreStatusCodes } = options; if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) throw new ExternalHostError(err); const staleResponse = await cacheProvider?.bypassServer(method, url, true); if (staleResponse) { logger.debug({ err }, `Request error: returning stale cache instead for ${url}`); return staleResponse; } this.handleError(requestUrl, httpOptions, err); } } _normalizeOptions(options) { return normalize(options, this.extraOptions()); } /** * Returns Renovate extra options which needs to be removed before passing to got. * @returns extra Renovate options. */ extraOptions() { return [ "baseUrl", "cacheProvider", "readOnly" ]; } processOptions(_url, _options) {} handleError(_url, _httpOptions, err) { throw err; } resolveUrl(requestUrl, options = void 0) { let url = requestUrl; if (url instanceof URL) return url; const baseUrl = options?.baseUrl ?? this.baseUrl; if (baseUrl) url = resolveBaseUrl(baseUrl, url); const parsedUrl = parseUrl(url); if (!parsedUrl || !isHttpUrl(parsedUrl)) { 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 = {}) { 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 }; 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 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 = 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(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.wrap(this.getYaml(url, httpOptions, schema)); else res = Result.wrap(this.getYaml(url, schema)); return res.transform((response) => 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.wrap(this.requestJson("get", args)).transform((response) => 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); // v8 ignore else -- TODO: add test #40625 if (isUndefined(combinedOptions.readOnly) && ["head", "get"].includes(combinedOptions.method)) combinedOptions.readOnly = true; const hostRule = findMatchingRule(url, combinedOptions); combinedOptions = applyHostRule(resolvedUrl, combinedOptions, hostRule); if (combinedOptions.enabled === false) throw new Error(HOST_DISABLED); combinedOptions = applyAuthorization(combinedOptions); return stream(resolvedUrl, this._normalizeOptions(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 Toml.pipe(schema).parseAsync(res.body); else res.body = await Toml.parseAsync(res.body); return res; } }; //#endregion export { HttpBase }; //# sourceMappingURL=http.js.map