Clarify server delete API errors

This commit is contained in:
npc0-hue
2026-08-04 11:58:48 +08:00
parent de1f0d107f
commit 8a25f52d68
2 changed files with 61 additions and 1 deletions
+32
View File
@@ -810,6 +810,38 @@ describe("PlatformApiClient AI providers", () => {
expect(onAuthFailure).not.toHaveBeenCalled();
});
it("surfaces safe validation details for delete denials", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
code: "validation_failed",
message: "validation failed",
details: [
"running or installing server instances must be stopped before delete",
"config path /Users/operator/private/server.ini is unavailable"
]
}), { status: 400, headers: { "Content-Type": "application/json" } })));
const client = new PlatformApiClient("/api/v1", () => "owner-session");
await expect(client.deleteServerInstance("running-server", { password: "secret-password" })).rejects.toMatchObject({
status: 400,
code: "validation_failed",
message: "运行中或安装中的服务器必须先停止再删除。;config path [host-path] is unavailable"
});
});
it("surfaces password confirmation denials without exposing generic forbidden text", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
code: "forbidden",
message: "password confirmation failed"
}), { status: 403, headers: { "Content-Type": "application/json" } })));
const client = new PlatformApiClient("/api/v1", () => "owner-session");
await expect(client.deleteServerInstance("server-1", { password: "wrong-password" })).rejects.toMatchObject({
status: 403,
code: "forbidden",
message: "当前登录密码不正确,请重新输入。"
});
});
it("surfaces allow-listed plugin capability denials", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
code: "forbidden",
+29 -1
View File
@@ -752,7 +752,9 @@ async function responseError(response: Response): Promise<PlatformApiError> {
const apiError = await safeReadError(response);
const message = response.status === 401
? "会话已失效,请重新登录。"
: response.status === 403
: apiError?.code === "validation_failed"
? safeValidationMessage(apiError)
: response.status === 403
? safeForbiddenMessage(apiError?.message)
: apiError?.message ?? `request failed: ${response.status}`;
const error = new PlatformApiError(response.status, apiError?.code ?? "request_failed", message);
@@ -763,11 +765,37 @@ async function responseError(response: Response): Promise<PlatformApiError> {
return error;
}
function safeValidationMessage(apiError?: ApiErrorResponse | null): string {
const details = apiError?.details
?.map((detail) => safeValidationDetail(detail))
.filter((detail): detail is string => Boolean(detail));
if (details?.length) {
return details.slice(0, 3).join("");
}
const sanitized = safeDiagnosticText(apiError?.message, "")?.trim();
return sanitized || "请求校验失败。";
}
function safeValidationDetail(detail: string): string | undefined {
const sanitized = safeDiagnosticText(detail, "")?.trim();
switch (sanitized) {
case "password is required":
return "请输入当前登录密码。";
case "running or installing server instances must be stopped before delete":
return "运行中或安装中的服务器必须先停止再删除。";
default:
return sanitized || undefined;
}
}
function safeForbiddenMessage(apiMessage?: string): string {
const sanitized = safeDiagnosticText(apiMessage, "")?.trim();
if (!sanitized || sanitized === "account is not allowed to access this resource") {
return "没有权限访问该资源。";
}
if (sanitized === "password confirmation failed") {
return "当前登录密码不正确,请重新输入。";
}
const missingPermission = sanitized.match(/^plugin does not declare required permission:\s*([a-z0-9._-]+)$/i);
if (missingPermission) {
return `插件未声明所需权限:${missingPermission[1]}`;