mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-24 22:48:06 +00:00
4dc30ea9bc
* fix(http): share concurrent identical cache provider requests Identical requests that use a cache provider and run at the same time now share one in-flight request instead of each fetching from the server. Callers that join an in-flight request get a deep copy of the response. This replaces the per-URL mutex, which only covered the cache check and was released before the fetch finished. Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * fix(http): reuse cache providers for npm and typst lookups The npm and typst datasources now use one module-level cache provider, so concurrent lookups of the same package share one request. The request key is only hashed for GET and HEAD requests and includes `throwHttpErrors`. Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> Co-Authored-By: Claude Sonnet 5.5 <michael.kriese+claude-code@mend.io> * fix(http): don't share requests that differ in response type Add `responseType` and `responseBecomesConfig` to the key of shared requests, and test that requests with different headers aren't shared and that a shared response is copied for every caller. Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> Co-Authored-By: Claude Sonnet 5.5 <michael.kriese+claude-code@mend.io> * fix(http): keep in-flight requests per Http class and copy binary bodies Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
818 lines
24 KiB
TypeScript
818 lines
24 KiB
TypeScript
import { isPlainObject, isUndefined } from '@sindresorhus/is';
|
|
import merge from 'deepmerge';
|
|
import type { Options, OptionsInit, RetryObject } from 'got';
|
|
import type { SetRequired } from 'type-fest';
|
|
import type { z } from 'zod/v4';
|
|
import { ZodType } from 'zod/v4';
|
|
import { GlobalConfig } from '../../config/global.ts';
|
|
import { HOST_DISABLED } from '../../constants/error-messages.ts';
|
|
import { logger } from '../../logger/index.ts';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
|
|
import type { CombinedHostRule } from '../../types/index.ts';
|
|
import { coerceArray } from '../array.ts';
|
|
import * as memCache from '../cache/memory/index.ts';
|
|
import { getEnv } from '../env.ts';
|
|
import { hash } from '../hash.ts';
|
|
import { coerceObject } from '../object.ts';
|
|
import { type AsyncResult, Result } from '../result.ts';
|
|
import { Toml } from '../schema-utils/index.ts';
|
|
import { ObsoleteCacheHitLogger } from '../stats.ts';
|
|
import { compile } from '../template/index.ts';
|
|
import { isHttpUrl, parseUrl, resolveBaseUrl } from '../url.ts';
|
|
import { parseSingleYaml } from '../yaml.ts';
|
|
import { applyAuthorization } from './auth.ts';
|
|
import type { HttpCacheProvider } from './cache/types.ts';
|
|
import { fetch, normalize, stream } from './got.ts';
|
|
import { applyHostGuard } from './host-guard.ts';
|
|
import { applyHostRule, findMatchingRule } from './host-rules.ts';
|
|
import { getQueue } from './queue.ts';
|
|
import { getRetryAfter, wrapWithRetry } from './retry-after.ts';
|
|
import { getThrottle } from './throttle.ts';
|
|
import type {
|
|
GotOptions,
|
|
GotTask,
|
|
HttpMethod,
|
|
HttpOptions,
|
|
HttpResponse,
|
|
SafeJsonError,
|
|
} from './types.ts';
|
|
import { copyResponse } from './util.ts';
|
|
|
|
export interface InternalJsonUnsafeOptions<
|
|
Opts extends HttpOptions = HttpOptions,
|
|
> {
|
|
url: string | URL;
|
|
httpOptions?: Opts;
|
|
}
|
|
|
|
export interface InternalJsonOptions<
|
|
Opts extends HttpOptions,
|
|
ResT = unknown,
|
|
Schema extends ZodType<ResT, any> = ZodType<ResT, any>,
|
|
> extends InternalJsonUnsafeOptions<Opts> {
|
|
schema?: Schema;
|
|
}
|
|
|
|
export type InternalGotOptions = SetRequired<GotOptions, 'method' | 'context'>;
|
|
|
|
export interface InternalHttpOptions extends HttpOptions {
|
|
json?: HttpOptions['body'];
|
|
|
|
method?: HttpMethod;
|
|
|
|
parseJson?: Options['parseJson'];
|
|
}
|
|
|
|
const inFlightRequests = new WeakMap<
|
|
HttpCacheProvider,
|
|
Map<string, Promise<HttpResponse<unknown>>>
|
|
>();
|
|
|
|
function getInFlightRequest(
|
|
cacheProvider: HttpCacheProvider,
|
|
requestKey: string,
|
|
): Promise<HttpResponse<unknown>> | null {
|
|
return inFlightRequests.get(cacheProvider)?.get(requestKey) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Registers a cache provider request as in flight until it settles.
|
|
*/
|
|
function setInFlightRequest(
|
|
cacheProvider: HttpCacheProvider,
|
|
requestKey: string,
|
|
request: Promise<HttpResponse<unknown>>,
|
|
): Promise<HttpResponse<unknown>> {
|
|
const requests = inFlightRequests.get(cacheProvider) ?? new Map();
|
|
inFlightRequests.set(cacheProvider, requests);
|
|
const pending = request.finally(() => requests.delete(requestKey));
|
|
requests.set(requestKey, pending);
|
|
return pending;
|
|
}
|
|
|
|
export function applyDefaultHeaders(options: OptionsInit): void {
|
|
const userAgentTemplate = GlobalConfig.get('userAgent');
|
|
options.headers = {
|
|
...options.headers,
|
|
'user-agent': compile(userAgentTemplate, {}),
|
|
};
|
|
}
|
|
|
|
export abstract class HttpBase<
|
|
JSONOpts extends HttpOptions = HttpOptions,
|
|
Opts extends HttpOptions = HttpOptions,
|
|
> {
|
|
private readonly options: InternalGotOptions;
|
|
|
|
protected get baseUrl(): string | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
protected hostType: string;
|
|
|
|
constructor(hostType: string, options: HttpOptions = {}) {
|
|
this.hostType = hostType;
|
|
const retryLimit = getEnv().NODE_ENV === 'test' ? 0 : 2;
|
|
this.options = merge<InternalGotOptions>(
|
|
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
|
|
enforceRetryRules: false,
|
|
},
|
|
},
|
|
{ isMergeableObject: isPlainObject },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Builds the final got options for a request to a given `url`
|
|
*
|
|
* This merges instance options, applies default headers, host rules and authorization, and rejects requests to disabled hosts.
|
|
*
|
|
* Both the `request()` and `stream()` paths must use this single entry point so that policy applied here covers every outbound request.
|
|
*/
|
|
private prepareOptions(
|
|
resolvedUrl: URL,
|
|
httpOptions: InternalHttpOptions,
|
|
): {
|
|
options: InternalGotOptions & InternalHttpOptions;
|
|
hostRule: CombinedHostRule;
|
|
} {
|
|
const url = resolvedUrl.toString();
|
|
|
|
let options = merge<InternalGotOptions, InternalHttpOptions>(
|
|
{
|
|
...this.options,
|
|
hostType: this.hostType,
|
|
},
|
|
httpOptions,
|
|
{ isMergeableObject: isPlainObject },
|
|
);
|
|
|
|
applyDefaultHeaders(options);
|
|
|
|
if (
|
|
isUndefined(options.readOnly) &&
|
|
['head', 'get'].includes(options.method.toLowerCase())
|
|
) {
|
|
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);
|
|
|
|
// enforced here so it runs before anything else - including cache lookups - can act on the URL, and for the stream path too
|
|
const guard = applyHostGuard(
|
|
resolvedUrl,
|
|
options.hostType,
|
|
hostRule.internalHostGrant,
|
|
options.responseBecomesConfig,
|
|
);
|
|
options.dnsLookup = guard.dnsLookup;
|
|
options.hooks = {
|
|
...options.hooks,
|
|
beforeRedirect: [
|
|
...coerceArray(options.hooks?.beforeRedirect),
|
|
guard.beforeRedirect,
|
|
],
|
|
};
|
|
if (guard.beforeRequest) {
|
|
options.hooks.beforeRequest = [
|
|
...coerceArray(options.hooks.beforeRequest),
|
|
guard.beforeRequest,
|
|
];
|
|
}
|
|
|
|
return { options, hostRule };
|
|
}
|
|
|
|
private async request(
|
|
requestUrl: string | URL,
|
|
httpOptions: InternalHttpOptions,
|
|
): Promise<HttpResponse>;
|
|
private async request(
|
|
requestUrl: string | URL,
|
|
httpOptions: InternalHttpOptions & { responseType: 'text' },
|
|
): Promise<HttpResponse>;
|
|
private async request(
|
|
requestUrl: string | URL,
|
|
httpOptions: InternalHttpOptions & { responseType: 'buffer' },
|
|
): Promise<HttpResponse<Uint8Array>>;
|
|
private async request<T = unknown>(
|
|
requestUrl: string | URL,
|
|
httpOptions: InternalHttpOptions & { responseType: 'json' },
|
|
): Promise<HttpResponse<T>>;
|
|
|
|
private async request(
|
|
requestUrl: string | URL,
|
|
httpOptions: InternalHttpOptions,
|
|
): Promise<HttpResponse<unknown>> {
|
|
const resolvedUrl = this.resolveUrl(requestUrl, httpOptions);
|
|
const url = resolvedUrl.toString();
|
|
|
|
this.processOptions(resolvedUrl, httpOptions);
|
|
|
|
const { options, hostRule } = this.prepareOptions(resolvedUrl, httpOptions);
|
|
|
|
const method = options.method.toLowerCase();
|
|
const isReadMethod = ['head', 'get'].includes(method);
|
|
|
|
logger.trace(`HTTP request: ${method.toUpperCase()} ${url}`);
|
|
|
|
const timeout = options.timeout ?? 60000;
|
|
options.timeout = timeout;
|
|
|
|
let cacheProvider: HttpCacheProvider | undefined;
|
|
if (isReadMethod && options.cacheProvider) {
|
|
cacheProvider = options.cacheProvider;
|
|
}
|
|
|
|
const requestKey = isReadMethod
|
|
? hash(
|
|
`got-${JSON.stringify({
|
|
url,
|
|
headers: options.headers,
|
|
method,
|
|
responseType: options.responseType,
|
|
responseBecomesConfig: options.responseBecomesConfig,
|
|
throwHttpErrors: options.throwHttpErrors !== false,
|
|
})}`,
|
|
)
|
|
: '';
|
|
|
|
const memCacheKey =
|
|
!process.env.RENOVATE_X_DISABLE_HTTP_MEMCACHE &&
|
|
!cacheProvider &&
|
|
options.memCache !== false &&
|
|
isReadMethod
|
|
? requestKey
|
|
: null;
|
|
|
|
// each Http subclass runs its own handleResponse, so it doesn't join another's request
|
|
const inFlightKey = `${this.constructor.name}-${requestKey}`;
|
|
|
|
let resPromise: Promise<HttpResponse<unknown>> | null = null;
|
|
let sharedResponse = false;
|
|
|
|
// Cache GET requests unless memCache=false
|
|
if (memCacheKey) {
|
|
resPromise = memCache.get(memCacheKey);
|
|
|
|
/* v8 ignore next: temporary code */
|
|
if (resPromise && !cacheProvider) {
|
|
ObsoleteCacheHitLogger.write(url);
|
|
}
|
|
} else if (cacheProvider) {
|
|
resPromise = getInFlightRequest(cacheProvider, inFlightKey);
|
|
sharedResponse = !!resPromise;
|
|
}
|
|
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (!resPromise) {
|
|
const startTime = Date.now();
|
|
const fetchTask: GotTask = () => {
|
|
const queueMs = Date.now() - startTime;
|
|
return fetch(url, this._normalizeOptions(options), { queueMs });
|
|
};
|
|
|
|
const throttle = getThrottle(url);
|
|
const throttledTask = throttle
|
|
? () => throttle.add(fetchTask)
|
|
: fetchTask;
|
|
|
|
const queue = getQueue(url);
|
|
const queuedTask = queue ? () => queue.add(throttledTask) : throttledTask;
|
|
|
|
const { maxRetryAfter = 60 } = hostRule;
|
|
function httpTask(): Promise<HttpResponse<unknown>> {
|
|
return wrapWithRetry(queuedTask, url, getRetryAfter, maxRetryAfter);
|
|
}
|
|
|
|
// cached responses don't hit the server, so they skip the throttle and queue
|
|
const cachedTask = async (
|
|
provider: HttpCacheProvider,
|
|
): Promise<HttpResponse<unknown>> => {
|
|
let res = await provider.bypassServer<unknown>(options.method, url);
|
|
if (!res) {
|
|
await provider.setCacheHeaders(method, url, options);
|
|
res = await httpTask();
|
|
}
|
|
const resCopy = copyResponse(res, false);
|
|
resCopy.authorization = !!options?.headers?.authorization;
|
|
this.handleResponse(resolvedUrl, resCopy);
|
|
return await provider.wrapServerResponse(method, url, resCopy);
|
|
};
|
|
|
|
if (cacheProvider) {
|
|
resPromise = setInFlightRequest(
|
|
cacheProvider,
|
|
inFlightKey,
|
|
cachedTask(cacheProvider),
|
|
);
|
|
} else {
|
|
resPromise = httpTask();
|
|
}
|
|
|
|
if (memCacheKey) {
|
|
memCache.set(memCacheKey, resPromise);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const res = await resPromise;
|
|
|
|
if (cacheProvider) {
|
|
if (!sharedResponse) {
|
|
return res;
|
|
}
|
|
// identical requests share one response, so each caller gets its own copy
|
|
const resCopy = copyResponse(res, true);
|
|
resCopy.authorization = res.authorization;
|
|
return resCopy;
|
|
}
|
|
|
|
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
|
|
const resCopy = copyResponse(res, deepCopyNeeded);
|
|
resCopy.authorization = !!options?.headers?.authorization;
|
|
|
|
this.handleResponse(resolvedUrl, resCopy);
|
|
|
|
return resCopy;
|
|
} catch (err) {
|
|
const { abortOnError, abortIgnoreStatusCodes } = options;
|
|
if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) {
|
|
throw new ExternalHostError(err);
|
|
}
|
|
|
|
const staleResponse = await cacheProvider?.bypassServer<
|
|
string | Uint8Array
|
|
>(method, url, true);
|
|
if (staleResponse) {
|
|
logger.debug(
|
|
{ err },
|
|
`Request error: returning stale cache instead for ${url}`,
|
|
);
|
|
return staleResponse;
|
|
}
|
|
|
|
this.handleError(requestUrl, httpOptions, err);
|
|
}
|
|
}
|
|
|
|
private _normalizeOptions<T extends OptionsInit>(options: T): T {
|
|
return normalize(options, this.extraOptions());
|
|
}
|
|
|
|
/**
|
|
* Returns Renovate extra options which needs to be removed before passing to got.
|
|
* @returns extra Renovate options.
|
|
*/
|
|
protected extraOptions(): readonly string[] {
|
|
return [
|
|
'baseUrl',
|
|
'cacheProvider',
|
|
'readOnly',
|
|
'responseBecomesConfig',
|
|
] as (keyof HttpOptions)[];
|
|
}
|
|
|
|
protected processOptions(_url: URL, _options: InternalHttpOptions): void {
|
|
// noop
|
|
}
|
|
|
|
protected handleError(
|
|
_url: string | URL,
|
|
_httpOptions: HttpOptions,
|
|
err: Error,
|
|
): never {
|
|
throw err;
|
|
}
|
|
|
|
protected handleResponse(_url: URL, _res: HttpResponse<unknown>): void {
|
|
// noop
|
|
}
|
|
|
|
resolveUrl(requestUrl: string | URL, options?: HttpOptions): URL {
|
|
let url = requestUrl;
|
|
|
|
if (url instanceof URL) {
|
|
// already a aboslute 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;
|
|
}
|
|
|
|
protected calculateRetryDelay({ computedValue }: RetryObject): number {
|
|
return computedValue;
|
|
}
|
|
|
|
get(
|
|
url: string,
|
|
options: HttpOptions = {},
|
|
): Promise<HttpResponse<string | Uint8Array>> {
|
|
return this.request(url, options);
|
|
}
|
|
|
|
head(url: string, options: HttpOptions = {}): Promise<HttpResponse<never>> {
|
|
// to complex to validate
|
|
return this.request(url, {
|
|
...options,
|
|
responseType: 'text',
|
|
method: 'head',
|
|
}) as Promise<HttpResponse<never>>;
|
|
}
|
|
|
|
getText(url: string | URL, options: HttpOptions = {}): Promise<HttpResponse> {
|
|
return this.request(url, { ...options, responseType: 'text' });
|
|
}
|
|
|
|
getBuffer(
|
|
url: string | URL,
|
|
options: HttpOptions = {},
|
|
): Promise<HttpResponse<Uint8Array>> {
|
|
return this.request(url, { ...options, responseType: 'buffer' });
|
|
}
|
|
|
|
protected requestJsonUnsafe<ResT>(
|
|
method: HttpMethod,
|
|
{ url, httpOptions: requestOptions }: InternalJsonUnsafeOptions<JSONOpts>,
|
|
): Promise<HttpResponse<ResT>> {
|
|
const { body: json, ...httpOptions } = { ...requestOptions };
|
|
const opts: InternalHttpOptions = {
|
|
...httpOptions,
|
|
method,
|
|
};
|
|
// signal that we expect a json response
|
|
opts.headers = {
|
|
accept: 'application/json',
|
|
...opts.headers,
|
|
};
|
|
if (json) {
|
|
opts.json = json;
|
|
}
|
|
return this.request<ResT>(url, { ...opts, responseType: 'json' });
|
|
}
|
|
|
|
private async requestJson<
|
|
ResT,
|
|
Schema extends ZodType<ResT, any> = ZodType<ResT, any>,
|
|
>(
|
|
method: HttpMethod,
|
|
options: InternalJsonOptions<JSONOpts, ResT, Schema>,
|
|
): Promise<HttpResponse<ResT>> {
|
|
const res = await this.requestJsonUnsafe<ResT>(method, options);
|
|
|
|
if (options.schema) {
|
|
res.body = await options.schema.parseAsync(res.body);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
private resolveArgs<ResT = unknown>(
|
|
arg1: string,
|
|
arg2: JSONOpts | ZodType<ResT> | undefined,
|
|
arg3: ZodType<ResT> | undefined,
|
|
): InternalJsonOptions<JSONOpts, ResT> {
|
|
const res: InternalJsonOptions<JSONOpts, ResT> = { 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: string, options?: Opts): Promise<HttpResponse> {
|
|
const opt = coerceObject(options);
|
|
return await this.getText(url, {
|
|
headers: {
|
|
Accept: 'text/plain',
|
|
},
|
|
...opt,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @deprecated use `getYaml` instead
|
|
*/
|
|
async getYamlUnchecked<ResT>(
|
|
url: string,
|
|
options?: Opts,
|
|
): Promise<HttpResponse<ResT>> {
|
|
const res = await this.getText(url, options);
|
|
const body = parseSingleYaml<ResT>(res.body);
|
|
return { ...res, body };
|
|
}
|
|
|
|
async getYaml<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
async getYaml<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
options: Opts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
async getYaml<Schema extends ZodType<any, any, any>>(
|
|
arg1: string,
|
|
arg2?: Opts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>> {
|
|
const url = arg1;
|
|
let schema: Schema;
|
|
let httpOptions: Opts | undefined;
|
|
if (arg3) {
|
|
schema = arg3;
|
|
httpOptions = arg2 as Opts;
|
|
} else {
|
|
schema = arg2 as Schema;
|
|
}
|
|
|
|
const opts: InternalHttpOptions = {
|
|
...httpOptions,
|
|
method: 'get',
|
|
};
|
|
|
|
const res = await this.getText(url, opts);
|
|
const body = await schema.parseAsync(parseSingleYaml(res.body));
|
|
return { ...res, body };
|
|
}
|
|
|
|
getYamlSafe<
|
|
ResT extends NonNullable<unknown>,
|
|
Schema extends ZodType<ResT> = ZodType<ResT>,
|
|
>(url: string, schema: Schema): AsyncResult<z.infer<Schema>, SafeJsonError>;
|
|
getYamlSafe<
|
|
ResT extends NonNullable<unknown>,
|
|
Schema extends ZodType<ResT> = ZodType<ResT>,
|
|
>(
|
|
url: string,
|
|
options: Opts,
|
|
schema: Schema,
|
|
): AsyncResult<z.infer<Schema>, SafeJsonError>;
|
|
getYamlSafe<
|
|
ResT extends NonNullable<unknown>,
|
|
Schema extends ZodType<ResT> = ZodType<ResT>,
|
|
>(
|
|
arg1: string,
|
|
arg2: Opts | Schema,
|
|
arg3?: Schema,
|
|
): AsyncResult<ResT, SafeJsonError> {
|
|
const url = arg1;
|
|
let schema: Schema;
|
|
let httpOptions: Opts | undefined;
|
|
if (arg3) {
|
|
schema = arg3;
|
|
httpOptions = arg2 as Opts;
|
|
} else {
|
|
schema = arg2 as Schema;
|
|
}
|
|
|
|
let res: AsyncResult<HttpResponse<ResT>, SafeJsonError>;
|
|
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<ResT = unknown>(
|
|
url: string,
|
|
options?: JSONOpts,
|
|
): Promise<HttpResponse<ResT>> {
|
|
return this.requestJson<ResT>('get', { url, httpOptions: options });
|
|
}
|
|
|
|
/**
|
|
* Request JSON with a Zod schema for the response,
|
|
* throwing an error if the response is not valid.
|
|
*
|
|
* @param url
|
|
* @param schema Zod schema for the response
|
|
*/
|
|
getJson<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
getJson<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
getJson<Schema extends ZodType<any, any, any>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>> {
|
|
const args = this.resolveArgs<z.infer<Schema>>(arg1, arg2, arg3);
|
|
return this.requestJson<z.infer<Schema>>('get', args);
|
|
}
|
|
|
|
/**
|
|
* Request JSON with a Zod schema for the response,
|
|
* wrapping response data in a `Result` class.
|
|
*
|
|
* @param url
|
|
* @param schema Zod schema for the response
|
|
*/
|
|
getJsonSafe<ResT extends NonNullable<unknown>, Schema extends ZodType<ResT>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): AsyncResult<z.infer<Schema>, SafeJsonError>;
|
|
getJsonSafe<ResT extends NonNullable<unknown>, Schema extends ZodType<ResT>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): AsyncResult<z.infer<Schema>, SafeJsonError>;
|
|
getJsonSafe<ResT extends NonNullable<unknown>, Schema extends ZodType<ResT>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): AsyncResult<ResT, SafeJsonError> {
|
|
const args = this.resolveArgs<ResT>(arg1, arg2, arg3);
|
|
return Result.wrap(this.requestJson<ResT>('get', args)).transform(
|
|
(response) => Result.ok(response.body),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @deprecated use `head` instead
|
|
*/
|
|
headJson(url: string, httpOptions?: JSONOpts): Promise<HttpResponse<never>> {
|
|
return this.requestJson<never>('head', { url, httpOptions });
|
|
}
|
|
|
|
postJson<T>(url: string, options?: JSONOpts): Promise<HttpResponse<T>>;
|
|
postJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
postJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
postJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<T>> {
|
|
const args = this.resolveArgs(arg1, arg2, arg3);
|
|
return this.requestJson<T>('post', args);
|
|
}
|
|
|
|
putJson<T>(url: string, options?: JSONOpts): Promise<HttpResponse<T>>;
|
|
putJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
putJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
putJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<T>> {
|
|
const args = this.resolveArgs<T>(arg1, arg2, arg3);
|
|
return this.requestJson<T>('put', args);
|
|
}
|
|
|
|
patchJson<T>(url: string, options?: JSONOpts): Promise<HttpResponse<T>>;
|
|
patchJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
patchJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
patchJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<T>> {
|
|
const args = this.resolveArgs(arg1, arg2, arg3);
|
|
return this.requestJson<T>('patch', args);
|
|
}
|
|
|
|
deleteJson<T>(url: string, options?: JSONOpts): Promise<HttpResponse<T>>;
|
|
deleteJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
deleteJson<T, Schema extends ZodType<T> = ZodType<T>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
deleteJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<T>> {
|
|
const args = this.resolveArgs(arg1, arg2, arg3);
|
|
return this.requestJson<T>('delete', args);
|
|
}
|
|
|
|
stream(url: string, options?: HttpOptions): NodeJS.ReadableStream {
|
|
const resolvedUrl = this.resolveUrl(url, options);
|
|
|
|
const { options: combinedOptions } = this.prepareOptions(resolvedUrl, {
|
|
...options,
|
|
method: 'get',
|
|
});
|
|
|
|
return stream(
|
|
resolvedUrl.toString(),
|
|
this._normalizeOptions(combinedOptions),
|
|
);
|
|
}
|
|
|
|
async getToml<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
schema?: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
async getToml<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
options: JSONOpts,
|
|
schema: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>>;
|
|
async getToml<Schema extends ZodType<any, any, any>>(
|
|
arg1: string,
|
|
arg2?: JSONOpts | Schema,
|
|
arg3?: Schema,
|
|
): Promise<HttpResponse<z.infer<Schema>>> {
|
|
const { url, schema, httpOptions } = this.resolveArgs<z.infer<Schema>>(
|
|
arg1,
|
|
arg2,
|
|
arg3,
|
|
);
|
|
|
|
const opts: InternalHttpOptions = {
|
|
...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)) as z.infer<Schema>;
|
|
}
|
|
|
|
return res as HttpResponse<z.infer<Schema>>;
|
|
}
|
|
}
|