diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 93b04f5a6..f78dc3810 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -2405,7 +2405,7 @@ async function loadObjects(options?: { allowCached?: boolean }) { void loadObjectStatistics(request, cacheWriteToken, cachedAt); } catch (e: any) { if (!objectBrowserRowsLoadGuard.isCurrent(request)) return; - error.value = e?.message || String(e); + error.value = translateBackendError(t, e); } finally { if (objectBrowserRowsLoadGuard.isCurrent(request)) finishObjectBrowserRowsLoad(); } diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 86c83743d..42ca4dfc0 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -720,7 +720,7 @@ async function toggle() { if (!wasExpanded) node.isExpanded = false; const errMsg = e?.message || String(e); if (errMsg.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return; - toast(t("connection.connectFailed", { message: translateBackendError(t, errMsg) }), 5000); + toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000); openDriverStoreForInstallError(errMsg); } } diff --git a/apps/desktop/src/i18n/__tests__/backendErrors.spec.ts b/apps/desktop/src/i18n/__tests__/backendErrors.spec.ts index ac2f0af14..e47d427ab 100644 --- a/apps/desktop/src/i18n/__tests__/backendErrors.spec.ts +++ b/apps/desktop/src/i18n/__tests__/backendErrors.spec.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it as test } from "vitest"; import { createI18n } from "vue-i18n"; import { translateBackendError, type BackendErrorTranslate } from "@/i18n/backend-errors"; -import { BackendErrorException, normalizeBackendError } from "@/lib/backend/errorUtils"; +import { BackendErrorException, formatError, normalizeBackendError, sanitizeBackendErrorMessage } from "@/lib/backend/errorUtils"; import en from "@/i18n/locales/en"; import es from "@/i18n/locales/es"; import it from "@/i18n/locales/it"; @@ -171,6 +171,23 @@ describe("backend error translation", () => { expect(translateBackendError(t, "some driver specific failure")).toBe("some driver specific failure"); }); + test("hides internal Agent error data from user-facing messages", () => { + const t = translatorFor("zh-CN"); + const message = 'Agent RPC error (-1): driver: bad connection\nDBX_AGENT_ERROR_DATA:{"category":null,"retryable":null,"sessionDisposition":null,"stage":null,"operationOutcome":null,"agentSessionId":"e1a4d0a2907947b8adf31abb10c4dff9"}'; + const expected = "Agent RPC error (-1): driver: bad connection"; + + expect(sanitizeBackendErrorMessage(message)).toBe(expected); + expect(formatError(new Error(message))).toBe(expected); + expect(new BackendErrorException(message).message).toBe(expected); + expect(translateBackendError(t, message)).toBe(expected); + }); + + test("preserves marker-like database messages without valid internal data", () => { + const message = "database returned\nDBX_AGENT_ERROR_DATA:not-json"; + + expect(sanitizeBackendErrorMessage(message)).toBe(message); + }); + test("normalizes Error and structural message objects before translation", () => { const t = translatorFor("zh-CN"); const message = "file does not exist: /tmp/missing.sqlite"; @@ -213,6 +230,22 @@ describe("backend error translation", () => { expect(translateBackendError(t, error)).toBe(`${t(error.messageKey)}\n\n${error.detail}`); }); + test("hides internal Agent error data from structured error details", () => { + const t = translatorFor("zh-CN"); + const detail = 'driver: bad connection\nDBX_AGENT_ERROR_DATA:{"category":null,"agentSessionId":"session-1"}'; + const error = { + version: 1, + code: "DBX-JDBC-9001", + messageKey: "backendErrors.jdbc.legacyFailure", + messageParams: {}, + source: "jdbcAgentLegacy", + operationOutcome: "unknown", + detail, + } as const; + + expect(translateBackendError(t, error)).toBe(`${t(error.messageKey)}\n\ndriver: bad connection`); + }); + test.each([ ["array params", { messageParams: ["execute"] }], ["nested params", { messageParams: { stage: { name: "execute" } } }], diff --git a/apps/desktop/src/i18n/backend-errors.ts b/apps/desktop/src/i18n/backend-errors.ts index 334252020..9594f467b 100644 --- a/apps/desktop/src/i18n/backend-errors.ts +++ b/apps/desktop/src/i18n/backend-errors.ts @@ -1,4 +1,4 @@ -import { normalizeBackendError, type BackendError } from "@/lib/backend/errorUtils"; +import { normalizeBackendError, sanitizeBackendErrorMessage, type BackendError } from "@/lib/backend/errorUtils"; /** * Minimal shape of a translate function, satisfied by both `useI18n().t` inside @@ -115,15 +115,15 @@ const paramNames: Record = { }; function backendErrorMessage(error: unknown): string { - if (typeof error === "string") return error; - if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message; - return String(error); + if (typeof error === "string") return sanitizeBackendErrorMessage(error); + if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return sanitizeBackendErrorMessage(error.message); + return sanitizeBackendErrorMessage(String(error)); } function translateStructuredBackendError(t: BackendErrorTranslate, error: BackendError): string { const translated = t(error.messageKey, error.messageParams); const summary = translated !== error.messageKey ? translated : t("backendErrors.unknown"); - const detail = error.detail?.trim(); + const detail = error.detail ? sanitizeBackendErrorMessage(error.detail).trim() : undefined; return detail && detail !== summary ? `${summary}\n\n${detail}` : summary; } diff --git a/apps/desktop/src/lib/backend/errorUtils.ts b/apps/desktop/src/lib/backend/errorUtils.ts index 4a0e0a9ce..72ad60dbb 100644 --- a/apps/desktop/src/lib/backend/errorUtils.ts +++ b/apps/desktop/src/lib/backend/errorUtils.ts @@ -16,6 +16,23 @@ export interface BackendError { helpUrl?: string; } +const AGENT_RPC_ERROR_DATA_MARKER = "\nDBX_AGENT_ERROR_DATA:"; + +export function sanitizeBackendErrorMessage(message: string): string { + const markerIndex = message.lastIndexOf(AGENT_RPC_ERROR_DATA_MARKER); + if (markerIndex < 0) return message; + + const rawData = message.slice(markerIndex + AGENT_RPC_ERROR_DATA_MARKER.length).trim(); + try { + const data: unknown = JSON.parse(rawData); + if (!data || typeof data !== "object" || Array.isArray(data)) return message; + } catch { + return message; + } + + return message.slice(0, markerIndex).trimEnd(); +} + function isBackendError(value: unknown): value is BackendError { if (!value || typeof value !== "object") return false; const candidate = value as Record; @@ -56,8 +73,8 @@ export class BackendErrorException extends Error { constructor(error: unknown) { const backendError = normalizeRawBackendError(error); - const fallbackMessage = typeof error === "string" ? error : error instanceof Error ? error.message : "Backend request failed"; - super(backendError?.detail || fallbackMessage); + const fallbackMessage = sanitizeBackendErrorMessage(typeof error === "string" ? error : error instanceof Error ? error.message : "Backend request failed"); + super(backendError?.detail ? sanitizeBackendErrorMessage(backendError.detail) : fallbackMessage); this.name = "BackendErrorException"; this.backendError = backendError ?? { version: 1, @@ -99,14 +116,14 @@ function normalizeRawBackendError(error: unknown): BackendError | null { */ export function formatError(e: unknown): string { const backendError = normalizeBackendError(e); - if (backendError?.detail) return backendError.detail; + if (backendError?.detail) return sanitizeBackendErrorMessage(backendError.detail); if (e instanceof Error) { - return e.message; + return sanitizeBackendErrorMessage(e.message); } if (typeof e === "string") { - return e; + return sanitizeBackendErrorMessage(e); } if (e === null || e === undefined) { @@ -117,13 +134,13 @@ export function formatError(e: unknown): string { if (typeof e === "object" && "message" in e) { const message = (e as { message: unknown }).message; if (typeof message === "string") { - return message; + return sanitizeBackendErrorMessage(message); } } // Fallback: attempt to stringify try { - return String(e); + return sanitizeBackendErrorMessage(String(e)); } catch { return "Unknown error occurred"; }