fix(extensions): handle marketplace outages safely

This commit is contained in:
wiiiii123
2026-07-10 14:59:29 +07:00
parent 641d2230a4
commit 651e0ccf4f
3 changed files with 87 additions and 3 deletions
+40
View File
@@ -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 = "<!DOCTYPE html><html><body>SSL handshake failed</body></html>";
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).");
});
});
+39 -1
View File
@@ -1,3 +1,41 @@
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
}
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}.`;
}
+8 -2
View File
@@ -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<T>(
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;