diff --git a/lib/config/presets/gitlab/index.ts b/lib/config/presets/gitlab/index.ts index 2af74c99d094cf0ede9395c33e2098e5ee0fac04..bfb1768c04860928145c265dd52af9ee9379073a 100644 --- a/lib/config/presets/gitlab/index.ts +++ b/lib/config/presets/gitlab/index.ts @@ -43,7 +43,7 @@ export async function fetchJSONFile( } url += `projects/${urlEncodedRepo}/repository/files/${urlEncodedPkgName}/raw${ref}`; logger.trace({ url }, `Preset URL`); - res = await gitlabApi.get(url); + res = await gitlabApi.getText(url); } catch (err) { if (err instanceof ExternalHostError) { throw err; diff --git a/lib/config/presets/http/index.ts b/lib/config/presets/http/index.ts index 90bd220f14e7384bc16b8543edb2b876fe18d511..e8aa66d516ae74b398327878f8ace97703d1391b 100644 --- a/lib/config/presets/http/index.ts +++ b/lib/config/presets/http/index.ts @@ -20,7 +20,7 @@ export async function getPreset({ } try { - response = await http.get(url); + response = await http.getText(url); } catch (err) { if (err instanceof ExternalHostError) { throw err; diff --git a/lib/modules/datasource/artifactory/index.ts b/lib/modules/datasource/artifactory/index.ts index c9389ab3630a10e6b6213dc9859eef30c3e5c1a8..e11b74522c3cb252d021f61cfeb0ddbfef1c03e7 100644 --- a/lib/modules/datasource/artifactory/index.ts +++ b/lib/modules/datasource/artifactory/index.ts @@ -50,7 +50,7 @@ export class ArtifactoryDatasource extends Datasource { releases: [], }; try { - const response = await this.http.get(url); + const response = await this.http.getText(url); const body = parse(response.body, { blockTextElements: { script: true, diff --git a/lib/modules/datasource/crate/index.ts b/lib/modules/datasource/crate/index.ts index d16585261c4e3221a56a4216a64bf998f853c53d..453e0c5f8d8e4ea839a77fd751d5fb8f2f3cc398 100644 --- a/lib/modules/datasource/crate/index.ts +++ b/lib/modules/datasource/crate/index.ts @@ -193,7 +193,7 @@ export class CrateDatasource extends Datasource { ); const crateUrl = joinUrlParts(baseUrl, ...packageSuffix); try { - return (await this.http.get(crateUrl)).body; + return (await this.http.getText(crateUrl)).body; } catch (err) { this.handleGenericErrors(err); } diff --git a/lib/modules/datasource/custom/formats/html.ts b/lib/modules/datasource/custom/formats/html.ts index 6195b1b609f212dce7441d56a2be03b075dfb488..e79d104f83880ca6779e784305da793b8eb54128 100644 --- a/lib/modules/datasource/custom/formats/html.ts +++ b/lib/modules/datasource/custom/formats/html.ts @@ -30,7 +30,7 @@ function extractLinks(content: string): ReleaseResult { export class HtmlFetcher implements CustomDatasourceFetcher { async fetch(http: Http, registryURL: string): Promise<unknown> { - const response = await http.get(registryURL, { + const response = await http.getText(registryURL, { headers: { Accept: 'text/html', }, diff --git a/lib/modules/datasource/custom/formats/yaml.ts b/lib/modules/datasource/custom/formats/yaml.ts index 541eb2a9d47bd6a91019da84d38ed73f02715d56..2784b9af699aec3a1bc0de762e6f05790405d6ea 100644 --- a/lib/modules/datasource/custom/formats/yaml.ts +++ b/lib/modules/datasource/custom/formats/yaml.ts @@ -5,7 +5,7 @@ import type { CustomDatasourceFetcher } from './types'; export class YamlFetcher implements CustomDatasourceFetcher { async fetch(http: Http, registryURL: string): Promise<unknown> { - const response = await http.get(registryURL); + const response = await http.getText(registryURL); return parseSingleYaml(response.body); } diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 192bc6fdb2591037bc81229945d90bd7be8c351f..c99dfea0ab34ad021c9bc78aef6e0c02dd56b2ff 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -187,7 +187,7 @@ export class DebDatasource extends Datasource { */ private async fetchInReleaseFile(baseReleaseUrl: string): Promise<string> { const inReleaseUrl = joinUrlParts(baseReleaseUrl, 'InRelease'); - const response = await this.http.get(inReleaseUrl); + const response = await this.http.getText(inReleaseUrl); return response.body; } diff --git a/lib/modules/datasource/docker/index.ts b/lib/modules/datasource/docker/index.ts index 5631990a9fa49ec53ff148837de2e90ad5da1c80..c206b764c09846e9ab88791f9401cd820c2bee59 100644 --- a/lib/modules/datasource/docker/index.ts +++ b/lib/modules/datasource/docker/index.ts @@ -98,7 +98,7 @@ export class DockerDatasource extends Datasource { registryHost: string, dockerRepository: string, tag: string, - mode: 'head' | 'get' = 'get', + mode: 'head' | 'getText' = 'getText', ): Promise<HttpResponse | null> { logger.debug( `getManifestResponse(${registryHost}, ${dockerRepository}, ${tag}, ${mode})`, diff --git a/lib/modules/datasource/github-release-attachments/index.ts b/lib/modules/datasource/github-release-attachments/index.ts index 2b95ca53fea59b06099a67676bc6e81c6837e5a6..42b0f6368239a06ccaa55101858138627e99e722 100644 --- a/lib/modules/datasource/github-release-attachments/index.ts +++ b/lib/modules/datasource/github-release-attachments/index.ts @@ -65,7 +65,7 @@ export class GithubReleaseAttachmentsDatasource extends Datasource { (a: GithubRestAsset) => a.size < 5 * 1024, ); for (const asset of smallAssets) { - const res = await this.http.get(asset.browser_download_url); + const res = await this.http.getText(asset.browser_download_url); for (const line of res.body.split(newlineRegex)) { const [lineDigest, lineFilename] = line.split(regEx(/\s+/), 2); if (lineDigest === digest) { @@ -162,7 +162,7 @@ export class GithubReleaseAttachmentsDatasource extends Datasource { current, next, ); - const res = await this.http.get(releaseAsset.browser_download_url); + const res = await this.http.getText(releaseAsset.browser_download_url); for (const line of res.body.split(newlineRegex)) { const [lineDigest, lineFn] = line.split(regEx(/\s+/), 2); if (lineFn === releaseFilename) { diff --git a/lib/modules/datasource/go/base.ts b/lib/modules/datasource/go/base.ts index ba3c74800afc4e5f9bf3c7f469a18dde9af8d224..060f7526d204aa9956b43663206a49938113c6e2 100644 --- a/lib/modules/datasource/go/base.ts +++ b/lib/modules/datasource/go/base.ts @@ -135,7 +135,7 @@ export class BaseGoDatasource { ): Promise<DataSource | null> { const goModuleUrl = goModule.replace(/\.git(\/[a-z0-9/]*)?$/, ''); const pkgUrl = `https://${goModuleUrl}?go-get=1`; - const { body: html } = await BaseGoDatasource.http.get(pkgUrl); + const { body: html } = await BaseGoDatasource.http.getText(pkgUrl); const goSourceHeader = BaseGoDatasource.goSourceHeader(html, goModule); if (goSourceHeader) { diff --git a/lib/modules/datasource/go/releases-goproxy.ts b/lib/modules/datasource/go/releases-goproxy.ts index e3f87682a3478f280d976b80fce71970b7cbb40f..f94a7cce4c362fd37aa5a8a62b39e8fd307be72c 100644 --- a/lib/modules/datasource/go/releases-goproxy.ts +++ b/lib/modules/datasource/go/releases-goproxy.ts @@ -134,7 +134,7 @@ export class GoProxyDatasource extends Datasource { '@v', 'list', ); - const { body } = await this.http.get(url); + const { body } = await this.http.getText(url); return filterMap(body.split(newlineRegex), (str) => { if (!is.nonEmptyStringAndNotWhitespace(str)) { return null; diff --git a/lib/modules/datasource/golang-version/index.ts b/lib/modules/datasource/golang-version/index.ts index 6d355a60aeb4bc94a87e2bc0b4fbfc05136b8aad..65bf1514cf2cc650c0f8df29cbde52eaed491132 100644 --- a/lib/modules/datasource/golang-version/index.ts +++ b/lib/modules/datasource/golang-version/index.ts @@ -60,7 +60,7 @@ export class GolangVersionDatasource extends Datasource { '/HEAD/internal/history/release.go', ); - const response = await this.http.get(golangVersionsUrl); + const response = await this.http.getText(golangVersionsUrl); const lines = response.body.split(lineTerminationRegex); diff --git a/lib/modules/datasource/hexpm-bob/index.ts b/lib/modules/datasource/hexpm-bob/index.ts index 780bbfb91fcb820a848aeb692f039d3119aebd59..89f111efe66637c27618a96cbf0808ba5fc52e54 100644 --- a/lib/modules/datasource/hexpm-bob/index.ts +++ b/lib/modules/datasource/hexpm-bob/index.ts @@ -59,7 +59,7 @@ export class HexpmBobDatasource extends Datasource { ...HexpmBobDatasource.getPackageDetails(packageType), }; try { - const { body } = await this.http.get(url); + const { body } = await this.http.getText(url); result.releases = body .split('\n') .map((line) => line.trim()) diff --git a/lib/modules/datasource/maven/util.spec.ts b/lib/modules/datasource/maven/util.spec.ts index dd9891abc615fb5275b7c1dee62b0f68b31e29b5..0bb61bd46494a33f2fa4be3795667a0c93ee2ae6 100644 --- a/lib/modules/datasource/maven/util.spec.ts +++ b/lib/modules/datasource/maven/util.spec.ts @@ -68,7 +68,7 @@ describe('modules/datasource/maven/util', () => { describe('downloadHttpProtocol', () => { it('returns empty for HOST_DISABLED error', async () => { const http = partial<Http>({ - get: () => Promise.reject(httpError({ message: HOST_DISABLED })), + getText: () => Promise.reject(httpError({ message: HOST_DISABLED })), }); const res = await downloadHttpProtocol(http, 'some://'); expect(res.unwrap()).toEqual({ @@ -79,7 +79,7 @@ describe('modules/datasource/maven/util', () => { it('returns empty for host error', async () => { const http = partial<Http>({ - get: () => Promise.reject(httpError({ code: 'ETIMEDOUT' })), + getText: () => Promise.reject(httpError({ code: 'ETIMEDOUT' })), }); const res = await downloadHttpProtocol(http, 'some://'); expect(res.unwrap()).toEqual({ @@ -90,7 +90,7 @@ describe('modules/datasource/maven/util', () => { it('returns empty for temporary error', async () => { const http = partial<Http>({ - get: () => Promise.reject(httpError({ code: 'ECONNRESET' })), + getText: () => Promise.reject(httpError({ code: 'ECONNRESET' })), }); const res = await downloadHttpProtocol(http, 'some://'); expect(res.unwrap()).toEqual({ @@ -101,7 +101,7 @@ describe('modules/datasource/maven/util', () => { it('returns empty for connection error', async () => { const http = partial<Http>({ - get: () => Promise.reject(httpError({ code: 'ECONNREFUSED' })), + getText: () => Promise.reject(httpError({ code: 'ECONNREFUSED' })), }); const res = await downloadHttpProtocol(http, 'some://'); expect(res.unwrap()).toEqual({ @@ -112,7 +112,7 @@ describe('modules/datasource/maven/util', () => { it('returns empty for unsupported error', async () => { const http = partial<Http>({ - get: () => + getText: () => Promise.reject(httpError({ name: 'UnsupportedProtocolError' })), }); const res = await downloadHttpProtocol(http, 'some://'); diff --git a/lib/modules/datasource/maven/util.ts b/lib/modules/datasource/maven/util.ts index 696836fd41f913cc4ef5b213a711131f7fa90c0b..b4c50a797b85c9a3780331df0ad1b8c86deedde2 100644 --- a/lib/modules/datasource/maven/util.ts +++ b/lib/modules/datasource/maven/util.ts @@ -74,7 +74,7 @@ export async function downloadHttpProtocol( ): Promise<MavenFetchResult> { const url = pkgUrl.toString(); const fetchResult = await Result.wrap<HttpResponse, Error>( - http.get(url, opts), + http.getText(url, opts), ) .transform((res): MavenFetchSuccess => { const result: MavenFetchSuccess = { data: res.body }; diff --git a/lib/modules/datasource/nuget/v2.ts b/lib/modules/datasource/nuget/v2.ts index 888be53280970e751e5a2e5cf346a8dee47be9b5..04c1e8eade94c32fa5bead8dfec73102b20932c8 100644 --- a/lib/modules/datasource/nuget/v2.ts +++ b/lib/modules/datasource/nuget/v2.ts @@ -26,7 +26,7 @@ export class NugetV2Api { )}/FindPackagesById()?id=%27${pkgName}%27&$select=Version,IsLatestVersion,ProjectUrl,Published`; while (pkgUrlList !== null) { // typescript issue - const pkgVersionsListRaw = await http.get(pkgUrlList); + const pkgVersionsListRaw = await http.getText(pkgUrlList); const pkgVersionsListDoc = new XmlDocument(pkgVersionsListRaw.body); const pkgInfoList = pkgVersionsListDoc.childrenNamed('entry'); diff --git a/lib/modules/datasource/nuget/v3.ts b/lib/modules/datasource/nuget/v3.ts index 2492c02d70a674de8985f5a554d1ade5a0e8eb46..96a813686b5161cc1db5001357603bd7a4737990 100644 --- a/lib/modules/datasource/nuget/v3.ts +++ b/lib/modules/datasource/nuget/v3.ts @@ -209,7 +209,7 @@ export class NugetV3Api { // TODO: types (#22198) latestStable }/${pkgName.toLowerCase()}.nuspec`; - const metaresult = await http.get(nuspecUrl); + const metaresult = await http.getText(nuspecUrl); const nuspec = new XmlDocument(metaresult.body); const sourceUrl = nuspec.valueWithPath('metadata.repository@url'); if (sourceUrl) { diff --git a/lib/modules/datasource/pod/index.ts b/lib/modules/datasource/pod/index.ts index 5ebd9c6cf06829c723aed2ab0e0c0bd453736017..d7b0a684a9add532ac1647d9a9d84a9c5b2bd5e8 100644 --- a/lib/modules/datasource/pod/index.ts +++ b/lib/modules/datasource/pod/index.ts @@ -109,7 +109,7 @@ export class PodDatasource extends Datasource { packageName: string, ): Promise<string | null> { try { - const resp = await this.http.get(url); + const resp = await this.http.getText(url); if (resp?.body) { return resp.body; } diff --git a/lib/modules/datasource/pypi/index.ts b/lib/modules/datasource/pypi/index.ts index 48fda7604991f10ebe8345d1b153176351108bb7..1a86701e17c9ed58d41d5c63bcbfbe062a11f19a 100644 --- a/lib/modules/datasource/pypi/index.ts +++ b/lib/modules/datasource/pypi/index.ts @@ -259,7 +259,7 @@ export class PypiDatasource extends Datasource { ); const dependency: ReleaseResult = { releases: [] }; const headers = await this.getAuthHeaders(lookupUrl); - const response = await this.http.get(lookupUrl, { headers }); + const response = await this.http.getText(lookupUrl, { headers }); const dep = response?.body; if (!dep) { logger.trace({ dependency: packageName }, 'pip package not found'); diff --git a/lib/modules/datasource/ruby-version/index.ts b/lib/modules/datasource/ruby-version/index.ts index edf3f52c46d6d47dbd9517d8123c432cf69b03b7..69d4b77dde9d9993153ccf34df1e747d17ce742c 100644 --- a/lib/modules/datasource/ruby-version/index.ts +++ b/lib/modules/datasource/ruby-version/index.ts @@ -40,7 +40,7 @@ export class RubyVersionDatasource extends Datasource { // TODO: types (#22198) const rubyVersionsUrl = `${registryUrl}en/downloads/releases/`; try { - const response = await this.http.get(rubyVersionsUrl); + const response = await this.http.getText(rubyVersionsUrl); const root = parse(response.body); const rows = diff --git a/lib/modules/datasource/rubygems/index.ts b/lib/modules/datasource/rubygems/index.ts index aa52a008791d4d0a29899739c15a02ea54180a39..9cb920352739812df94e2c8476e91102c5969aed 100644 --- a/lib/modules/datasource/rubygems/index.ts +++ b/lib/modules/datasource/rubygems/index.ts @@ -118,7 +118,7 @@ export class RubygemsDatasource extends Datasource { packageName: string, ): AsyncResult<ReleaseResult, Error | ZodError> { const url = joinUrlParts(registryUrl, '/info', packageName); - return Result.wrap(this.http.get(url)) + return Result.wrap(this.http.getText(url)) .transform(({ body }) => body) .parse(GemInfo); } diff --git a/lib/modules/datasource/rubygems/versions-endpoint-cache.ts b/lib/modules/datasource/rubygems/versions-endpoint-cache.ts index 5a6ca75ff8297a963d415679ec400516634bbd48..79ecc45f14583637443f74217ccb979bd6d6c61d 100644 --- a/lib/modules/datasource/rubygems/versions-endpoint-cache.ts +++ b/lib/modules/datasource/rubygems/versions-endpoint-cache.ts @@ -209,7 +209,7 @@ export class VersionsEndpointCache { try { const url = `${registryUrl}/versions`; const opts: HttpOptions = { headers: { 'Accept-Encoding': 'gzip' } }; - const { body } = await this.http.get(url, opts); + const { body } = await this.http.getText(url, opts); return parseFullBody(body); } catch (err) { if (err instanceof HttpError && err.response?.statusCode === 404) { @@ -233,7 +233,7 @@ export class VersionsEndpointCache { ['Range']: `bytes=${startByte}-`, }, }; - const { statusCode, body } = await this.http.get(url, opts); + const { statusCode, body } = await this.http.getText(url, opts); /** * Rubygems will return the full body instead of `416 Range Not Satisfiable`. diff --git a/lib/modules/datasource/terraform-provider/index.ts b/lib/modules/datasource/terraform-provider/index.ts index 0f3e72dcacab757796dcc96730d35af9caf303f4..9f37151eff32baf8d8f72aecd437db18582fdd99 100644 --- a/lib/modules/datasource/terraform-provider/index.ts +++ b/lib/modules/datasource/terraform-provider/index.ts @@ -304,7 +304,7 @@ export class TerraformProviderDatasource extends TerraformDatasource { // The hashes are formatted as the result of sha256sum in plain text, each line: <hash>\t<filename> let rawHashData: string; try { - rawHashData = (await this.http.get(zipHashUrl)).body; + rawHashData = (await this.http.getText(zipHashUrl)).body; } catch (err) { /* istanbul ignore next */ if (err instanceof ExternalHostError) { diff --git a/lib/modules/manager/batect-wrapper/artifacts.ts b/lib/modules/manager/batect-wrapper/artifacts.ts index 344782786f8fb04117fe2f56a9a921d2f1010e78..fb1466d3fd17d9712e5a448a3326d4bb9a8c373b 100644 --- a/lib/modules/manager/batect-wrapper/artifacts.ts +++ b/lib/modules/manager/batect-wrapper/artifacts.ts @@ -12,7 +12,7 @@ async function updateArtifact( const url = `https://github.com/batect/batect/releases/download/${version}/${fileName}`; try { - const response = await http.get(url); + const response = await http.getText(url); const contents = response.body; return { diff --git a/lib/modules/manager/gradle-wrapper/artifacts.ts b/lib/modules/manager/gradle-wrapper/artifacts.ts index b523ae69860ce0c49e4a8e5166e4b438f34aea17..50146dc908e56bc06270a34039cd326fbf54c5d4 100644 --- a/lib/modules/manager/gradle-wrapper/artifacts.ts +++ b/lib/modules/manager/gradle-wrapper/artifacts.ts @@ -63,7 +63,7 @@ function getDistributionUrl(newPackageFileContent: string): string | null { } async function getDistributionChecksum(url: string): Promise<string> { - const { body } = await http.get(`${url}.sha256`); + const { body } = await http.getText(`${url}.sha256`); return body; } diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index 628aefa3ef87f4199c14737bf10cfc593ac8a009..8052ca9204f93eda85c533ab6ab2ab23036cae9e 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -169,7 +169,7 @@ export async function getRawFile( `/2.0/repositories/${repo}/src/` + (finalBranchOrTag ?? `HEAD`) + `/${path}`; - const res = await bitbucketHttp.get(url, { + const res = await bitbucketHttp.getText(url, { cacheProvider: repoCacheProvider, memCache: true, }); diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index e058f0b703f791e4470bac2196fdb777c875e783..c187dbf2b30f0bf92d09b17c66d19733f4bb6597 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -181,7 +181,7 @@ class GerritClient { branch: string, fileName: string, ): Promise<string> { - const base64Content = await this.gerritHttp.get( + const base64Content = await this.gerritHttp.getText( `a/projects/${encodeURIComponent( repo, )}/branches/${encodeURIComponent(branch)}/files/${encodeURIComponent(fileName)}/content`, diff --git a/lib/util/http/auth.spec.ts b/lib/util/http/auth.spec.ts index 208a8209b67da11fa1267681c8a24595a37d9332..e5a8e8dd69c773add8927b709dd56200296593f3 100644 --- a/lib/util/http/auth.spec.ts +++ b/lib/util/http/auth.spec.ts @@ -221,6 +221,7 @@ describe('util/http/auth', () => { hostname: 'amazon.com', href: 'https://amazon.com', search: 'something X-Amz-Algorithm something', + headers: {}, }); removeAuthorization(opts); @@ -229,6 +230,7 @@ describe('util/http/auth', () => { hostname: 'amazon.com', href: 'https://amazon.com', search: 'something X-Amz-Algorithm something', + headers: {}, }); }); diff --git a/lib/util/http/auth.ts b/lib/util/http/auth.ts index 0beec921d901c98fc6174b98be402ac138a10e34..afbd65f03455dde370c23d0868ace5955e2a59bd 100644 --- a/lib/util/http/auth.ts +++ b/lib/util/http/auth.ts @@ -109,7 +109,6 @@ export function removeAuthorization(options: Options): void { // if there is no port in the redirect URL string, then delete it from the redirect options. // This can be evaluated for removal after upgrading to Got v10 const portInUrl = options.href?.split?.('/')?.[2]?.split(':')?.[1]; - // istanbul ignore next if (!portInUrl) { delete options.port; // Redirect will instead use 80 or 443 for HTTP or HTTPS respectively } diff --git a/lib/util/http/bitbucket-server.spec.ts b/lib/util/http/bitbucket-server.spec.ts index 855c50db94dfd7de38c7e2d4de098a92fb7ef84b..ec528f79e61970c586bbb21d314be13390e2a973 100644 --- a/lib/util/http/bitbucket-server.spec.ts +++ b/lib/util/http/bitbucket-server.spec.ts @@ -32,9 +32,7 @@ describe('util/http/bitbucket-server', () => { it('invalid path', async () => { setBaseUrl('some-in|valid-host'); const res = api.getJsonUnchecked('/some-url'); - await expect(res).rejects.toThrow( - 'Bitbucket Server: cannot parse path /some-url', - ); + await expect(res).rejects.toThrow('Invalid URL'); }); it('pagination: uses default limit if not configured', async () => { diff --git a/lib/util/http/bitbucket-server.ts b/lib/util/http/bitbucket-server.ts index 2665ae1bdc6287cf3e4960ef0c763e1fb5c03729..e458d9a63a26831716307f016be03a3597b6420a 100644 --- a/lib/util/http/bitbucket-server.ts +++ b/lib/util/http/bitbucket-server.ts @@ -1,7 +1,6 @@ import is from '@sindresorhus/is'; -import { logger } from '../../logger'; -import { parseUrl, resolveBaseUrl } from '../url'; -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import type { InternalJsonUnsafeOptions } from './http'; +import type { HttpMethod, HttpOptions, HttpResponse } from './types'; import { Http } from '.'; const MAX_LIMIT = 100; @@ -22,45 +21,44 @@ interface PagedResult<T = unknown> { } export class BitbucketServerHttp extends Http<BitbucketServerHttpOptions> { + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(options?: HttpOptions) { super('bitbucket-server', options); } - protected override async request<T>( - path: string, - options?: InternalHttpOptions & BitbucketServerHttpOptions, + protected override async requestJsonUnsafe<T>( + method: HttpMethod, + options: InternalJsonUnsafeOptions<BitbucketServerHttpOptions>, ): Promise<HttpResponse<T>> { - const opts = { baseUrl, ...options }; - opts.headers = { - ...opts.headers, - 'X-Atlassian-Token': 'no-check', - }; - - const resolvedUrl = parseUrl(resolveBaseUrl(baseUrl, path)); - if (!resolvedUrl) { - logger.error({ path }, 'Bitbucket Server: cannot parse path'); - throw new Error(`Bitbucket Server: cannot parse path ${path}`); - } + const resolvedUrl = this.resolveUrl(options.url, options.httpOptions); + const opts = { ...options, url: resolvedUrl }; + opts.httpOptions ??= {}; + opts.httpOptions.headers ??= {}; + opts.httpOptions.headers['X-Atlassian-Token'] = 'no-check'; - if (opts.paginate) { - const limit = opts.limit ?? MAX_LIMIT; + const paginate = opts.httpOptions.paginate; + if (paginate) { + const limit = opts.httpOptions.limit ?? MAX_LIMIT; resolvedUrl.searchParams.set('limit', limit.toString()); } - const result = await super.request<T | PagedResult<T>>( - resolvedUrl.toString(), + const result = await super.requestJsonUnsafe<T | PagedResult<T>>( + method, opts, ); - if (opts.paginate && isPagedResult(result.body)) { + if (paginate && isPagedResult(result.body)) { const collectedValues = [...result.body.values]; let nextPageStart = result.body.nextPageStart; while (nextPageStart) { resolvedUrl.searchParams.set('start', nextPageStart.toString()); - const nextResult = await super.request<PagedResult<T>>( - resolvedUrl.toString(), + const nextResult = await super.requestJsonUnsafe<PagedResult<T>>( + method, opts, ); collectedValues.push(...nextResult.body.values); diff --git a/lib/util/http/bitbucket.ts b/lib/util/http/bitbucket.ts index 28341b0291def4f87e1b73583b9e18e41c2a742e..bee3ca1c84c2d9d51d3468ac565e52319a74a0d4 100644 --- a/lib/util/http/bitbucket.ts +++ b/lib/util/http/bitbucket.ts @@ -1,8 +1,7 @@ import is from '@sindresorhus/is'; -import { logger } from '../../logger'; import type { PagedResult } from '../../modules/platform/bitbucket/types'; -import { parseUrl, resolveBaseUrl } from '../url'; -import type { HttpOptions, HttpResponse } from './types'; +import type { InternalJsonUnsafeOptions } from './http'; +import type { HttpMethod, HttpOptions, HttpResponse } from './types'; import { Http } from '.'; const MAX_PAGES = 100; @@ -10,9 +9,9 @@ const MAX_PAGELEN = 100; let baseUrl = 'https://api.bitbucket.org/'; -export const setBaseUrl = (url: string): void => { +export function setBaseUrl(url: string): void { baseUrl = url; -}; +} export interface BitbucketHttpOptions extends HttpOptions { paginate?: boolean; @@ -20,46 +19,46 @@ export interface BitbucketHttpOptions extends HttpOptions { } export class BitbucketHttp extends Http<BitbucketHttpOptions> { + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(type = 'bitbucket', options?: BitbucketHttpOptions) { super(type, options); } - protected override async request<T>( - path: string, - options?: BitbucketHttpOptions, + protected override async requestJsonUnsafe<T>( + method: HttpMethod, + options: InternalJsonUnsafeOptions<BitbucketHttpOptions>, ): Promise<HttpResponse<T>> { - const opts = { baseUrl, ...options }; + const resolvedUrl = this.resolveUrl(options.url, options.httpOptions); + const opts = { ...options, url: resolvedUrl }; + const paginate = opts.httpOptions?.paginate; - const resolvedURL = parseUrl(resolveBaseUrl(baseUrl, path)); - - // istanbul ignore if: this should never happen - if (is.nullOrUndefined(resolvedURL)) { - logger.error({ path }, 'Bitbucket: cannot parse path'); - throw new Error(`Bitbucket: cannot parse path ${path}`); + if (paginate && !hasPagelen(resolvedUrl)) { + const pagelen = opts.httpOptions!.pagelen ?? MAX_PAGELEN; + resolvedUrl.searchParams.set('pagelen', pagelen.toString()); } - if (opts.paginate && !hasPagelen(resolvedURL)) { - const pagelen = opts.pagelen ?? MAX_PAGELEN; - resolvedURL.searchParams.set('pagelen', pagelen.toString()); - } - - const result = await super.request<T>(resolvedURL.toString(), opts); - - if (opts.paginate && isPagedResult(result.body)) { - const resultBody = result.body as PagedResult<T>; - let page = 1; - let nextURL = resultBody.next; - - while (is.nonEmptyString(nextURL) && page <= MAX_PAGES) { - const nextResult = await super.request<PagedResult<T>>( - nextURL, - options!, + const result = await super.requestJsonUnsafe<T | PagedResult<T>>( + method, + opts, + ); + + if (paginate && isPagedResult(result.body)) { + const resultBody = result.body; + let nextURL = result.body.next; + let page = 2; + + for (; nextURL && page <= MAX_PAGES; page++) { + resolvedUrl.searchParams.set('page', page.toString()); + const nextResult = await super.requestJsonUnsafe<PagedResult<T>>( + method, + opts, ); resultBody.values.push(...nextResult.body.values); - - nextURL = nextResult.body?.next; - page += 1; + nextURL = nextResult.body.next; } // Override other page-related attributes @@ -67,12 +66,12 @@ export class BitbucketHttp extends Http<BitbucketHttpOptions> { resultBody.size = page <= MAX_PAGES ? resultBody.values.length - : /* istanbul ignore next */ undefined; + : /* v8 ignore next */ undefined; resultBody.next = - page <= MAX_PAGES ? nextURL : /* istanbul ignore next */ undefined; + page <= MAX_PAGES ? nextURL : /* v8 ignore next */ undefined; } - return result; + return result as HttpResponse<T>; } } @@ -80,6 +79,6 @@ function hasPagelen(url: URL): boolean { return !is.nullOrUndefined(url.searchParams.get('pagelen')); } -function isPagedResult(obj: any): obj is PagedResult { +function isPagedResult<T>(obj: any): obj is PagedResult<T> { return is.nonEmptyObject(obj) && Array.isArray(obj.values); } diff --git a/lib/util/http/cache/abstract-http-cache-provider.ts b/lib/util/http/cache/abstract-http-cache-provider.ts index 2d1eb05955890b309608a206289e65222bce982a..08288a0bddc079b8b932776d929398d9367ed19d 100644 --- a/lib/util/http/cache/abstract-http-cache-provider.ts +++ b/lib/util/http/cache/abstract-http-cache-provider.ts @@ -66,7 +66,7 @@ export abstract class AbstractHttpCacheProvider implements HttpCacheProvider { timestamp, }); - // istanbul ignore if: should never happen + /* v8 ignore next 4: should never happen */ if (!newHttpCache) { logger.debug(`http cache: failed to persist cache for ${url}`); return resp; diff --git a/lib/util/http/cache/package-http-cache-provider.spec.ts b/lib/util/http/cache/package-http-cache-provider.spec.ts index 6db9bf461feb5396a74a63b92621371335044d06..1e0d30a121354a5694459694c792f89d2168f05f 100644 --- a/lib/util/http/cache/package-http-cache-provider.spec.ts +++ b/lib/util/http/cache/package-http-cache-provider.spec.ts @@ -52,7 +52,7 @@ describe('util/http/cache/package-http-cache-provider', () => { }); httpMock.scope(url).get('').reply(200, 'new response'); - const res = await http.get(url, { cacheProvider }); + const res = await http.getText(url, { cacheProvider }); expect(res.body).toBe('new response'); }); @@ -69,7 +69,7 @@ describe('util/http/cache/package-http-cache-provider', () => { namespace: '_test-namespace', }); - const res = await http.get(url, { cacheProvider }); + const res = await http.getText(url, { cacheProvider }); expect(res.body).toBe('cached response'); expect(packageCache.set).not.toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('util/http/cache/package-http-cache-provider', () => { mockTime('2024-06-15T00:15:00.000Z'); httpMock.scope(url).get('').reply(200, 'new response', { etag: 'foobar' }); - const res2 = await http.get(url, { cacheProvider }); + const res2 = await http.getText(url, { cacheProvider }); expect(res2.body).toBe('new response'); expect(packageCache.set).toHaveBeenCalled(); }); @@ -91,7 +91,7 @@ describe('util/http/cache/package-http-cache-provider', () => { .get('') .reply(200, 'fetched response', { etag: 'foobar' }); - const res = await http.get(url, { cacheProvider }); + const res = await http.getText(url, { cacheProvider }); expect(res.body).toBe('fetched response'); expect(cache).toEqual({ @@ -121,7 +121,7 @@ describe('util/http/cache/package-http-cache-provider', () => { }); httpMock.scope(url).get('').reply(500); - const res = await http.get(url, { cacheProvider }); + const res = await http.getText(url, { cacheProvider }); expect(res.body).toBe('cached response'); expect(packageCache.set).not.toHaveBeenCalled(); diff --git a/lib/util/http/gerrit.spec.ts b/lib/util/http/gerrit.spec.ts index 7d6563817cf5a8d1b7b3ad19296b4c0ba5ce6141..3e92dd7bba2cc22155a19ffbeb3a54a212a4efc6 100644 --- a/lib/util/http/gerrit.spec.ts +++ b/lib/util/http/gerrit.spec.ts @@ -18,7 +18,7 @@ describe('util/http/gerrit', () => { .get(/some-url\/$/) .reply(200, body, { 'content-type': 'text/plain;charset=utf-8' }); - const res = await api.get(pathOrUrl); + const res = await api.getText(pathOrUrl); expect(res.body).toEqual(body); }); diff --git a/lib/util/http/gerrit.ts b/lib/util/http/gerrit.ts index a690ba951b3147e660566b4670228e475dcaa58d..babd02bf43a99d719a45516071283803217abd70 100644 --- a/lib/util/http/gerrit.ts +++ b/lib/util/http/gerrit.ts @@ -1,8 +1,9 @@ import { parseJson } from '../common'; import { regEx } from '../regex'; import { isHttpUrl } from '../url'; -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; -import { Http } from './index'; +import type { InternalHttpOptions } from './http'; +import type { HttpOptions } from './types'; +import { Http } from '.'; let baseUrl: string; export function setBaseUrl(url: string): void { @@ -16,23 +17,30 @@ export function setBaseUrl(url: string): void { export class GerritHttp extends Http { private static magicPrefix = regEx(/^\)]}'\n/g); + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(options?: HttpOptions) { super('gerrit', options); } - protected override async request<T>( - path: string, - options?: InternalHttpOptions, - ): Promise<HttpResponse<T>> { - const url = isHttpUrl(path) ? path : baseUrl + path; - const opts: InternalHttpOptions = { - parseJson: (text: string) => - parseJson(text.replace(GerritHttp.magicPrefix, ''), path), - ...options, - }; - opts.headers = { - ...opts.headers, - }; - return await super.request<T>(url, opts); + protected override resolveUrl( + requestUrl: string | URL, + options: HttpOptions | undefined, + ): URL { + // ensure trailing slash for gerrit + return super.resolveUrl( + isHttpUrl(requestUrl) ? requestUrl : `${baseUrl}${requestUrl}`, + options, + ); + } + + protected override processOptions( + url: URL, + options: InternalHttpOptions, + ): void { + options.parseJson = (text: string) => + parseJson(text.replace(GerritHttp.magicPrefix, ''), url.pathname); } } diff --git a/lib/util/http/gitea.ts b/lib/util/http/gitea.ts index f69470c44ed6f317c72003c638282afa596f62fd..51fb0cba281446e38dd379e6ac189ec6ef9db7f6 100644 --- a/lib/util/http/gitea.ts +++ b/lib/util/http/gitea.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; -import { resolveBaseUrl } from '../url'; -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import type { InternalJsonUnsafeOptions } from './http'; +import type { HttpMethod, HttpOptions, HttpResponse } from './types'; import { Http } from '.'; let baseUrl: string; @@ -24,28 +24,28 @@ function getPaginationContainer<T = unknown>(body: unknown): T[] | null { return null; } -function resolveUrl(path: string, base: string): URL { - const resolvedUrlString = resolveBaseUrl(base, path); - return new URL(resolvedUrlString); -} - export class GiteaHttp extends Http<GiteaHttpOptions> { + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(hostType?: string, options?: HttpOptions) { super(hostType ?? 'gitea', options); } - protected override async request<T>( - path: string, - options?: InternalHttpOptions & GiteaHttpOptions, + protected override async requestJsonUnsafe<T = unknown>( + method: HttpMethod, + options: InternalJsonUnsafeOptions<GiteaHttpOptions>, ): Promise<HttpResponse<T>> { - const resolvedUrl = resolveUrl(path, options?.baseUrl ?? baseUrl); + const resolvedUrl = this.resolveUrl(options.url, options.httpOptions); const opts = { - baseUrl, ...options, + url: resolvedUrl, }; - const res = await super.request<T>(resolvedUrl, opts); + const res = await super.requestJsonUnsafe<T>(method, opts); const pc = getPaginationContainer<T>(res.body); - if (opts.paginate && pc) { + if (opts.httpOptions?.paginate && pc) { + delete opts.httpOptions.paginate; const total = parseInt(res.headers['x-total-count'] as string, 10); let nextPage = parseInt(resolvedUrl.searchParams.get('page') ?? '1', 10); @@ -53,7 +53,7 @@ export class GiteaHttp extends Http<GiteaHttpOptions> { nextPage += 1; resolvedUrl.searchParams.set('page', nextPage.toString()); - const nextRes = await super.request<T>(resolvedUrl.toString(), opts); + const nextRes = await super.requestJsonUnsafe<T>(method, opts); const nextPc = getPaginationContainer<T>(nextRes.body); if (nextPc === null) { break; diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts index 7223400b9ba493fed33e4dc96f0339100d157e32..322cf10d7c318571e706f51e80af684afa02a832 100644 --- a/lib/util/http/github.spec.ts +++ b/lib/util/http/github.spec.ts @@ -1,4 +1,3 @@ -import { Buffer } from 'node:buffer'; import { codeBlock } from 'common-tags'; import { DateTime } from 'luxon'; import * as httpMock from '../../../test/http-mock'; @@ -566,7 +565,7 @@ describe('util/http/github', () => { .scope('https://ghe.mycompany.com') .post('/api/graphql') .reply(200, { data: { repository } }); - await githubApi.requestGraphql(graphqlQuery); + await githubApi.requestGraphql(graphqlQuery, { token: 'abc' }); const [req] = httpMock.getTrace(); expect(req).toBeDefined(); expect(req.url).toBe('https://ghe.mycompany.com/api/graphql'); @@ -939,21 +938,5 @@ describe('util/http/github', () => { body: 'test', }); }); - - it('throw error if a', async () => { - httpMock - .scope(githubApiHost) - .get('/foo/bar/contents/lore/ipsum.bin') - .matchHeader( - 'accept', - 'application/vnd.github.raw+json, application/vnd.github.v3+json', - ) - .reply(200, Buffer.from('foo', 'binary')); - await expect( - githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.bin`, { - responseType: 'buffer', - }), - ).rejects.toThrow(); - }); }); }); diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts index 56db54db38716c77c989888242f97489d607ab88..b1f45271cd0f061a9685c497790f7c4e71eec900 100644 --- a/lib/util/http/github.ts +++ b/lib/util/http/github.ts @@ -13,14 +13,15 @@ import { maskToken } from '../mask'; import * as p from '../promises'; import { range } from '../range'; import { regEx } from '../regex'; -import { joinUrlParts, parseLinkHeader, resolveBaseUrl } from '../url'; +import { joinUrlParts, parseLinkHeader } from '../url'; import { findMatchingRule } from './host-rules'; +import type { InternalHttpOptions, InternalJsonUnsafeOptions } from './http'; import type { GotLegacyError } from './legacy'; import type { GraphqlOptions, + HttpMethod, HttpOptions, HttpResponse, - InternalHttpOptions, } from './types'; import { Http } from '.'; @@ -267,23 +268,20 @@ function replaceUrlBase(url: URL, baseUrl: string): URL { } export class GithubHttp extends Http<GithubHttpOptions> { + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(hostType = 'github', options?: GithubHttpOptions) { super(hostType, options); } - protected override async request<T>( - url: string | URL, - options?: InternalHttpOptions & GithubHttpOptions, - okToRetry = true, - ): Promise<HttpResponse<T>> { - const opts: InternalHttpOptions & GithubHttpOptions = { - baseUrl, - ...options, - throwHttpErrors: true, - }; - + protected override processOptions( + url: URL, + opts: InternalHttpOptions & GithubHttpOptions, + ): void { if (!opts.token) { - const authUrl = new URL(resolveBaseUrl(opts.baseUrl!, url)); + const authUrl = new URL(url); if (opts.repository) { // set authUrl to https://api.github.com/repos/org/repo or https://gihub.domain.com/api/v3/repos/org/repo @@ -317,68 +315,87 @@ export class GithubHttp extends Http<GithubHttpOptions> { ...opts.headers, accept, }; + } - try { - const result = await super.request<T>(url, opts); - if (opts.paginate) { - // Check if result is paginated - const pageLimit = opts.pageLimit ?? 10; - const linkHeader = parseLinkHeader(result?.headers?.link); - const next = linkHeader?.next; - if (next?.url && linkHeader?.last?.page) { - let lastPage = parseInt(linkHeader.last.page, 10); - // istanbul ignore else: needs a test - if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') { - lastPage = Math.min(pageLimit, lastPage); - } - const baseUrl = opts.baseUrl; - const parsedUrl = new URL(next.url, baseUrl); - const rebasePagination = - !!baseUrl && - !!process.env.RENOVATE_X_REBASE_PAGINATION_LINKS && - // Preserve github.com URLs for use cases like release notes - parsedUrl.origin !== 'https://api.github.com'; - const firstPageUrl = rebasePagination - ? replaceUrlBase(parsedUrl, baseUrl) - : parsedUrl; - const queue = [...range(2, lastPage)].map( - (pageNumber) => (): Promise<HttpResponse<T>> => { - // copy before modifying searchParams - const nextUrl = new URL(firstPageUrl); - nextUrl.searchParams.set('page', String(pageNumber)); - return this.request<T>( - nextUrl, - { ...opts, paginate: false, cacheProvider: undefined }, - okToRetry, - ); - }, - ); - const pages = await p.all(queue); - if (opts.paginationField && is.plainObject(result.body)) { - const paginatedResult = result.body[opts.paginationField]; - if (is.array<T>(paginatedResult)) { - for (const nextPage of pages) { - if (is.plainObject(nextPage.body)) { - const nextPageResults = nextPage.body[opts.paginationField]; - if (is.array<T>(nextPageResults)) { - paginatedResult.push(...nextPageResults); - } + protected override handleError( + url: string | URL, + opts: HttpOptions, + err: GotLegacyError, + ): never { + throw handleGotError(err, url, opts); + } + + protected override async requestJsonUnsafe<T>( + method: HttpMethod, + options: InternalJsonUnsafeOptions<GithubHttpOptions>, + ): Promise<HttpResponse<T>> { + const httpOptions = options.httpOptions ?? {}; + const resolvedUrl = this.resolveUrl(options.url, httpOptions); + const opts = { + ...options, + url: resolvedUrl, + }; + + const result = await super.requestJsonUnsafe<T>(method, opts); + if (httpOptions.paginate) { + delete httpOptions.cacheProvider; + // Check if result is paginated + const pageLimit = httpOptions.pageLimit ?? 10; + const linkHeader = parseLinkHeader(result?.headers?.link); + const next = linkHeader?.next; + if (next?.url && linkHeader?.last?.page) { + let lastPage = parseInt(linkHeader.last.page, 10); + if ( + !process.env.RENOVATE_PAGINATE_ALL && + httpOptions.paginate !== 'all' + ) { + lastPage = Math.min(pageLimit, lastPage); + } + const baseUrl = httpOptions.baseUrl ?? this.baseUrl; + const parsedUrl = new URL(next.url, baseUrl); + const rebasePagination = + !!baseUrl && + !!process.env.RENOVATE_X_REBASE_PAGINATION_LINKS && + // Preserve github.com URLs for use cases like release notes + parsedUrl.origin !== 'https://api.github.com'; + const firstPageUrl = rebasePagination + ? replaceUrlBase(parsedUrl, baseUrl) + : parsedUrl; + const queue = [...range(2, lastPage)].map( + (pageNumber) => (): Promise<HttpResponse<T>> => { + // copy before modifying searchParams + const nextUrl = new URL(firstPageUrl); + nextUrl.searchParams.set('page', String(pageNumber)); + return super.requestJsonUnsafe<T>(method, { + ...opts, + url: nextUrl, + }); + }, + ); + const pages = await p.all(queue); + if (httpOptions.paginationField && is.plainObject(result.body)) { + const paginatedResult = result.body[httpOptions.paginationField]; + if (is.array<T>(paginatedResult)) { + for (const nextPage of pages) { + if (is.plainObject(nextPage.body)) { + const nextPageResults = + nextPage.body[httpOptions.paginationField]; + if (is.array<T>(nextPageResults)) { + paginatedResult.push(...nextPageResults); } } } - } else if (is.array<T>(result.body)) { - for (const nextPage of pages) { - if (is.array<T>(nextPage.body)) { - result.body.push(...nextPage.body); - } + } + } else if (is.array<T>(result.body)) { + for (const nextPage of pages) { + if (is.array<T>(nextPage.body)) { + result.body.push(...nextPage.body); } } } } - return result; - } catch (err) { - throw handleGotError(err, url, opts); } + return result; } public async requestGraphql<T = unknown>( @@ -522,12 +539,6 @@ export class GithubHttp extends Http<GithubHttpOptions> { newURL = joinUrlParts(options.repository, 'contents', url); } - const result = await this.get(newURL, newOptions); - if (!is.string(result.body)) { - throw new Error( - `Expected raw text file but received ${typeof result.body}`, - ); - } - return result; + return await this.getText(newURL, newOptions); } } diff --git a/lib/util/http/gitlab.spec.ts b/lib/util/http/gitlab.spec.ts index 9b0b9c6d88eb98c067d5e9d16691cdf7b714902c..0838af126451d6295c9b7df7aea2b5ffb123dbdc 100644 --- a/lib/util/http/gitlab.spec.ts +++ b/lib/util/http/gitlab.spec.ts @@ -4,6 +4,7 @@ import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages'; import { GitlabReleasesDatasource } from '../../modules/datasource/gitlab-releases'; import * as hostRules from '../host-rules'; import { GitlabHttp, setBaseUrl } from './gitlab'; +import { logger } from '~test/util'; hostRules.add({ hostType: 'gitlab', @@ -36,18 +37,26 @@ describe('util/http/gitlab', () => { .scope(gitlabApiHost) .get('/api/v4/some-url') .reply(200, ['a'], { - link: '<https://gitlab.com/api/v4/some-url&page=2>; rel="next", <https://gitlab.com/api/v4/some-url&page=3>; rel="last"', + link: '<https://gitlab.com/api/v4/some-url&page=2>; rel="next", <https://gitlab.com/api/v4/some-url&page=4>; rel="last"', }) .get('/api/v4/some-url&page=2') .reply(200, ['b', 'c'], { - link: '<https://gitlab.com/api/v4/some-url&page=3>; rel="next", <https://gitlab.com/api/v4/some-url&page=3>; rel="last"', + link: '<https://gitlab.com/api/v4/some-url&page=3>; rel="next", <https://gitlab.com/api/v4/some-url&page=4>; rel="last"', }) .get('/api/v4/some-url&page=3') - .reply(200, ['d']); + .reply(200, ['d'], { + link: '<https://gitlab.com/api/v4/some-url&page=4>; rel="next", <https://gitlab.com/api/v4/some-url&page=4>; rel="last"', + }) + .get('/api/v4/some-url&page=4') + .reply(500); const res = await gitlabApi.getJsonUnchecked('some-url', { paginate: true, }); expect(res.body).toHaveLength(4); + expect(logger.logger.warn).toHaveBeenCalledWith( + { err: expect.any(Error) }, + 'Pagination error', + ); }); it('paginates with GITLAB_IGNORE_REPO_URL set', async () => { diff --git a/lib/util/http/gitlab.ts b/lib/util/http/gitlab.ts index 28beca26261f11dac55063060455ce4ecf4275b4..4bdda9dd7ee21dc4dbf55969726d390ede098a92 100644 --- a/lib/util/http/gitlab.ts +++ b/lib/util/http/gitlab.ts @@ -1,9 +1,10 @@ import is from '@sindresorhus/is'; -import type { RetryObject } from 'got'; +import { RequestError, type RetryObject } from 'got'; import { logger } from '../../logger'; import { ExternalHostError } from '../../types/errors/external-host-error'; import { parseLinkHeader, parseUrl } from '../url'; -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import type { InternalJsonUnsafeOptions } from './http'; +import type { HttpMethod, HttpOptions, HttpResponse } from './types'; import { Http } from '.'; let baseUrl = 'https://gitlab.com/api/v4/'; @@ -16,73 +17,92 @@ export interface GitlabHttpOptions extends HttpOptions { } export class GitlabHttp extends Http<GitlabHttpOptions> { + protected override get baseUrl(): string | undefined { + return baseUrl; + } + constructor(type = 'gitlab', options?: GitlabHttpOptions) { super(type, options); } - protected override async request<T>( - url: string | URL, - options?: InternalHttpOptions & GitlabHttpOptions, + protected override async requestJsonUnsafe<T = unknown>( + method: HttpMethod, + options: InternalJsonUnsafeOptions<GitlabHttpOptions>, ): Promise<HttpResponse<T>> { + const resolvedUrl = this.resolveUrl(options.url, options.httpOptions); const opts = { - baseUrl, ...options, - throwHttpErrors: true, + url: resolvedUrl, }; + opts.httpOptions ??= {}; + opts.httpOptions.throwHttpErrors = true; + + const result = await super.requestJsonUnsafe<T>(method, opts); + if (opts.httpOptions.paginate && is.array(result.body)) { + // Check if result is paginated + try { + const linkHeader = parseLinkHeader(result.headers.link); + const nextUrl = linkHeader?.next?.url + ? parseUrl(linkHeader.next.url) + : null; + if (nextUrl) { + if (process.env.GITLAB_IGNORE_REPO_URL) { + const defaultEndpoint = new URL(baseUrl); + nextUrl.protocol = defaultEndpoint.protocol; + nextUrl.host = defaultEndpoint.host; + } - try { - const result = await super.request<T>(url, opts); - if (opts.paginate && is.array(result.body)) { - // Check if result is paginated - try { - const linkHeader = parseLinkHeader(result.headers.link); - const nextUrl = linkHeader?.next?.url - ? parseUrl(linkHeader.next.url) - : null; - if (nextUrl) { - if (process.env.GITLAB_IGNORE_REPO_URL) { - const defaultEndpoint = new URL(baseUrl); - nextUrl.protocol = defaultEndpoint.protocol; - nextUrl.host = defaultEndpoint.host; - } + opts.url = nextUrl; - const nextResult = await this.request<T>(nextUrl, opts); - if (is.array(nextResult.body)) { - result.body.push(...nextResult.body); - } + const nextResult = await this.requestJsonUnsafe<T>(method, opts); + if (is.array(nextResult.body)) { + result.body.push(...nextResult.body); } - } catch (err) /* istanbul ignore next */ { - logger.warn({ err }, 'Pagination error'); } + } catch (err) { + logger.warn({ err }, 'Pagination error'); } - return result; - } catch (err) { - if (err.statusCode === 404) { + } + return result; + } + + protected override handleError( + url: string | URL, + _httpOptions: HttpOptions, + err: Error, + ): never { + if (err instanceof RequestError && err.response?.statusCode) { + if (err.response.statusCode === 404) { logger.trace({ err }, 'GitLab 404'); - logger.debug({ url: err.url }, 'GitLab API 404'); + logger.debug({ url }, 'GitLab API 404'); throw err; } logger.debug({ err }, 'Gitlab API error'); if ( - err.statusCode === 429 || - (err.statusCode >= 500 && err.statusCode < 600) + err.response.statusCode === 429 || + (err.response.statusCode >= 500 && err.response.statusCode < 600) ) { throw new ExternalHostError(err, 'gitlab'); } - const platformFailureCodes = [ - 'EAI_AGAIN', - 'ECONNRESET', - 'ETIMEDOUT', - 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', - ]; - if (platformFailureCodes.includes(err.code)) { - throw new ExternalHostError(err, 'gitlab'); - } - if (err.name === 'ParseError') { - throw new ExternalHostError(err, 'gitlab'); - } - throw err; } + const platformFailureCodes = [ + 'EAI_AGAIN', + 'ECONNRESET', + 'ETIMEDOUT', + 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', + ]; + // TODO: fix test, should be `RequestError` + if ( + 'code' in err && + is.string(err.code) && + platformFailureCodes.includes(err.code) + ) { + throw new ExternalHostError(err, 'gitlab'); + } + if (err.name === 'ParseError') { + throw new ExternalHostError(err, 'gitlab'); + } + throw err; } protected override calculateRetryDelay(retryObject: RetryObject): number { diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts index b70b19b20f7292926bfe6a94b809ba268ae21fef..cd946419d0ebf51dcb8df1f3d0f03ddddb10017e 100644 --- a/lib/util/http/host-rules.ts +++ b/lib/util/http/host-rules.ts @@ -12,8 +12,9 @@ import type { HostRule } from '../../types'; import * as hostRules from '../host-rules'; import { matchRegexOrGlobList } from '../string-match'; import { parseUrl } from '../url'; +import type { InternalHttpOptions } from './http'; import { keepAliveAgents } from './keep-alive'; -import type { GotOptions, InternalHttpOptions } from './types'; +import type { GotOptions } from './types'; export type HostRulesGotOptions = Pick< GotOptions & InternalHttpOptions, diff --git a/lib/util/http/http.ts b/lib/util/http/http.ts new file mode 100644 index 0000000000000000000000000000000000000000..589bce68527dbedcfbe0a727e8078d371e32408b --- /dev/null +++ b/lib/util/http/http.ts @@ -0,0 +1,29 @@ +import type { Options } from 'got'; +import type { SetRequired } from 'type-fest'; +import type { ZodType } from 'zod'; +import type { GotOptions, HttpMethod, HttpOptions } from './types'; + +export interface InternalJsonUnsafeOptions< + Opts extends HttpOptions = HttpOptions, +> { + url: string | URL; + httpOptions?: Opts; +} + +export interface InternalJsonOptions< + Opts extends HttpOptions, + ResT = unknown, + Schema extends ZodType<ResT> = ZodType<ResT>, +> 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']; +} diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts index 8af2746c61ee0311ea1845d582b316237ad1bb19..aa3745b99019cc8b782331cd4f4438268cb845e1 100644 --- a/lib/util/http/index.spec.ts +++ b/lib/util/http/index.spec.ts @@ -28,7 +28,7 @@ describe('util/http/index', () => { it('get', async () => { httpMock.scope(baseUrl).get('/test').reply(200); - expect(await http.get('http://renovate.com/test')).toEqual({ + expect(await http.getText('http://renovate.com/test')).toEqual({ authorization: false, body: '', headers: {}, diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts index 245fdb541988dc4592359ed526a0c220bd827053..6cd54196705b703388f95732f0816c929d7c6e0e 100644 --- a/lib/util/http/index.ts +++ b/lib/util/http/index.ts @@ -1,3 +1,6 @@ +// TODO: refactor code to remove this (#9651) +import './legacy'; + import is from '@sindresorhus/is'; import merge from 'deepmerge'; import type { Options, RetryObject } from 'got'; @@ -14,24 +17,28 @@ import * as memCache from '../cache/memory'; import { hash } from '../hash'; import { type AsyncResult, Result } from '../result'; import { type HttpRequestStatsDataPoint, HttpStats } from '../stats'; -import { resolveBaseUrl } from '../url'; +import { isHttpUrl, parseUrl, resolveBaseUrl } from '../url'; import { parseSingleYaml } from '../yaml'; import { applyAuthorization, removeAuthorization } from './auth'; import { hooks } from './hooks'; import { applyHostRule, findMatchingRule } from './host-rules'; +import type { + InternalGotOptions, + InternalHttpOptions, + InternalJsonOptions, + InternalJsonUnsafeOptions, +} from './http'; import { getQueue } from './queue'; import { getRetryAfter, wrapWithRetry } from './retry-after'; import { getThrottle } from './throttle'; import type { - GotJSONOptions, + GotBufferOptions, GotOptions, GotTask, + HttpMethod, HttpOptions, HttpResponse, - InternalHttpOptions, } from './types'; -// TODO: refactor code to remove this (#9651) -import './legacy'; import { copyResponse } from './util'; export { RequestError as HttpError }; @@ -39,16 +46,6 @@ export { RequestError as HttpError }; export class EmptyResultError extends Error {} export type SafeJsonError = RequestError | ZodError | EmptyResultError; -interface HttpFnArgs< - Opts extends HttpOptions, - ResT = unknown, - Schema extends ZodType<ResT> = ZodType<ResT>, -> { - url: string; - httpOptions?: Opts; - schema?: Schema; -} - function applyDefaultHeaders(options: Options): void { const renovateVersion = pkg.version; options.headers = { @@ -61,48 +58,38 @@ function applyDefaultHeaders(options: Options): void { type QueueStatsData = Pick<HttpRequestStatsDataPoint, 'queueMs'>; -// 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<T>( +async function gotTask( url: string, options: SetRequired<GotOptions, 'method'>, queueStats: QueueStatsData, -): Promise<HttpResponse<T>> { +): Promise<HttpResponse<unknown>> { 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 got<T>(url, { ...options, hooks } as GotJSONOptions); + const resp = await got(url, { ...options, hooks } as GotBufferOptions); statusCode = resp.statusCode; duration = - resp.timings.phases.total ?? - /* istanbul ignore next: can't be tested */ 0; + resp.timings.phases.total ?? /* v8 ignore next: can't be tested */ 0; return resp; } catch (error) { if (error instanceof RequestError) { - statusCode = - error.response?.statusCode ?? - /* istanbul ignore next: can't be tested */ -1; + statusCode = error.response?.statusCode ?? -1; duration = - error.timings?.phases.total ?? - /* istanbul ignore next: can't be tested */ -1; + error.timings?.phases.total ?? /* v8 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; + const code = error.code ?? /* v8 ignore next */ 'UNKNOWN'; + const retryCount = error.request?.retryCount ?? /* v8 ignore next */ -1; logger.debug( `${method} ${url} = (code=${code}, statusCode=${statusCode} retryCount=${retryCount}, duration=${duration})`, ); } throw error; + /* v8 ignore next: 🛠https://github.com/bcoe/c8/issues/229 */ } finally { HttpStats.write({ method: options.method, @@ -115,16 +102,21 @@ async function gotTask<T>( } export class Http<Opts extends HttpOptions = HttpOptions> { - private options?: GotOptions; + private readonly options: InternalGotOptions; + + protected get baseUrl(): string | undefined { + return undefined; + } constructor( protected hostType: string, options: HttpOptions = {}, ) { const retryLimit = process.env.NODE_ENV === 'test' ? 0 : 2; - this.options = merge<GotOptions>( + this.options = merge<InternalGotOptions>( options, { + method: 'get', context: { hostType }, retry: { calculateDelay: (retryObject) => @@ -136,19 +128,34 @@ export class Http<Opts extends HttpOptions = HttpOptions> { { isMergeableObject: is.plainObject }, ); } + private async request( + requestUrl: string | URL, + httpOptions: InternalHttpOptions, + ): Promise<HttpResponse<string>>; + private async request( + requestUrl: string | URL, + httpOptions: InternalHttpOptions & { responseType: 'text' }, + ): Promise<HttpResponse<string>>; + private async request( + requestUrl: string | URL, + httpOptions: InternalHttpOptions & { responseType: 'buffer' }, + ): Promise<HttpResponse<Buffer>>; + private async request<T = unknown>( + requestUrl: string | URL, + httpOptions: InternalHttpOptions & { responseType: 'json' }, + ): Promise<HttpResponse<T>>; - protected async request<T>( + private async request( requestUrl: string | URL, httpOptions: InternalHttpOptions, - ): Promise<HttpResponse<T>> { - let url = requestUrl.toString(); - if (httpOptions?.baseUrl) { - url = resolveBaseUrl(httpOptions.baseUrl, url); - } + ): Promise<HttpResponse<unknown>> { + const resolvedUrl = this.resolveUrl(requestUrl, httpOptions); + const url = resolvedUrl.toString(); - let options = merge<SetRequired<GotOptions, 'method'>, InternalHttpOptions>( + this.processOptions(resolvedUrl, httpOptions); + + let options = merge<InternalGotOptions, InternalHttpOptions>( { - method: 'get', ...this.options, hostType: this.hostType, }, @@ -181,7 +188,9 @@ export class Http<Opts extends HttpOptions = HttpOptions> { options.timeout ??= 60000; const { cacheProvider } = options; - const cachedResponse = await cacheProvider?.bypassServer<T>(url); + const cachedResponse = await cacheProvider?.bypassServer<string | Buffer>( + url, + ); if (cachedResponse) { return cachedResponse; } @@ -198,34 +207,29 @@ export class Http<Opts extends HttpOptions = HttpOptions> { ) : null; - let resPromise: Promise<HttpResponse<T>> | null = null; + let resPromise: Promise<HttpResponse<unknown>> | null = null; // Cache GET requests unless memCache=false if (memCacheKey) { resPromise = memCache.get(memCacheKey); } - // istanbul ignore else: no cache tests if (!resPromise) { if (cacheProvider) { await cacheProvider.setCacheHeaders(url, options); } const startTime = Date.now(); - const httpTask: GotTask<T> = () => { + const httpTask: GotTask = () => { const queueMs = Date.now() - startTime; return gotTask(url, options, { queueMs }); }; const throttle = getThrottle(url); - const throttledTask: GotTask<T> = throttle - ? () => throttle.add<HttpResponse<T>>(httpTask) - : httpTask; + const throttledTask = throttle ? () => throttle.add(httpTask) : httpTask; const queue = getQueue(url); - const queuedTask: GotTask<T> = queue - ? () => queue.add<HttpResponse<T>>(throttledTask) - : throttledTask; + const queuedTask = queue ? () => queue.add(throttledTask) : throttledTask; const { maxRetryAfter = 60 } = hostRule; resPromise = wrapWithRetry(queuedTask, url, getRetryAfter, maxRetryAfter); @@ -252,7 +256,10 @@ export class Http<Opts extends HttpOptions = HttpOptions> { throw new ExternalHostError(err); } - const staleResponse = await cacheProvider?.bypassServer<T>(url, true); + const staleResponse = await cacheProvider?.bypassServer<string | Buffer>( + url, + true, + ); if (staleResponse) { logger.debug( { err }, @@ -261,57 +268,110 @@ export class Http<Opts extends HttpOptions = HttpOptions> { return staleResponse; } - throw err; + this.handleError(requestUrl, httpOptions, err); } } + protected processOptions(_url: URL, _options: InternalHttpOptions): void { + // noop + } + + protected handleError( + _url: string | URL, + _httpOptions: HttpOptions, + err: Error, + ): never { + throw err; + } + + protected resolveUrl( + requestUrl: string | URL, + options: HttpOptions | undefined, + ): 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 }, '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> { - return this.request<string>(url, options); + get( + url: string, + options: HttpOptions = {}, + ): Promise<HttpResponse<string | Buffer>> { + 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>>; } - head(url: string, options: HttpOptions = {}): Promise<HttpResponse> { - return this.request<string>(url, { ...options, method: 'head' }); + getText( + url: string | URL, + options: HttpOptions = {}, + ): Promise<HttpResponse<string>> { + return this.request(url, { ...options, responseType: 'text' }); } getBuffer( - url: string, + url: string | URL, options: HttpOptions = {}, ): Promise<HttpResponse<Buffer>> { - return this.request<Buffer>(url, { - ...options, - responseType: 'buffer', - }); + return this.request(url, { ...options, responseType: 'buffer' }); } - private async requestJson<ResT = unknown>( - method: InternalHttpOptions['method'], - { url, httpOptions: requestOptions, schema }: HttpFnArgs<Opts, ResT>, + protected requestJsonUnsafe<ResT>( + method: HttpMethod, + { url, httpOptions: requestOptions }: InternalJsonUnsafeOptions<Opts>, ): Promise<HttpResponse<ResT>> { - const { body, ...httpOptions } = { ...requestOptions }; + const { body: json, ...httpOptions } = { ...requestOptions }; const opts: InternalHttpOptions = { ...httpOptions, method, - responseType: 'json', }; // signal that we expect a json response opts.headers = { accept: 'application/json', ...opts.headers, }; - if (body) { - opts.json = body; + if (json) { + opts.json = json; } - const res = await this.request<ResT>(url, opts); + return this.request<ResT>(url, { ...opts, responseType: 'json' }); + } + + private async requestJson<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>( + method: HttpMethod, + options: InternalJsonOptions<Opts, ResT, Schema>, + ): Promise<HttpResponse<ResT>> { + const res = await this.requestJsonUnsafe<ResT>(method, options); - if (!schema) { - return res; + if (options.schema) { + res.body = await options.schema.parseAsync(res.body); } - res.body = await schema.parseAsync(res.body); return res; } @@ -319,8 +379,8 @@ export class Http<Opts extends HttpOptions = HttpOptions> { arg1: string, arg2: Opts | ZodType<ResT> | undefined, arg3: ZodType<ResT> | undefined, - ): HttpFnArgs<Opts, ResT> { - const res: HttpFnArgs<Opts, ResT> = { url: arg1 }; + ): InternalJsonOptions<Opts, ResT> { + const res: InternalJsonOptions<Opts, ResT> = { url: arg1 }; if (arg2 instanceof ZodType) { res.schema = arg2; @@ -337,7 +397,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> { async getPlain(url: string, options?: Opts): Promise<HttpResponse> { const opt = options ?? {}; - return await this.get(url, { + return await this.getText(url, { headers: { Accept: 'text/plain', }, @@ -352,7 +412,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> { url: string, options?: Opts, ): Promise<HttpResponse<ResT>> { - const res = await this.get(url, options); + const res = await this.getText(url, options); const body = parseSingleYaml<ResT>(res.body); return { ...res, body }; } @@ -386,7 +446,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> { method: 'get', }; - const res = await this.get(url, opts); + const res = await this.getText(url, opts); const body = await schema.parseAsync(parseSingleYaml(res.body)); return { ...res, body }; } @@ -583,17 +643,12 @@ export class Http<Opts extends HttpOptions = HttpOptions> { stream(url: string, options?: HttpOptions): NodeJS.ReadableStream { // TODO: fix types (#22198) let combinedOptions: any = { - method: 'get', ...this.options, hostType: this.hostType, ...options, }; - let resolvedUrl = url; - // istanbul ignore else: needs test - if (options?.baseUrl) { - resolvedUrl = resolveBaseUrl(options.baseUrl, url); - } + const resolvedUrl = this.resolveUrl(url, options).toString(); applyDefaultHeaders(combinedOptions); diff --git a/lib/util/http/jira.ts b/lib/util/http/jira.ts index 706780b284915ea333783a94409509871972be49..adfe8aa3d0322e12db5bc87e587e1023f6796d2f 100644 --- a/lib/util/http/jira.ts +++ b/lib/util/http/jira.ts @@ -1,22 +1,18 @@ -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import type { HttpOptions } from './types'; import { Http } from '.'; let baseUrl: string; -export const setBaseUrl = (url: string): void => { +export function setBaseUrl(url: string): void { baseUrl = url; -}; +} export class JiraHttp extends Http { - constructor(type = 'jira', options?: HttpOptions) { - super(type, options); + protected override get baseUrl(): string | undefined { + return baseUrl; } - protected override request<T>( - url: string | URL, - options?: InternalHttpOptions, - ): Promise<HttpResponse<T>> { - const opts = { baseUrl, ...options }; - return super.request<T>(url, opts); + constructor(type = 'jira', options?: HttpOptions) { + super(type, options); } } diff --git a/lib/util/http/types.ts b/lib/util/http/types.ts index 978184cf2b16d9afd676a4ea10e666d50a4b0c9e..2b6a31b4e574a95c15e47cc48fb05229a9a3d43c 100644 --- a/lib/util/http/types.ts +++ b/lib/util/http/types.ts @@ -2,7 +2,7 @@ import type { IncomingHttpHeaders } from 'node:http'; import type { OptionsOfBufferResponseBody, OptionsOfJSONResponseBody, - ParseJsonFunction, + OptionsOfTextResponseBody, } from 'got'; import type { HttpCacheProvider } from './cache/types'; @@ -11,8 +11,9 @@ export type GotContextOptions = { } & Record<string, unknown>; // TODO: Move options to context -export type GotOptions = GotBufferOptions | GotJSONOptions; +export type GotOptions = GotBufferOptions | GotTextOptions | GotJSONOptions; export type GotBufferOptions = OptionsOfBufferResponseBody & GotExtraOptions; +export type GotTextOptions = OptionsOfTextResponseBody & GotExtraOptions; export type GotJSONOptions = OptionsOfJSONResponseBody & GotExtraOptions; export interface GotExtraOptions { @@ -69,17 +70,12 @@ export interface HttpOptions { readOnly?: boolean; } -export interface InternalHttpOptions extends HttpOptions { - json?: HttpOptions['body']; - responseType?: 'json' | 'buffer'; - method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'; - parseJson?: ParseJsonFunction; -} - export interface HttpHeaders extends IncomingHttpHeaders { link?: string | undefined; } +export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'; + export interface HttpResponse<T = string> { statusCode: number; body: T; @@ -88,7 +84,7 @@ export interface HttpResponse<T = string> { } export type Task<T> = () => Promise<T>; -export type GotTask<T> = Task<HttpResponse<T>>; +export type GotTask<T = unknown> = Task<HttpResponse<T>>; export interface ThrottleLimitRule { matchHost: string; diff --git a/lib/util/url.spec.ts b/lib/util/url.spec.ts index e1ed216102e3be2602453dd433e47d1abc0535b8..77d3bdc0f158a31dc7e7ec1355e2fbbc9c3e3bee 100644 --- a/lib/util/url.spec.ts +++ b/lib/util/url.spec.ts @@ -106,6 +106,7 @@ describe('util/url', () => { expect(isHttpUrl('ssh://github.com')).toBeFalse(); expect(isHttpUrl('http://github.com')).toBeTrue(); expect(isHttpUrl('https://github.com')).toBeTrue(); + expect(isHttpUrl(new URL('https://github.com'))).toBeTrue(); }); it('parses URL', () => { diff --git a/lib/util/url.ts b/lib/util/url.ts index d9886939dc1dd3f67ed5a4869a9c7330e867051f..44efb7f5b365486cd38a0c261ccc6d3ada800a79 100644 --- a/lib/util/url.ts +++ b/lib/util/url.ts @@ -86,15 +86,11 @@ export function getQueryString(params: Record<string, any>): string { } export function isHttpUrl(url: unknown): boolean { - if (!is.nonEmptyString(url)) { - return false; - } - try { - const { protocol } = new URL(url); - return protocol === 'https:' || protocol === 'http:'; - } catch { + if (!is.nonEmptyString(url) && !is.urlInstance(url)) { return false; } + const protocol = parseUrl(url)?.protocol; + return protocol === 'https:' || protocol === 'http:'; } export function parseUrl(url: URL | string | undefined | null): URL | null { diff --git a/lib/workers/repository/update/pr/changelog/bitbucket/index.ts b/lib/workers/repository/update/pr/changelog/bitbucket/index.ts index 3f0ca2cce7a7ab239d026cae047b4ffec42bf68f..e9e43bc3a03df8ee609b38b1de487075fed0234f 100644 --- a/lib/workers/repository/update/pr/changelog/bitbucket/index.ts +++ b/lib/workers/repository/update/pr/changelog/bitbucket/index.ts @@ -61,7 +61,7 @@ export async function getReleaseNotesMd( ); } - const fileRes = await bitbucketHttp.get( + const fileRes = await bitbucketHttp.getText( joinUrlParts( apiBaseUrl, '2.0/repositories', diff --git a/lib/workers/repository/update/pr/changelog/gitlab/index.ts b/lib/workers/repository/update/pr/changelog/gitlab/index.ts index 4c17d4bac34fa5394281ee1cafffd00e0551f96b..8f838070c34b166a866cf223900c5ae8d3f6d65a 100644 --- a/lib/workers/repository/update/pr/changelog/gitlab/index.ts +++ b/lib/workers/repository/update/pr/changelog/gitlab/index.ts @@ -54,7 +54,7 @@ export async function getReleaseNotesMd( } // https://docs.gitlab.com/13.2/ee/api/repositories.html#raw-blob-content - const fileRes = await http.get(`${apiPrefix}blobs/${id}/raw`); + const fileRes = await http.getText(`${apiPrefix}blobs/${id}/raw`); const changelogMd = fileRes.body + '\n#\n##'; return { changelogFile, changelogMd }; } diff --git a/test/http-mock.ts b/test/http-mock.ts index ce695dd0aecdb1d1dc6f7d16039fa89eb196b917..d799e59289567bde943b689c776044b402ebbaea 100644 --- a/test/http-mock.ts +++ b/test/http-mock.ts @@ -73,7 +73,7 @@ export function clear(check = true): void { } if (missing.length) { - const err = new Error(missingHttpMockMessage(done, missing)); + const err = new Error(missingHttpMockMessage(done, missing, pending)); massageHttpMockStacktrace(err); throw err; } @@ -158,6 +158,7 @@ function massageHttpMockStacktrace(err: Error): void { function missingHttpMockMessage( done: RequestLog[], missing: MissingRequestLog[], + pending: string[], ): string { const blocks: string[] = []; @@ -208,6 +209,14 @@ function missingHttpMockMessage( `); } + if (pending.length) { + blocks.push(codeBlock` + Pending mocks: + + ${pending.join('\n')} + `); + } + blocks.push(explanation); return blocks.join('\n\n'); diff --git a/vitest.config.ts b/vitest.config.ts index 237d98eca691bb86feb35473227ebf89e25ae340..05bacaccd0179176fdc7098c84370693d2b6059a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -109,6 +109,7 @@ export default defineConfig(() => 'lib/modules/datasource/hex/v2/package.ts', 'lib/modules/datasource/hex/v2/signed.ts', 'lib/util/cache/package/redis.ts', + 'lib/util/http/http.ts', // TODO: remove when code is moved from index 'lib/util/http/legacy.ts', 'lib/workers/repository/cache.ts', ],