"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Http = exports.EmptyResultError = exports.HttpError = void 0;
const tslib_1 = require("tslib");
const deepmerge_1 = tslib_1.__importDefault(require("deepmerge"));
const got_1 = tslib_1.__importStar(require("got"));
Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return got_1.RequestError; } });
const zod_1 = require("zod");
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 repository_1 = require("../cache/repository");
const clone_1 = require("../clone");
const hash_1 = require("../hash");
const result_1 = require("../result");
const url_1 = require("../url");
const auth_1 = require("./auth");
const hooks_1 = require("./hooks");
const host_rules_1 = require("./host-rules");
const queue_1 = require("./queue");
const retry_after_1 = require("./retry-after");
const throttle_1 = require("./throttle");
// TODO: refactor code to remove this (#9651)
require("./legacy");
class EmptyResultError extends Error {
}
exports.EmptyResultError = EmptyResultError;
// Copying will help to avoid circular structure
// and mutation of the cached response.
function copyResponse(response, deep) {
    const { body, statusCode, headers } = response;
    return deep
        ? {
            statusCode,
            body: body instanceof Buffer ? body.subarray() : (0, clone_1.clone)(body),
            headers: (0, clone_1.clone)(headers),
        }
        : {
            statusCode,
            body,
            headers,
        };
}
function applyDefaultHeaders(options) {
    const renovateVersion = expose_cjs_1.pkg.version;
    options.headers = {
        ...options.headers,
        'user-agent': process.env.RENOVATE_USER_AGENT ??
            `RenovateBot/${renovateVersion} (https://github.com/renovatebot/renovate)`,
    };
}
// Note on types:
// options.requestType can be either 'json' or 'buffer', but `T` should be
// `Buffer` in the latter case.
// We don't declare overload signatures because it's immediately wrapped by
// `request`.
async function gotTask(url, options, requestStats) {
    logger_1.logger.trace({ url, options }, 'got request');
    let duration = 0;
    let statusCode = 0;
    try {
        // Cheat the TS compiler using `as` to pick a specific overload.
        // Otherwise it doesn't typecheck.
        const resp = await (0, got_1.default)(url, { ...options, hooks: hooks_1.hooks });
        statusCode = resp.statusCode;
        duration =
            resp.timings.phases.total ??
                /* istanbul ignore next: can't be tested */ 0;
        return resp;
    }
    catch (error) {
        if (error instanceof got_1.RequestError) {
            statusCode =
                error.response?.statusCode ??
                    /* istanbul ignore next: can't be tested */ -1;
            duration =
                error.timings?.phases.total ??
                    /* istanbul ignore next: can't be tested */ -1;
            const method = options.method.toUpperCase();
            const code = error.code ?? /* istanbul ignore next */ 'UNKNOWN';
            const retryCount = error.request?.retryCount ?? /* istanbul ignore next */ -1;
            logger_1.logger.debug(`${method} ${url} = (code=${code}, statusCode=${statusCode} retryCount=${retryCount}, duration=${duration})`);
        }
        throw error;
    }
    finally {
        const httpRequests = memCache.get('http-requests') || [];
        httpRequests.push({ ...requestStats, duration, statusCode });
        memCache.set('http-requests', httpRequests);
    }
}
class Http {
    hostType;
    options;
    constructor(hostType, options = {}) {
        this.hostType = hostType;
        const retryLimit = process.env.NODE_ENV === 'test' ? 0 : 2;
        this.options = (0, deepmerge_1.default)(options, {
            context: { hostType },
            retry: {
                limit: retryLimit,
                maxRetryAfter: 0, // Don't rely on `got` retry-after handling, just let it fail and then we'll handle it
            },
        });
    }
    getThrottle(url) {
        return (0, throttle_1.getThrottle)(url);
    }
    async request(requestUrl, httpOptions) {
        let url = requestUrl.toString();
        if (httpOptions?.baseUrl) {
            url = (0, url_1.resolveBaseUrl)(httpOptions.baseUrl, url);
        }
        let options = (0, deepmerge_1.default)({
            method: 'get',
            ...this.options,
            hostType: this.hostType,
        }, httpOptions);
        logger_1.logger.trace(`HTTP request: ${options.method.toUpperCase()} ${url}`);
        const etagCache = httpOptions.etagCache && options.method === 'get'
            ? httpOptions.etagCache
            : null;
        if (etagCache) {
            options.headers = {
                ...options.headers,
                'If-None-Match': etagCache.etag,
            };
        }
        options.hooks = {
            beforeRedirect: [auth_1.removeAuthorization],
        };
        applyDefaultHeaders(options);
        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 memCacheKey = options.memCache !== false &&
            (options.method === 'get' || options.method === 'head')
            ? (0, hash_1.hash)(`got-${JSON.stringify({
                url,
                headers: options.headers,
                method: options.method,
            })}`)
            : null;
        let resPromise = null;
        // Cache GET requests unless memCache=false
        if (memCacheKey) {
            resPromise = memCache.get(memCacheKey);
        }
        // istanbul ignore else: no cache tests
        if (!resPromise) {
            if (httpOptions.repoCache) {
                const responseCache = (0, repository_1.getCache)().httpCache?.[url];
                // Prefer If-Modified-Since over If-None-Match
                if (responseCache?.['lastModified']) {
                    logger_1.logger.debug(`http cache: trying cached Last-Modified "${responseCache?.['lastModified']}" for ${url}`);
                    options.headers = {
                        ...options.headers,
                        'If-Modified-Since': responseCache['lastModified'],
                    };
                }
                else if (responseCache?.etag) {
                    logger_1.logger.debug(`http cache: trying cached etag "${responseCache.etag}" for ${url}`);
                    options.headers = {
                        ...options.headers,
                        'If-None-Match': responseCache.etag,
                    };
                }
            }
            const startTime = Date.now();
            const httpTask = () => {
                const queueDuration = Date.now() - startTime;
                return gotTask(url, options, {
                    method: options.method,
                    url,
                    queueDuration,
                });
            };
            const throttle = this.getThrottle(url);
            const throttledTask = throttle
                ? () => throttle.add(httpTask)
                : httpTask;
            const queue = (0, queue_1.getQueue)(url);
            const queuedTask = queue
                ? () => queue.add(throttledTask)
                : 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 = copyResponse(res, deepCopyNeeded);
            resCopy.authorization = !!options?.headers?.authorization;
            if (httpOptions.repoCache) {
                const cache = (0, repository_1.getCache)();
                cache.httpCache ??= {};
                if (resCopy.statusCode === 200 &&
                    (resCopy.headers?.etag ?? resCopy.headers['last-modified'])) {
                    logger_1.logger.debug(`http cache: saving ${url} (etag=${resCopy.headers.etag}, lastModified=${resCopy.headers['last-modified']})`);
                    cache.httpCache[url] = {
                        etag: resCopy.headers.etag,
                        httpResponse: copyResponse(res, deepCopyNeeded),
                        lastModified: resCopy.headers['last-modified'],
                        timeStamp: new Date().toISOString(),
                    };
                }
                if (resCopy.statusCode === 304 && cache.httpCache[url]?.httpResponse) {
                    logger_1.logger.debug(`http cache: Using cached response: ${url} from ${cache.httpCache[url].timeStamp}`);
                    const cacheCopy = copyResponse(cache.httpCache[url].httpResponse, deepCopyNeeded);
                    cacheCopy.authorization = !!options?.headers?.authorization;
                    return cacheCopy;
                }
            }
            return resCopy;
        }
        catch (err) {
            const { abortOnError, abortIgnoreStatusCodes } = options;
            if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) {
                throw new external_host_error_1.ExternalHostError(err);
            }
            throw err;
        }
    }
    get(url, options = {}) {
        return this.request(url, options);
    }
    head(url, options = {}) {
        return this.request(url, { ...options, method: 'head' });
    }
    getBuffer(url, options = {}) {
        return this.request(url, {
            ...options,
            responseType: 'buffer',
        });
    }
    async requestJson(method, { url, httpOptions: requestOptions, schema, }) {
        const { body, ...httpOptions } = { ...requestOptions };
        const opts = {
            ...httpOptions,
            method,
            responseType: 'json',
        };
        // signal that we expect a json response
        opts.headers = {
            accept: 'application/json',
            ...opts.headers,
        };
        if (body) {
            opts.json = body;
        }
        const res = await this.request(url, opts);
        const etagCacheHit = httpOptions.etagCache && res.statusCode === 304
            ? (0, clone_1.clone)(httpOptions.etagCache.data)
            : null;
        if (!schema) {
            if (etagCacheHit) {
                res.body = etagCacheHit;
            }
            return res;
        }
        if (etagCacheHit) {
            res.body = etagCacheHit;
        }
        else {
            res.body = await 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.get(url, {
            headers: {
                Accept: 'text/plain',
            },
            ...opt,
        });
    }
    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));
    }
    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) {
        // TODO: fix types (#22198)
        let combinedOptions = {
            method: 'get',
            ...this.options,
            hostType: this.hostType,
            ...options,
        };
        let resolvedUrl = url;
        // istanbul ignore else: needs test
        if (options?.baseUrl) {
            resolvedUrl = (0, url_1.resolveBaseUrl)(options.baseUrl, url);
        }
        applyDefaultHeaders(combinedOptions);
        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 got_1.default.stream(resolvedUrl, combinedOptions);
    }
}
exports.Http = Http;
//# sourceMappingURL=index.js.map