fix(cli-tools): prevent React crash from object error in Apply handler

Two root causes fixed:
1. Schema: cliModelConfigSchema.apiKey was z.string().optional() but
   the frontend sends null when cloudEnabled is true. Zod rejects null
   for optional strings → validation 400 with structured error object.
   Fix: z.string().nullable().optional()

2. Frontend: All 25 tool card error handlers used data.error directly
   as React children. When data.error is an object ({message, details}),
   React crashes with Error #31 (Objects are not valid as React child).
   Fix: extract data.error.message when data.error is an object.
This commit is contained in:
diegosouzapw
2026-04-26 11:04:52 -03:00
parent 167404f19e
commit aa6b2f7567
10 changed files with 139 additions and 26 deletions

View File

@@ -116,7 +116,11 @@ export default function AntigravityToolCard({
setSudoPassword("");
fetchStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedStart") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) || t("failedStart"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -142,7 +146,11 @@ export default function AntigravityToolCard({
setSudoPassword("");
fetchStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedStop") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) || t("failedStop"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -197,7 +205,10 @@ export default function AntigravityToolCard({
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || t("failedSaveMappings"));
throw new Error(
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedSaveMappings")
);
}
setMessage({ type: "success", text: t("mappingsSaved") });

View File

@@ -169,7 +169,12 @@ export default function ClaudeToolCard({
settings: { ...prev?.settings, env },
}));
} else {
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedApplySettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -191,7 +196,12 @@ export default function ClaudeToolCard({
);
setSelectedApiKey("");
} else {
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedResetSettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -257,7 +267,12 @@ export default function ClaudeToolCard({
checkClaudeStatus();
fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failedRestore") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestore"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -118,7 +118,12 @@ export default function ClineToolCard({
await fetchBackups();
} else {
const data = await res.json();
setMessage({ type: "error", text: data.error || t("failedRestoreBackup") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestoreBackup"),
});
}
} catch (e) {
setMessage({ type: "error", text: e.message });
@@ -173,7 +178,10 @@ export default function ClineToolCard({
await checkClineStatus();
await fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failed") });
setMessage({
type: "error",
text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -195,7 +203,10 @@ export default function ClineToolCard({
await checkClineStatus();
await fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failed") });
setMessage({
type: "error",
text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -75,7 +75,12 @@ export default function CliproxyapiToolCard({ isExpanded, onToggle }) {
await fetchStatus();
if (action === "install" || action === "restart") await fetchUpdateInfo();
} else {
setMessage({ type: "error", text: data.error || `${action} failed` });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
`${action} failed`,
});
}
} catch (err) {
setMessage({ type: "error", text: err instanceof Error ? err.message : "Request failed" });

View File

@@ -178,7 +178,12 @@ export default function CodexToolCard({
setMessage({ type: "success", text: t("settingsApplied") });
checkCodexStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedApplySettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -198,7 +203,12 @@ export default function CodexToolCard({
setSelectedModel("");
checkCodexStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedResetSettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -246,7 +256,12 @@ export default function CodexToolCard({
setNewProfileName("");
fetchProfiles();
} else {
setMessage({ type: "error", text: data.error || t("failedSaveProfile") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedSaveProfile"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -270,7 +285,12 @@ export default function CodexToolCard({
checkCodexStatus();
fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failedActivateProfile") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedActivateProfile"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -318,7 +338,12 @@ export default function CodexToolCard({
checkCodexStatus();
fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failedRestore") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestore"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -249,7 +249,12 @@ export default function DefaultToolCard({
if (res.ok) {
setMessage({ type: "success", text: data.message || t("configurationSaved") });
} else {
setMessage({ type: "error", text: data.error || t("failedToSave") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedToSave"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -150,7 +150,12 @@ export default function DroidToolCard({
setMessage({ type: "success", text: t("settingsApplied") });
checkDroidStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedApplySettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -171,7 +176,12 @@ export default function DroidToolCard({
setSelectedApiKeyId("");
checkDroidStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedResetSettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -211,7 +221,12 @@ export default function DroidToolCard({
checkDroidStatus();
fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failedRestore") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestore"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -104,7 +104,12 @@ export default function KiloToolCard({
await fetchBackups();
} else {
const data = await res.json();
setMessage({ type: "error", text: data.error || t("failedRestoreBackup") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestoreBackup"),
});
}
} catch (e) {
setMessage({ type: "error", text: e.message });
@@ -159,7 +164,10 @@ export default function KiloToolCard({
await checkKiloStatus();
await fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failed") });
setMessage({
type: "error",
text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -181,7 +189,10 @@ export default function KiloToolCard({
await checkKiloStatus();
await fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failed") });
setMessage({
type: "error",
text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -152,7 +152,12 @@ export default function OpenClawToolCard({
setMessage({ type: "success", text: t("settingsApplied") });
checkOpenclawStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedApplySettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -173,7 +178,12 @@ export default function OpenClawToolCard({
setSelectedApiKeyId("");
checkOpenclawStatus();
} else {
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedResetSettings"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });
@@ -213,7 +223,12 @@ export default function OpenClawToolCard({
checkOpenclawStatus();
fetchBackups();
} else {
setMessage({ type: "error", text: data.error || t("failedRestore") });
setMessage({
type: "error",
text:
(typeof data.error === "string" ? data.error : data.error?.message) ||
t("failedRestore"),
});
}
} catch (error) {
setMessage({ type: "error", text: error.message });

View File

@@ -1708,7 +1708,7 @@ export const cliSettingsEnvSchema = z.object({
export const cliModelConfigSchema = z.object({
baseUrl: z.string().trim().min(1, "baseUrl and model are required"),
apiKey: z.string().optional(),
apiKey: z.string().nullable().optional(),
model: z.string().trim().min(1, "baseUrl and model are required"),
reasoningEffort: z.enum(["none", "low", "medium", "high", "xhigh"]).optional(),
wireApi: z.enum(["chat", "responses"]).optional(),