diff --git a/electron/extensions/errorUtils.test.ts b/electron/extensions/errorUtils.test.ts new file mode 100644 index 00000000..3f35dd2a --- /dev/null +++ b/electron/extensions/errorUtils.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { formatMarketplaceHttpError } from "./errorUtils"; + +describe("formatMarketplaceHttpError", () => { + it("hides upstream HTML when the marketplace is unavailable", () => { + const html = "
SSL handshake failed"; + + const message = formatMarketplaceHttpError({ + status: 525, + contentType: "text/html; charset=UTF-8", + body: html, + }); + + expect(message).toBe( + "Marketplace is temporarily unavailable (HTTP 525). Please try again later.", + ); + expect(message).not.toContain(html); + }); + + it("keeps a short JSON error for client-side request failures", () => { + expect( + formatMarketplaceHttpError({ + status: 400, + contentType: "application/json", + body: JSON.stringify({ error: "Invalid search query" }), + }), + ).toBe("Marketplace request failed (HTTP 400): Invalid search query"); + }); + + it("does not expose non-JSON response bodies", () => { + expect( + formatMarketplaceHttpError({ + status: 404, + contentType: "text/plain", + body: "internal route details", + }), + ).toBe("Marketplace request failed (HTTP 404)."); + }); +}); diff --git a/electron/extensions/errorUtils.ts b/electron/extensions/errorUtils.ts index ae5ccfda..3119d045 100644 --- a/electron/extensions/errorUtils.ts +++ b/electron/extensions/errorUtils.ts @@ -1,3 +1,41 @@ export function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); -} \ No newline at end of file +} + +const MAX_MARKETPLACE_ERROR_DETAIL_LENGTH = 200; + +export function formatMarketplaceHttpError({ + status, + contentType, + body, +}: { + status: number; + contentType: string | null; + body: string; +}): string { + if (status >= 500) { + return `Marketplace is temporarily unavailable (HTTP ${status}). Please try again later.`; + } + + let detail: string | null = null; + if (contentType?.toLowerCase().includes("json")) { + try { + const payload: unknown = JSON.parse(body); + if (payload && typeof payload === "object") { + const { error, message } = payload as { error?: unknown; message?: unknown }; + const value = typeof error === "string" ? error : message; + if (typeof value === "string" && value.trim()) { + detail = value + .trim() + .replace(/\s+/g, " ") + .slice(0, MAX_MARKETPLACE_ERROR_DETAIL_LENGTH); + } + } + } catch { + // Malformed or non-API responses are intentionally not exposed to the renderer. + } + } + + const summary = `Marketplace request failed (HTTP ${status})`; + return detail ? `${summary}: ${detail}` : `${summary}.`; +} diff --git a/electron/extensions/extensionMarketplace.ts b/electron/extensions/extensionMarketplace.ts index e8bc66dd..b7a38e46 100644 --- a/electron/extensions/extensionMarketplace.ts +++ b/electron/extensions/extensionMarketplace.ts @@ -12,7 +12,7 @@ import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { ReadableStream as NodeReadableStream } from "node:stream/web"; import { app } from "electron"; -import { getErrorMessage } from "./errorUtils"; +import { formatMarketplaceHttpError, getErrorMessage } from "./errorUtils"; import { getRegisteredExtensions, installExtensionFromPath } from "./extensionLoader"; import type { ExtensionReview, @@ -97,7 +97,13 @@ async function marketplaceFetch