mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
Merge pull request #743 from webadderallorg/fix/735-marketplace-error-response
fix(extensions): handle marketplace outages safely
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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("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("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({
|
||||
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({
|
||||
status: 404,
|
||||
contentType: "text/plain",
|
||||
body: "internal route details",
|
||||
}),
|
||||
).toBe("Marketplace request failed (HTTP 404).");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,43 @@
|
||||
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()) {
|
||||
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 {
|
||||
// Malformed or non-API responses are intentionally not exposed to the renderer.
|
||||
}
|
||||
}
|
||||
|
||||
const summary = `Marketplace request failed (HTTP ${status})`;
|
||||
return detail ? `${summary}: ${detail}` : `${summary}.`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user