Skip to content

Commit

Permalink
fix(datasource/npm): respect abortOnError hostRule for registries (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Shegox authored Mar 23, 2024
1 parent 3ee1a42 commit 0445d3f
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 10 deletions.
85 changes: 85 additions & 0 deletions lib/modules/datasource/npm/get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
41 changes: 31 additions & 10 deletions lib/modules/datasource/npm/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<NpmResponse>(packageUrl, options);
if (cachedResult?.cacheData && raw.statusCode === 304) {
logger.trace(`Cached npm result for ${packageName} is revalidated`);
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 0445d3f

Please sign in to comment.