mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-13 15:36:25 +00:00
8785f70b93
* fix(core/http): fallback to github /gitlab hosttype * fix: remove generic host rule * Update lib/util/http/index.ts Co-authored-by: Rhys Arkins <rhys@arkins.net>
304 lines
8.1 KiB
TypeScript
304 lines
8.1 KiB
TypeScript
import crypto from 'crypto';
|
|
import merge from 'deepmerge';
|
|
import got, { Options, Response } from 'got';
|
|
import { HOST_DISABLED } from '../../constants/error-messages';
|
|
import { logger } from '../../logger';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error';
|
|
import * as memCache from '../cache/memory';
|
|
import { clone } from '../clone';
|
|
import { resolveBaseUrl } from '../url';
|
|
import { applyAuthorization, removeAuthorization } from './auth';
|
|
import { applyHostRules } from './host-rules';
|
|
import { getQueue } from './queue';
|
|
import type {
|
|
GotJSONOptions,
|
|
GotOptions,
|
|
OutgoingHttpHeaders,
|
|
RequestStats,
|
|
} from './types';
|
|
|
|
// TODO: refactor code to remove this (#9651)
|
|
import './legacy';
|
|
|
|
export interface HttpOptions {
|
|
body?: any;
|
|
username?: string;
|
|
password?: string;
|
|
baseUrl?: string;
|
|
headers?: OutgoingHttpHeaders;
|
|
|
|
/**
|
|
* Do not use authentication
|
|
*/
|
|
noAuth?: boolean;
|
|
|
|
throwHttpErrors?: boolean;
|
|
useCache?: boolean;
|
|
}
|
|
|
|
export interface HttpPostOptions extends HttpOptions {
|
|
body: unknown;
|
|
}
|
|
|
|
export interface InternalHttpOptions extends HttpOptions {
|
|
json?: Record<string, unknown>;
|
|
responseType?: 'json' | 'buffer';
|
|
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
|
}
|
|
|
|
export interface HttpResponse<T = string> {
|
|
statusCode: number;
|
|
body: T;
|
|
headers: any;
|
|
authorization?: boolean;
|
|
}
|
|
|
|
function cloneResponse<T extends Buffer | string | any>(
|
|
response: HttpResponse<T>
|
|
): HttpResponse<T> {
|
|
const { body, statusCode, headers } = response;
|
|
// clone body and headers so that the cached result doesn't get accidentally mutated
|
|
// Don't use json clone for buffers
|
|
return {
|
|
statusCode,
|
|
body: body instanceof Buffer ? (body.slice() as T) : clone<T>(body),
|
|
headers: clone(headers),
|
|
authorization: !!response.authorization,
|
|
};
|
|
}
|
|
|
|
function applyDefaultHeaders(options: Options): void {
|
|
let renovateVersion = 'unknown';
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
renovateVersion = require('../../../package.json').version; // eslint-disable-line global-require
|
|
} catch (err) /* istanbul ignore next */ {
|
|
logger.debug({ err }, 'Error getting renovate version');
|
|
}
|
|
// eslint-disable-next-line no-param-reassign
|
|
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 gotRoutine<T>(
|
|
url: string,
|
|
options: GotOptions,
|
|
requestStats: Partial<RequestStats>
|
|
): Promise<Response<T>> {
|
|
logger.trace({ url, options }, 'got request');
|
|
|
|
// Cheat the TS compiler using `as` to pick a specific overload.
|
|
// Otherwise it doesn't typecheck.
|
|
const resp = await got<T>(url, options as GotJSONOptions);
|
|
const duration =
|
|
resp.timings.phases.total || /* istanbul ignore next: can't be tested */ 0;
|
|
|
|
const httpRequests = memCache.get('http-requests') || [];
|
|
httpRequests.push({ ...requestStats, duration });
|
|
memCache.set('http-requests', httpRequests);
|
|
|
|
return resp;
|
|
}
|
|
|
|
export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
|
|
private options?: GotOptions;
|
|
|
|
constructor(private hostType: string, options?: HttpOptions) {
|
|
this.options = merge<GotOptions>(options, { context: { hostType } });
|
|
}
|
|
|
|
protected async request<T>(
|
|
requestUrl: string | URL,
|
|
httpOptions?: InternalHttpOptions
|
|
): Promise<HttpResponse<T> | null> {
|
|
let url = requestUrl.toString();
|
|
if (httpOptions?.baseUrl) {
|
|
url = resolveBaseUrl(httpOptions.baseUrl, url);
|
|
}
|
|
|
|
let options: GotOptions = merge<GotOptions>(
|
|
{
|
|
method: 'get',
|
|
...this.options,
|
|
hostType: this.hostType,
|
|
},
|
|
httpOptions
|
|
);
|
|
|
|
if (process.env.NODE_ENV === 'test') {
|
|
options.retry = 0;
|
|
}
|
|
options.hooks = {
|
|
beforeRedirect: [removeAuthorization],
|
|
};
|
|
|
|
applyDefaultHeaders(options);
|
|
|
|
options = applyHostRules(url, options);
|
|
if (options.enabled === false) {
|
|
throw new Error(HOST_DISABLED);
|
|
}
|
|
options = applyAuthorization(options);
|
|
|
|
const cacheKey = crypto
|
|
.createHash('md5')
|
|
.update(
|
|
'got-' +
|
|
JSON.stringify({
|
|
url,
|
|
headers: options.headers,
|
|
method: options.method,
|
|
})
|
|
)
|
|
.digest('hex');
|
|
|
|
let resPromise;
|
|
|
|
// Cache GET requests unless useCache=false
|
|
if (
|
|
['get', 'head'].includes(options.method) &&
|
|
options.useCache !== false
|
|
) {
|
|
resPromise = memCache.get(cacheKey);
|
|
}
|
|
|
|
// istanbul ignore else: no cache tests
|
|
if (!resPromise) {
|
|
const startTime = Date.now();
|
|
const queueTask = (): Promise<Response<T>> => {
|
|
const queueDuration = Date.now() - startTime;
|
|
return gotRoutine(url, options, {
|
|
method: options.method,
|
|
url,
|
|
queueDuration,
|
|
});
|
|
};
|
|
const queue = getQueue(url);
|
|
resPromise = queue?.add(queueTask) ?? queueTask();
|
|
if (options.method === 'get') {
|
|
memCache.set(cacheKey, resPromise); // always set if it's a get
|
|
}
|
|
}
|
|
|
|
try {
|
|
const res = await resPromise;
|
|
res.authorization = !!options?.headers?.authorization;
|
|
return cloneResponse(res);
|
|
} catch (err) {
|
|
const { abortOnError, abortIgnoreStatusCodes } = options;
|
|
if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) {
|
|
throw new ExternalHostError(err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
get(url: string, options: HttpOptions = {}): Promise<HttpResponse> {
|
|
return this.request<string>(url, options);
|
|
}
|
|
|
|
head(url: string, options: HttpOptions = {}): Promise<HttpResponse> {
|
|
return this.request<string>(url, { ...options, method: 'head' });
|
|
}
|
|
|
|
protected requestBuffer(
|
|
url: string | URL,
|
|
httpOptions?: InternalHttpOptions
|
|
): Promise<HttpResponse<Buffer> | null> {
|
|
return this.request<Buffer>(url, {
|
|
...httpOptions,
|
|
responseType: 'buffer',
|
|
});
|
|
}
|
|
|
|
getBuffer(
|
|
url: string,
|
|
options: HttpOptions = {}
|
|
): Promise<HttpResponse<Buffer> | null> {
|
|
return this.requestBuffer(url, options);
|
|
}
|
|
|
|
private async requestJson<T = unknown>(
|
|
url: string,
|
|
options: InternalHttpOptions
|
|
): Promise<HttpResponse<T>> {
|
|
const { body, ...jsonOptions } = options;
|
|
if (body) {
|
|
jsonOptions.json = body;
|
|
}
|
|
const res = await this.request<T>(url, {
|
|
...jsonOptions,
|
|
responseType: 'json',
|
|
});
|
|
return { ...res, body: res.body };
|
|
}
|
|
|
|
getJson<T = unknown>(
|
|
url: string,
|
|
options?: GetOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options });
|
|
}
|
|
|
|
headJson<T = unknown>(
|
|
url: string,
|
|
options?: GetOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options, method: 'head' });
|
|
}
|
|
|
|
postJson<T = unknown>(
|
|
url: string,
|
|
options?: PostOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options, method: 'post' });
|
|
}
|
|
|
|
putJson<T = unknown>(
|
|
url: string,
|
|
options?: PostOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options, method: 'put' });
|
|
}
|
|
|
|
patchJson<T = unknown>(
|
|
url: string,
|
|
options?: PostOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options, method: 'patch' });
|
|
}
|
|
|
|
deleteJson<T = unknown>(
|
|
url: string,
|
|
options?: PostOptions
|
|
): Promise<HttpResponse<T>> {
|
|
return this.requestJson<T>(url, { ...options, method: 'delete' });
|
|
}
|
|
|
|
stream(url: string, options?: HttpOptions): NodeJS.ReadableStream {
|
|
const combinedOptions: any = {
|
|
method: 'get',
|
|
...this.options,
|
|
hostType: this.hostType,
|
|
...options,
|
|
};
|
|
|
|
// istanbul ignore else: needs test
|
|
if (options?.baseUrl) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
url = resolveBaseUrl(options.baseUrl, url);
|
|
}
|
|
|
|
applyDefaultHeaders(combinedOptions);
|
|
return got.stream(url, combinedOptions);
|
|
}
|
|
}
|