Clarify server delete API errors
This commit is contained in:
@@ -810,6 +810,38 @@ describe("PlatformApiClient AI providers", () => {
|
|||||||
expect(onAuthFailure).not.toHaveBeenCalled();
|
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 () => {
|
it("surfaces allow-listed plugin capability denials", async () => {
|
||||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
|
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
|
||||||
code: "forbidden",
|
code: "forbidden",
|
||||||
|
|||||||
@@ -752,7 +752,9 @@ async function responseError(response: Response): Promise<PlatformApiError> {
|
|||||||
const apiError = await safeReadError(response);
|
const apiError = await safeReadError(response);
|
||||||
const message = response.status === 401
|
const message = response.status === 401
|
||||||
? "会话已失效,请重新登录。"
|
? "会话已失效,请重新登录。"
|
||||||
: response.status === 403
|
: apiError?.code === "validation_failed"
|
||||||
|
? safeValidationMessage(apiError)
|
||||||
|
: response.status === 403
|
||||||
? safeForbiddenMessage(apiError?.message)
|
? safeForbiddenMessage(apiError?.message)
|
||||||
: apiError?.message ?? `request failed: ${response.status}`;
|
: apiError?.message ?? `request failed: ${response.status}`;
|
||||||
const error = new PlatformApiError(response.status, apiError?.code ?? "request_failed", message);
|
const error = new PlatformApiError(response.status, apiError?.code ?? "request_failed", message);
|
||||||
@@ -763,11 +765,37 @@ async function responseError(response: Response): Promise<PlatformApiError> {
|
|||||||
return error;
|
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 {
|
function safeForbiddenMessage(apiMessage?: string): string {
|
||||||
const sanitized = safeDiagnosticText(apiMessage, "")?.trim();
|
const sanitized = safeDiagnosticText(apiMessage, "")?.trim();
|
||||||
if (!sanitized || sanitized === "account is not allowed to access this resource") {
|
if (!sanitized || sanitized === "account is not allowed to access this resource") {
|
||||||
return "没有权限访问该资源。";
|
return "没有权限访问该资源。";
|
||||||
}
|
}
|
||||||
|
if (sanitized === "password confirmation failed") {
|
||||||
|
return "当前登录密码不正确,请重新输入。";
|
||||||
|
}
|
||||||
const missingPermission = sanitized.match(/^plugin does not declare required permission:\s*([a-z0-9._-]+)$/i);
|
const missingPermission = sanitized.match(/^plugin does not declare required permission:\s*([a-z0-9._-]+)$/i);
|
||||||
if (missingPermission) {
|
if (missingPermission) {
|
||||||
return `插件未声明所需权限:${missingPermission[1]}`;
|
return `插件未声明所需权限:${missingPermission[1]}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user