From 0445d3f4e07838b8cbba095ce63bc51dd8b6675f Mon Sep 17 00:00:00 2001 From: Tobias Date: Sat, 23 Mar 2024 13:47:04 +0100 Subject: [PATCH] fix(datasource/npm): respect `abortOnError` hostRule for registries (#26196) --- lib/modules/datasource/npm/get.spec.ts | 85 ++++++++++++++++++++++++++ lib/modules/datasource/npm/get.ts | 41 ++++++++++--- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/lib/modules/datasource/npm/get.spec.ts b/lib/modules/datasource/npm/get.spec.ts index b19d4359e93946..b8a3809bc4bc28 100644 --- a/lib/modules/datasource/npm/get.spec.ts +++ b/lib/modules/datasource/npm/get.spec.ts @@ -245,6 +245,91 @@ describe('modules/datasource/npm/get', () => { expect(await getDependency(http, registryUrl, 'npm-error-402')).toBeNull(); }); + it('throw ExternalHostError when error happens on registry.npmjs.org', async () => { + httpMock + .scope('https://registry.npmjs.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + await expect( + getDependency(http, registryUrl, 'npm-parse-error'), + ).rejects.toThrow(ExternalHostError); + }); + + it('redact body for ExternalHostError when error happens on registry.npmjs.org', async () => { + httpMock + .scope('https://registry.npmjs.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + let thrownError; + try { + await getDependency(http, registryUrl, 'npm-parse-error'); + } catch (error) { + thrownError = error; + } + expect(thrownError.err.name).toBe('ParseError'); + expect(thrownError.err.body).toBe('err.body deleted by Renovate'); + }); + + it('do not throw ExternalHostError when error happens on custom host', async () => { + setNpmrc('registry=https://test.org'); + httpMock + .scope('https://test.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + expect( + await getDependency(http, registryUrl, 'npm-parse-error'), + ).toBeNull(); + }); + + it('do not throw ExternalHostError when error happens on registry.npmjs.org when hostRules disables abortOnError', async () => { + hostRules.add({ + matchHost: 'https://registry.npmjs.org', + abortOnError: false, + }); + httpMock + .scope('https://registry.npmjs.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + expect( + await getDependency(http, registryUrl, 'npm-parse-error'), + ).toBeNull(); + }); + + it('do not throw ExternalHostError when error happens on registry.npmjs.org when hostRules without protocol disables abortOnError', async () => { + hostRules.add({ + matchHost: 'registry.npmjs.org', + abortOnError: false, + }); + httpMock + .scope('https://registry.npmjs.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + expect( + await getDependency(http, registryUrl, 'npm-parse-error'), + ).toBeNull(); + }); + + it('throw ExternalHostError when error happens on custom host when hostRules enables abortOnError', async () => { + setNpmrc('registry=https://test.org'); + hostRules.add({ + matchHost: 'https://test.org', + abortOnError: true, + }); + httpMock + .scope('https://test.org') + .get('/npm-parse-error') + .reply(200, 'not-a-json'); + const registryUrl = resolveRegistryUrl('npm-parse-error'); + await expect( + getDependency(http, registryUrl, 'npm-parse-error'), + ).rejects.toThrow(ExternalHostError); + }); + it('massages non-compliant repository urls', async () => { setNpmrc('registry=https://test.org\n_authToken=XXX'); diff --git a/lib/modules/datasource/npm/get.ts b/lib/modules/datasource/npm/get.ts index 0848f5fdfc01d6..cc8024ebdd0e80 100644 --- a/lib/modules/datasource/npm/get.ts +++ b/lib/modules/datasource/npm/get.ts @@ -7,6 +7,7 @@ import { HOST_DISABLED } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import * as packageCache from '../../../util/cache/package'; +import * as hostRules from '../../../util/host-rules'; import type { Http } from '../../../util/http'; import type { HttpOptions } from '../../../util/http/types'; import { regEx } from '../../../util/regex'; @@ -128,6 +129,23 @@ export async function getDependency( logger.trace({ packageName }, 'Using cached etag'); options.headers = { 'If-None-Match': cachedResult.cacheData.etag }; } + + // set abortOnError for registry.npmjs.org if no hostRule with explicit abortOnError exists + if ( + registryUrl === 'https://registry.npmjs.org' && + hostRules.find({ url: 'https://registry.npmjs.org' })?.abortOnError === + undefined + ) { + logger.trace( + { packageName, registry: 'https://registry.npmjs.org' }, + 'setting abortOnError hostRule for well known host', + ); + hostRules.add({ + matchHost: 'https://registry.npmjs.org', + abortOnError: true, + }); + } + const raw = await http.getJson(packageUrl, options); if (cachedResult?.cacheData && raw.statusCode === 304) { logger.trace(`Cached npm result for ${packageName} is revalidated`); @@ -229,29 +247,32 @@ export async function getDependency( } return dep; } catch (err) { + const actualError = err instanceof ExternalHostError ? err.err : err; const ignoredStatusCodes = [401, 402, 403, 404]; const ignoredResponseCodes = ['ENOTFOUND']; if ( - err.message === HOST_DISABLED || - ignoredStatusCodes.includes(err.statusCode) || - ignoredResponseCodes.includes(err.code) + actualError.message === HOST_DISABLED || + ignoredStatusCodes.includes(actualError.statusCode) || + ignoredResponseCodes.includes(actualError.code) ) { return null; } - if (uri.host === 'registry.npmjs.org') { + + if (err instanceof ExternalHostError) { if (cachedResult) { logger.warn( - { err }, - 'npmjs error, reusing expired cached result instead', + { err, host: uri.host }, + `npm host error, reusing expired cached result instead`, ); delete cachedResult.cacheData; return cachedResult; } - // istanbul ignore if - if (err.name === 'ParseError' && err.body) { - err.body = 'err.body deleted by Renovate'; + + if (actualError.name === 'ParseError' && actualError.body) { + actualError.body = 'err.body deleted by Renovate'; + err.err = actualError; } - throw new ExternalHostError(err); + throw err; } logger.debug({ err }, 'Unknown npm lookup error'); return null;