fix(kv): hide internal agent error data in browser messages

This commit is contained in:
Thanadon Kongkanun authored and GitHub committed 2026-09-11 16:49:23 +08:00
1 parent 5fa0c5a56d
commit 19be861c49
4 files changed
+46 -21

No files matched your search

@@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { useToast } from "@/composables/useToast";
import { useTabUiState } from "@/lib/tabs/tabUiState";
import * as api from "@/lib/backend/api";
import { formatError } from "@/lib/backend/errorUtils";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { isKeyInKvExportScope, kvExportFilenameStem, kvValueByteIdentity, type KvExportScopeKind, type KvExportScopeRequest } from "@/lib/kv/kvExportScope";
import { detectKvValueFormat } from "@/lib/kv/kvValueFormat";
@@ -596,7 +597,7 @@ async function exportTreeSelection(format: EtcdExportFormat) {
const exported = await downloadExport(file);
if (exported) toast(t("etcd.exported", { count: entries.length }), 2500);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -623,7 +624,7 @@ async function deleteSelectedTreeKeys() {
toast(t("etcd.batchDeleteSuccess", { count: deleted }), 3000);
} catch (error) {
failed = true;
const message = error instanceof Error ? error.message : String(error);
const message = formatError(error);
toast(t("etcd.batchDeletePartial", { count: deleted, error: message }), 5000);
} finally {
if (failed) browserRef.value?.clearMultiSelection();
@@ -711,7 +712,7 @@ async function exportAll(format: EtcdExportFormat) {
const exported = await downloadExport(file);
if (exported) toast(t("etcd.exported", { count: scan.entries.length }), 2500);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -744,7 +745,7 @@ async function exportEtcdNodeScope(connectionId: string, request: KvExportScopeR
const exported = await downloadExport(file);
if (exported) toast(t("etcd.exported", { count: entries.length }), 2500);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -776,7 +777,7 @@ async function onImportFile(event: Event) {
transferOpen.value = true;
await previewTransfer();
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -825,7 +826,7 @@ async function loadSyncPreview() {
transferBundle.value = bundleFromSummaries(scan.entries, prefix, scan.revision);
await previewTransfer();
} catch (error) {
transferError.value = error instanceof Error ? error.message : String(error);
transferError.value = formatError(error);
} finally {
transferLoading.value = false;
transferLoadingDetail.value = "";
@@ -870,7 +871,7 @@ async function previewTransfer() {
if (transferMode.value === "sync") syncConfigurationExpanded.value = false;
} catch (error) {
if (generation !== transferPreviewGeneration) return;
transferError.value = error instanceof Error ? error.message : String(error);
transferError.value = formatError(error);
transferPreviewLoaded.value = false;
} finally {
if (generation === transferPreviewGeneration) {
@@ -915,7 +916,7 @@ async function applyTransfer() {
transferOpen.value = false;
if (targetId === props.connectionId) browserRef.value?.refresh();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const message = formatError(error);
// Rebuild the preview so an ambiguous network failure or a successful
// prefix of the batch is reflected before the user retries.
let previewError = "";
@@ -1032,7 +1033,7 @@ async function runSearch() {
return { id: `${identity}:${summary.modRevision || ""}`, displayKey: shown, keyIdentity: identity, summary: { ...summary, key: shown, keyBytes: bytes, keyIdentity: identity }, matchesKey, matchesValue, selected: true };
});
} catch (error) {
searchError.value = error instanceof Error ? error.message : String(error);
searchError.value = formatError(error);
} finally {
searchRunning.value = false;
}
@@ -1063,7 +1064,7 @@ async function openOperations(nextMode: Extract<WorkbenchMode, "maintenance" | "
try {
operationsStatus.value = await api.etcdStatus(props.connectionId);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 5000);
toast(formatError(error), 5000);
} finally {
operationsLoading.value = false;
}
@@ -1083,7 +1084,7 @@ async function refreshLeaseOptions() {
try {
leaseOptions.value = (await api.etcdLeaseList(props.connectionId)).leases;
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -1099,7 +1100,7 @@ async function exportSearchResults(format: EtcdExportFormat) {
const exported = await downloadExport(file);
if (exported) toast(t("etcd.exported", { count: selected.length }), 2500);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 4000);
toast(formatError(error), 4000);
}
}
@@ -18,6 +18,7 @@ import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomC
import NacosConfigDiffDialog from "@/components/nacos/NacosConfigDiffDialog.vue";
import KvValueEditor from "@/components/kv/KvValueEditor.vue";
import type { KvCreateMode, KvDeleteOptions, KvGetOptions, KvGetResponse, KvHistoryEvent, KvHistoryResponse, KvInt64, KvKeySummary, KvListPrefixOptions, KvPutOptions, KvPutResponse, KvValue } from "@/lib/backend/api";
import { formatError } from "@/lib/backend/errorUtils";
import type { KvExportScopeRequest } from "@/lib/kv/kvExportScope";
import { buildKvKeyTree, flattenVisibleKvKeyTree, kvKeyTreeNodePath, preserveKvExpandedGroupIds, type KvKeyTreeNode } from "@/lib/kv/kvKeyTree";
import { decideKvMetadataRefresh, hasPositiveKvLease, knownKvLeaseSummaries, mergeKvKeyMetadata, mergeKvValueRefresh, nextKvLeaseRefreshDelay, removeMissingKvKey, updateKvResponseTtl } from "@/lib/kv/kvMetadataRefresh";
@@ -712,7 +713,7 @@ async function loadKeys(reset = true, options: LoadKeysOptions = {}) {
}
} catch (error) {
if (reset && generation === keyLoadGeneration && props.connectionId === connectionId) {
listError.value = error instanceof Error ? error.message : String(error);
listError.value = formatError(error);
}
} finally {
if (generation === keyLoadGeneration && props.connectionId === connectionId) {
@@ -792,7 +793,7 @@ async function loadLazyRoot(reset = true, options: LoadKeysOptions = {}) {
}
} catch (error) {
if (lazyLoadContextValid(context)) {
listError.value = error instanceof Error ? error.message : String(error);
listError.value = formatError(error);
}
} finally {
if (lazyLoadContextValid(context)) loading.value = false;
@@ -966,7 +967,7 @@ async function loadSelectedKey(input: string | KvKeyRoute) {
startKeyListRefresh();
} catch (error) {
if (requestId !== detailRequestId || selectedKey.value !== key || connectionId !== props.connectionId) return;
detailError.value = error instanceof Error ? error.message : String(error);
detailError.value = formatError(error);
} finally {
if (requestId === detailRequestId && connectionId === props.connectionId) detailLoading.value = false;
}
@@ -1414,7 +1415,7 @@ async function deleteSelectedKey() {
}
toast(props.labels.deleted, 2500);
} catch (error) {
detailError.value = error instanceof Error ? error.message : String(error);
detailError.value = formatError(error);
showDeleteConfirm.value = false;
} finally {
deleting.value = false;
@@ -1471,7 +1472,7 @@ async function copySelectedValue() {
}, 1500);
} catch (error) {
selectedValueCopied.value = false;
const message = error instanceof Error ? error.message : String(error);
const message = formatError(error);
const failureTemplate = props.labels.copyFailed || "Copy failed: {message}";
toast(failureTemplate.includes("{message}") ? failureTemplate.replace("{message}", message) : `${failureTemplate}: ${message}`, 3500);
}
@@ -1603,7 +1604,7 @@ async function moveOrCopySelectedKey() {
await loadSelectedKey({ key: next, keyIdentity: next, keyBytes: { encoding: "utf8", data: next } });
toast(props.labels.saved, 2500);
} catch (error) {
renameError.value = error instanceof Error ? error.message : String(error);
renameError.value = formatError(error);
} finally {
renaming.value = false;
}
@@ -1626,7 +1627,7 @@ async function openHistory() {
historyEvents.value = response.events;
} catch (error) {
historyEvents.value = [];
historyError.value = error instanceof Error ? error.message : String(error);
historyError.value = formatError(error);
} finally {
historyLoading.value = false;
}
@@ -1650,7 +1651,7 @@ async function restoreHistory() {
await loadKeys(true, { preserveSelection: true });
toast(props.labels.saved, 2500);
} catch (error) {
historyError.value = error instanceof Error ? error.message : String(error);
historyError.value = formatError(error);
} finally {
restoring.value = false;
}
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { formatError, sanitizeBackendErrorMessage } from "@/lib/backend/errorUtils";
// Reported in #8860: an etcd revision that has already been compacted surfaces
// its internal Agent contract payload in the history dialog.
const COMPACTED_HISTORY_ERROR =
'ETCD_COMPACTED: requested history was compacted at revision 6538169\nDBX_AGENT_ERROR_DATA:{"contractVersion":1,"category":"protocol","retryable":false,"sessionDisposition":"keep","stage":"execute","operationOutcome":"unknown","agentSessionId":null,"sqlState":null,"vendorCode":null,"exceptionClass":"ETCD_COMPACTED:requestedhistorywascompactedatrevision6538169"}';
describe("KV browser error presentation", () => {
it("keeps the internal Agent payload out of a compacted-revision history error", () => {
const expected = "ETCD_COMPACTED: requested history was compacted at revision 6538169";
expect(sanitizeBackendErrorMessage(COMPACTED_HISTORY_ERROR)).toBe(expected);
expect(formatError(new Error(COMPACTED_HISTORY_ERROR))).toBe(expected);
});
it("leaves an error without the internal marker untouched", () => {
const message = "etcdserver: permission denied";
expect(formatError(new Error(message))).toBe(message);
expect(formatError(message)).toBe(message);
});
});
@@ -42,7 +42,7 @@ describe("KvKeyBrowser search and split layout", () => {
it("shows list request errors instead of the empty-state message", () => {
expect(browserSource).toContain('const listError = ref("");');
expect(browserSource.match(/listError\.value = error instanceof Error \? error\.message : String\(error\);/g)).toHaveLength(2);
expect(browserSource.match(/listError\.value = formatError\(error\);/g)).toHaveLength(2);
expect(browserSource.indexOf('v-else-if="listError"')).toBeLessThan(browserSource.indexOf('v-else-if="visibleRows.length === 0"'));
});