From bccabfeff70eab8b94c482676fa83cc0141ef5d1 Mon Sep 17 00:00:00 2001 From: macayu17 Date: Sun, 24 May 2026 20:31:51 +0530 Subject: [PATCH 1/3] fix: add OSV HTTP error hints --- src/index.ts | 19 ++++++++++++- src/utils/network.ts | 52 +++++++++++++++++++++++++++++++++-- tests/cli-integration.test.ts | 38 +++++++++++++++++++++++++ tests/network.test.ts | 35 ++++++++++++++++++++++- 4 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 30627b5..08048d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,16 @@ import { createSpinner } from "./output/spinner.js"; import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js"; import { scanProjectForPackageUsage } from "./usage/scanner.js"; import { getCliVersion } from "./utils/version-info.js"; -import { isLikelyBlockedAdvisoryRequestError, isSslCertificateError, sslCertificateErrorHint, blockedAdvisoryRequestHint } from "./utils/network.js"; +import { + blockedAdvisoryRequestHint, + isLikelyBlockedAdvisoryRequestError, + isRateLimitError, + isServerError, + isSslCertificateError, + rateLimitAdvisoryRequestHint, + serverAdvisoryRequestHint, + sslCertificateErrorHint, +} from "./utils/network.js"; import { formatAdvisoryDbFreshness } from "./utils/time.js"; import type { SuggestedFixCommandPlan, SuggestedFixTarget } from "./remediation/fix-commands.js"; import type { ParsedOptions } from "./types.js"; @@ -290,6 +299,14 @@ if (parsedArgs) { const [hint, ...rest] = sslCertificateErrorHint(); console.error(chalk.yellow(hint)); rest.forEach(line => console.error(chalk.gray(line))); + } else if (isRateLimitError(errorMessage)) { + const [hint, ...rest] = rateLimitAdvisoryRequestHint(); + console.error(chalk.yellow(hint)); + rest.forEach(line => console.error(chalk.gray(line))); + } else if (isServerError(errorMessage)) { + const [hint, ...rest] = serverAdvisoryRequestHint(); + console.error(chalk.yellow(hint)); + rest.forEach(line => console.error(chalk.gray(line))); } else if (isLikelyBlockedAdvisoryRequestError(errorMessage)) { const [hint, ...rest] = blockedAdvisoryRequestHint(); console.error(chalk.yellow(hint)); diff --git a/src/utils/network.ts b/src/utils/network.ts index 69e4e3b..7f4c4ab 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -23,6 +23,22 @@ export function blockedAdvisoryRequestHint(): string[] { ]; } +export function rateLimitAdvisoryRequestHint(): string[] { + return [ + "Hint: OSV API rate limit reached. Wait a moment and retry, or scan offline:", + " cve-lite . --offline", + " (requires a local advisory DB - run `cve-lite advisories sync` to build one)", + ]; +} + +export function serverAdvisoryRequestHint(): string[] { + return [ + "Hint: OSV API may be temporarily unavailable. Wait a moment and retry, or scan offline:", + " cve-lite . --offline", + " (requires a local advisory DB - run `cve-lite advisories sync` to build one)", + ]; +} + const SSL_ERROR_CODES = new Set([ "SELF_SIGNED_CERT_IN_CHAIN", "CERT_UNTRUSTED", @@ -41,6 +57,15 @@ const SSL_ERROR_MESSAGE_FRAGMENTS = [ "unable to verify the first certificate", ]; +function finalErrorCause(message: string): string { + const normalized = message.toLowerCase(); + return normalized.split(":").pop()?.trim() ?? normalized; +} + +function isOsvError(message: string): boolean { + return message.includes("OSV"); +} + export function isSslCertificateError(error: unknown): boolean { if (!(error instanceof Error)) return false; const code = (error as NodeJS.ErrnoException).code; @@ -51,13 +76,34 @@ export function isSslCertificateError(error: unknown): boolean { return SSL_ERROR_MESSAGE_FRAGMENTS.some(f => normalized.includes(f)); } +export function isRateLimitError(message: string): boolean { + if (!isOsvError(message)) { + return false; + } + + const finalCause = finalErrorCause(message); + return /^429\b/.test(finalCause); +} + +export function isServerError(message: string): boolean { + if (!isOsvError(message)) { + return false; + } + + const finalCause = finalErrorCause(message); + return /^5\d\d\b/.test(finalCause); +} + export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { - if (!message.includes("OSV")) { + if (!isOsvError(message)) { return false; } - const normalized = message.toLowerCase(); - const finalCause = normalized.split(":").pop()?.trim() ?? normalized; + if (isRateLimitError(message) || isServerError(message)) { + return false; + } + + const finalCause = finalErrorCause(message); const blockedIndicators = [ "access denied", "blocked", diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index a0d043b..fcc30e8 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -518,6 +518,44 @@ describe("CLI integration", () => { expect(stderr).toContain("cve-lite advisories sync --output /path/to/advisories.db"); }); + it("prints a retry and offline hint when OSV rate limits the scan", async () => { + loadPackagesMock.mockReturnValue(createScanInput({ + packages: [{ name: "lodash", version: "4.17.21", ecosystem: "npm", paths: [["project", "lodash"]] }], + })); + scanPackagesMock.mockRejectedValue( + new Error("OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests"), + ); + + const result = await runIndexModule(); + const stderr = stripAnsi(result.stderr.join("\n")); + + expect(result.exitCode).toBe(1); + expect(stderr).toContain("Error: OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests"); + expect(stderr).toContain("Hint: OSV API rate limit reached. Wait a moment and retry, or scan offline:"); + expect(stderr).toContain("cve-lite . --offline"); + expect(stderr).toContain("cve-lite advisories sync"); + expect(stderr).not.toContain("Outbound access to the OSV API may be blocked"); + }); + + it("prints a transient service hint when OSV returns a server error", async () => { + loadPackagesMock.mockReturnValue(createScanInput({ + packages: [{ name: "lodash", version: "4.17.21", ecosystem: "npm", paths: [["project", "lodash"]] }], + })); + scanPackagesMock.mockRejectedValue( + new Error("OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable"), + ); + + const result = await runIndexModule(); + const stderr = stripAnsi(result.stderr.join("\n")); + + expect(result.exitCode).toBe(1); + expect(stderr).toContain("Error: OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable"); + expect(stderr).toContain("Hint: OSV API may be temporarily unavailable. Wait a moment and retry, or scan offline:"); + expect(stderr).toContain("cve-lite . --offline"); + expect(stderr).toContain("cve-lite advisories sync"); + expect(stderr).not.toContain("Outbound access to the OSV API may be blocked"); + }); + it("routes verbose mode through the detailed printer pipeline", async () => { const finding = createFinding({ severity: "medium" }); parseArgsMock.mockReturnValue({ diff --git a/tests/network.test.ts b/tests/network.test.ts index 0864cac..aa91320 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -1,4 +1,4 @@ -import { isLikelyBlockedAdvisoryRequestError, isSslCertificateError, extractErrorMessage } from "../src/utils/network.js"; +import { isLikelyBlockedAdvisoryRequestError, isRateLimitError, isServerError, isSslCertificateError, extractErrorMessage } from "../src/utils/network.js"; describe("extractErrorMessage", () => { it("returns the message of a plain Error", () => { @@ -77,3 +77,36 @@ describe("isLikelyBlockedAdvisoryRequestError", () => { ).toBe(false); }); }); + +describe("isRateLimitError", () => { + it("returns true for OSV 429 responses", () => { + expect( + isRateLimitError( + "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests", + ), + ).toBe(true); + }); + + it("returns false for non-OSV 429 responses", () => { + expect(isRateLimitError("API failed: 429 Too Many Requests")).toBe(false); + }); +}); + +describe("isServerError", () => { + it("returns true for OSV 5xx responses", () => { + for (const message of [ + "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 500 Internal Server Error", + "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable", + ]) { + expect(isServerError(message)).toBe(true); + } + }); + + it("returns false for OSV client errors", () => { + expect( + isServerError( + "OSV vuln fetch failed for OSV-404 via https://api.osv.dev: OSV vuln fetch failed for OSV-404: 404 Not Found", + ), + ).toBe(false); + }); +}); From 8b7450aad5a944c1a150d4ef78bafd2499100c51 Mon Sep 17 00:00:00 2001 From: Ayush <63203492+macayu17@users.noreply.github.com> Date: Sun, 24 May 2026 20:39:32 +0530 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/utils/network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/network.ts b/src/utils/network.ts index 7f4c4ab..147c42e 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -63,7 +63,7 @@ function finalErrorCause(message: string): string { } function isOsvError(message: string): boolean { - return message.includes("OSV"); + return message.toLowerCase().includes("osv"); } export function isSslCertificateError(error: unknown): boolean { From 40a116a5e92c16bac5397b6be8adb49161ed7b5c Mon Sep 17 00:00:00 2001 From: macayu17 Date: Mon, 25 May 2026 18:09:18 +0530 Subject: [PATCH 3/3] fix: address OSV hint review --- src/utils/network.ts | 31 ++++++++++++++++++++----------- tests/cli-integration.test.ts | 1 + tests/network.test.ts | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/utils/network.ts b/src/utils/network.ts index 147c42e..7b25fcf 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -23,19 +23,22 @@ export function blockedAdvisoryRequestHint(): string[] { ]; } +const OFFLINE_FALLBACK_LINES = [ + " cve-lite . --offline", + " (requires a local advisory DB - run `cve-lite advisories sync` to build one)", +]; + export function rateLimitAdvisoryRequestHint(): string[] { return [ "Hint: OSV API rate limit reached. Wait a moment and retry, or scan offline:", - " cve-lite . --offline", - " (requires a local advisory DB - run `cve-lite advisories sync` to build one)", + ...OFFLINE_FALLBACK_LINES, ]; } export function serverAdvisoryRequestHint(): string[] { return [ "Hint: OSV API may be temporarily unavailable. Wait a moment and retry, or scan offline:", - " cve-lite . --offline", - " (requires a local advisory DB - run `cve-lite advisories sync` to build one)", + ...OFFLINE_FALLBACK_LINES, ]; } @@ -66,6 +69,14 @@ function isOsvError(message: string): boolean { return message.toLowerCase().includes("osv"); } +function isRateLimitCause(finalCause: string): boolean { + return /^429\b/.test(finalCause); +} + +function isServerCause(finalCause: string): boolean { + return /^5\d\d\b/.test(finalCause); +} + export function isSslCertificateError(error: unknown): boolean { if (!(error instanceof Error)) return false; const code = (error as NodeJS.ErrnoException).code; @@ -81,8 +92,7 @@ export function isRateLimitError(message: string): boolean { return false; } - const finalCause = finalErrorCause(message); - return /^429\b/.test(finalCause); + return isRateLimitCause(finalErrorCause(message)); } export function isServerError(message: string): boolean { @@ -90,8 +100,7 @@ export function isServerError(message: string): boolean { return false; } - const finalCause = finalErrorCause(message); - return /^5\d\d\b/.test(finalCause); + return isServerCause(finalErrorCause(message)); } export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { @@ -99,11 +108,11 @@ export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { return false; } - if (isRateLimitError(message) || isServerError(message)) { + const finalCause = finalErrorCause(message); + if (isRateLimitCause(finalCause) || isServerCause(finalCause)) { return false; } - const finalCause = finalErrorCause(message); const blockedIndicators = [ "access denied", "blocked", @@ -131,5 +140,5 @@ export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { return true; } - return /^(401|403|407|408|429|451|502|503|504)\b/.test(finalCause); + return /^(401|403|407|408|451)\b/.test(finalCause); } diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index fcc30e8..8183eee 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -84,6 +84,7 @@ jest.unstable_mockModule("../src/output/formatters.js", () => ({ printCacheSummary: printCacheSummaryMock, serializeFinding: serializeFindingMock, sortFindingsForOutput: sortFindingsForOutputMock, + formatAdvisorySourceLine: jest.fn(sourceLabel => sourceLabel), getRecommendedAction: jest.fn(() => "Upgrade to latest"), })); diff --git a/tests/network.test.ts b/tests/network.test.ts index aa91320..4cc5c4f 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -76,6 +76,15 @@ describe("isLikelyBlockedAdvisoryRequestError", () => { ), ).toBe(false); }); + + it("returns false for OSV rate-limit and server responses", () => { + for (const message of [ + "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests", + "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable", + ]) { + expect(isLikelyBlockedAdvisoryRequestError(message)).toBe(false); + } + }); }); describe("isRateLimitError", () => { @@ -87,6 +96,14 @@ describe("isRateLimitError", () => { ).toBe(true); }); + it("matches OSV errors case-insensitively", () => { + expect( + isRateLimitError( + "osv batch query failed for https://api.osv.dev: osv batch query failed: 429 Too Many Requests", + ), + ).toBe(true); + }); + it("returns false for non-OSV 429 responses", () => { expect(isRateLimitError("API failed: 429 Too Many Requests")).toBe(false); }); @@ -109,4 +126,8 @@ describe("isServerError", () => { ), ).toBe(false); }); + + it("returns false for non-OSV 5xx responses", () => { + expect(isServerError("API failed: 500 Internal Server Error")).toBe(false); + }); });