From 651e0ccf4f8e8f80978b80efd734032b367b6f7b Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 10 Jul 2026 14:59:29 +0700 Subject: [PATCH 1/3] fix(extensions): handle marketplace outages safely --- electron/extensions/errorUtils.test.ts | 40 +++++++++++++++++++++ electron/extensions/errorUtils.ts | 40 ++++++++++++++++++++- electron/extensions/extensionMarketplace.ts | 10 ++++-- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 electron/extensions/errorUtils.test.ts diff --git a/electron/extensions/errorUtils.test.ts b/electron/extensions/errorUtils.test.ts new file mode 100644 index 000000000..3f35dd2aa --- /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 ae5ccfdac..3119d045d 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 e8bc66ddf..b7a38e46b 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( if (!response.ok) { const text = await response.text().catch(() => ""); - throw new Error(`Marketplace API error ${response.status}: ${text}`); + throw new Error( + formatMarketplaceHttpError({ + status: response.status, + contentType: response.headers.get("content-type"), + body: text, + }), + ); } return (await response.json()) as T; From e032f3705f05fa265887180d04aba7a1cbaa9196 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 11 Jul 2026 11:19:59 +0700 Subject: [PATCH 2/3] test(extensions): cover bounded marketplace details --- electron/extensions/errorUtils.test.ts | 22 ++++++++++++++++++++++ electron/extensions/errorUtils.ts | 10 ++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/electron/extensions/errorUtils.test.ts b/electron/extensions/errorUtils.test.ts index 3f35dd2aa..7e1214114 100644 --- a/electron/extensions/errorUtils.test.ts +++ b/electron/extensions/errorUtils.test.ts @@ -28,6 +28,28 @@ describe("formatMarketplaceHttpError", () => { ).toBe("Marketplace request failed (HTTP 400): Invalid search query"); }); + it("uses a JSON message when an error field is absent", () => { + expect( + formatMarketplaceHttpError({ + status: 409, + contentType: "application/json", + body: JSON.stringify({ message: "Extension version already exists" }), + }), + ).toBe("Marketplace request failed (HTTP 409): Extension version already exists"); + }); + + it("bounds long JSON details and marks truncation without splitting Unicode", () => { + const detail = `🚀${"x".repeat(200)}`; + const message = formatMarketplaceHttpError({ + status: 400, + contentType: "application/problem+json", + body: JSON.stringify({ error: detail }), + }); + + expect(message).toBe(`Marketplace request failed (HTTP 400): 🚀${"x".repeat(198)}…`); + expect(Array.from(message.split(": ")[1])).toHaveLength(200); + }); + it("does not expose non-JSON response bodies", () => { expect( formatMarketplaceHttpError({ diff --git a/electron/extensions/errorUtils.ts b/electron/extensions/errorUtils.ts index 3119d045d..7d44675c3 100644 --- a/electron/extensions/errorUtils.ts +++ b/electron/extensions/errorUtils.ts @@ -25,10 +25,12 @@ export function formatMarketplaceHttpError({ 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); + const normalized = value.trim().replace(/\s+/g, " "); + const codePoints = Array.from(normalized); + detail = + codePoints.length > MAX_MARKETPLACE_ERROR_DETAIL_LENGTH + ? `${codePoints.slice(0, MAX_MARKETPLACE_ERROR_DETAIL_LENGTH - 1).join("")}…` + : normalized; } } } catch { From 2b4d69a2106f9569642667c8bb625b0c205f0da3 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 11 Jul 2026 11:22:22 +0700 Subject: [PATCH 3/3] test(extensions): cover malformed marketplace details --- electron/extensions/errorUtils.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/electron/extensions/errorUtils.test.ts b/electron/extensions/errorUtils.test.ts index 7e1214114..5b834b3b9 100644 --- a/electron/extensions/errorUtils.test.ts +++ b/electron/extensions/errorUtils.test.ts @@ -38,6 +38,28 @@ describe("formatMarketplaceHttpError", () => { ).toBe("Marketplace request failed (HTTP 409): Extension version already exists"); }); + it("prefers a string error when both JSON detail fields are present", () => { + expect( + formatMarketplaceHttpError({ + status: 400, + contentType: "application/json", + body: JSON.stringify({ error: "Primary detail", message: "Secondary detail" }), + }), + ).toBe("Marketplace request failed (HTTP 400): Primary detail"); + }); + + it("hides malformed JSON bodies", () => { + const body = '{"error":"internal route details"'; + const message = formatMarketplaceHttpError({ + status: 400, + contentType: "application/json", + body, + }); + + expect(message).toBe("Marketplace request failed (HTTP 400)."); + expect(message).not.toContain(body); + }); + it("bounds long JSON details and marks truncation without splitting Unicode", () => { const detail = `🚀${"x".repeat(200)}`; const message = formatMarketplaceHttpError({