fix(frontend): surface backend detail when agent name check fails (#3048)

* fix(frontend): surface backend detail when agent name check fails

The new-agent page caught AgentNameCheckError but only branched on
reason === "backend_unreachable". Everything else (notably the 422
"Invalid agent name '...'. Must match ^[A-Za-z0-9-]+$" response from
GET /api/agents/check when the user submits a name with disallowed
characters — trailing space, dot, Chinese, invisible whitespace from
copy-paste) fell through to the generic fallback "Could not verify
name availability — please try again", swallowing the detail that
already told the user exactly what to fix.

Add a request_failed branch that surfaces err.message (which
checkAgentName already populates from the backend's detail at
core/agents/api.ts). The disabled / backend_unreachable / unknown-
error paths are unchanged.

Pin the contract with unit tests covering: 200 success, fetch
rejection, 502/503/504 network errors, agents_api disabled detail,
422 validation detail carried verbatim, statusText fallback when
detail is absent, and a regression guard against misclassifying a
422 as agents_api disabled.

Closes #3041

* fix(frontend): localise the error prefix when surfacing backend detail

The previous commit surfaced the backend's raw `err.message` on the
new-agent page when the name check failed. The detail itself is
English (backend's `_validate_agent_name` text, any 5xx business
message, etc.) and dropping it bare into a zh-CN page produced a
jarring English-among-Chinese line that didn't match neighbouring
strings like "已存在同名智能体" / "无法验证名称可用性".

Add `nameStepCheckErrorWithDetail` as a templated string ("Name
check failed: {detail}" / "名称校验失败:{detail}"), mirroring the
existing `nameStepBootstrapMessage` `{name}` template pattern. The
page wraps `err.message` in it when present and falls back to the
plain `nameStepCheckError` when the detail is empty.

Rendered output (verified locally with a Console fetch mock that
returns 500 + detail):

  zh-CN: 名称校验失败:Database connection lost: SQLAlchemy connection
         pool exhausted (max 5 connections, all in use)
  en-US: Name check failed: Database connection lost: SQLAlchemy
         connection pool exhausted (max 5 connections, all in use)

The localised prefix tells the user *what operation* failed; the
raw detail tells them *why*. Translating the detail itself would
be lossy (any unbounded backend string would need a translation
table) and would break the debuggability the previous commit
delivered.

Refs #3041

* fix(frontend): distinguish backend detail from generated fallback in AgentNameCheckError

Addresses Copilot's review on #3048: the previous commits keyed off
`err.message`, but `checkAgentName` substitutes a generated fallback
string ("Failed to check agent name: ${statusText}") when the backend
sent no detail. That guaranteed `err.message` was always truthy, made
the `nameStepCheckError` fallback branch unreachable in practice, and
could surface awkward strings like "名称校验失败:Failed to check
agent name: Bad Gateway" in the UI.

Add an explicit `detail: string | null` field to AgentNameCheckError.
`checkAgentName` populates it only when the backend response actually
carried a string `detail` (defensive guard against the dict-shaped
detail that other deer-flow endpoints use for typed error codes).
The new-agent page now selects on `err.detail` instead of `err.message`
so the localised fallback wins when no real detail exists.

Also fix the prettier formatting that broke lint-frontend CI on the
previous push.

Test changes:
- The 422 carry-through test now asserts both `detail` and `message`
  hold the backend string verbatim.
- A new "falls back to statusText in message but leaves detail null"
  test pins the contract that no real detail ⇒ no UI surface leak.
- A new "treats non-string detail as null" test guards against future
  backend schema drift toward dict-shaped detail.

Refs #3041 #3048
This commit is contained in:
Xinmin Zeng
2026-05-28 18:38:45 +08:00
committed by GitHub
parent 8330b244a9
commit 2ace78d1e5
6 changed files with 186 additions and 1 deletions
@@ -146,6 +146,27 @@ export default function NewAgentPage() {
err.reason === "backend_unreachable"
) {
setNameError(t.agents.nameStepNetworkError);
} else if (
err instanceof AgentNameCheckError &&
err.reason === "request_failed"
) {
// Surface the backend-provided detail (e.g. validation error) when
// one is present, wrapped in a localised prefix so zh-CN users
// don't see a bare English string next to the surrounding Chinese
// UI. Falls back to the generic localised fallback when the backend
// sent no detail — `err.message` is unreliable for this branch
// because `checkAgentName` substitutes a generated fallback string
// ("Failed to check agent name: ${statusText}") when `detail` is
// missing, so testing `err.message` would always be truthy and the
// generated fallback would leak through.
setNameError(
err.detail
? t.agents.nameStepCheckErrorWithDetail.replace(
"{detail}",
err.detail,
)
: t.agents.nameStepCheckError,
);
} else {
setNameError(t.agents.nameStepCheckError);
}
@@ -172,6 +193,7 @@ export default function NewAgentPage() {
t.agents.nameStepNetworkError,
t.agents.nameStepBootstrapMessage,
t.agents.nameStepCheckError,
t.agents.nameStepCheckErrorWithDetail,
t.agents.nameStepInvalidError,
threadId,
]);
+12 -1
View File
@@ -9,6 +9,15 @@ export class AgentNameCheckError extends Error {
constructor(
message: string,
public readonly reason: "backend_unreachable" | "request_failed",
/**
* Raw backend `detail` string when the failure came from a backend
* response carrying one. `null` when no detail was provided (e.g.
* network-layer failure, empty response body, unparseable body) — in
* which case `message` is a generated fallback like "Failed to check
* agent name: Bad Gateway" and the UI should prefer its own localized
* fallback instead of surfacing the generated string.
*/
public readonly detail: string | null = null,
) {
super(message);
this.name = "AgentNameCheckError";
@@ -104,9 +113,11 @@ export async function checkAgentName(
"backend_unreachable",
);
}
const backendDetail = typeof err.detail === "string" ? err.detail : null;
throw new AgentNameCheckError(
err.detail ?? `Failed to check agent name: ${res.statusText}`,
backendDetail ?? `Failed to check agent name: ${res.statusText}`,
"request_failed",
backendDetail,
);
}
return res.json() as Promise<{ available: boolean; name: string }>;
+1
View File
@@ -204,6 +204,7 @@ export const enUS: Translations = {
nameStepNetworkError:
"Network request failed — check your network or backend connection",
nameStepCheckError: "Could not verify name availability — please try again",
nameStepCheckErrorWithDetail: "Name check failed: {detail}",
nameStepApiDisabledError:
"Custom agent management is not enabled on this server. Please contact your administrator.",
nameStepBootstrapMessage:
+1
View File
@@ -141,6 +141,7 @@ export interface Translations {
nameStepAlreadyExistsError: string;
nameStepNetworkError: string;
nameStepCheckError: string;
nameStepCheckErrorWithDetail: string;
nameStepApiDisabledError: string;
nameStepBootstrapMessage: string;
save: string;
+1
View File
@@ -192,6 +192,7 @@ export const zhCN: Translations = {
nameStepAlreadyExistsError: "已存在同名智能体",
nameStepNetworkError: "网络请求失败,请检查网络或后端连接",
nameStepCheckError: "无法验证名称可用性,请稍后重试",
nameStepCheckErrorWithDetail: "名称校验失败:{detail}",
nameStepApiDisabledError:
"服务器未开启自定义智能体管理功能,请联系管理员。",
nameStepBootstrapMessage: