Files
netbird/client/ui/frontend/src/lib/errors.ts
Zoltan Papp d3b660afba classify daemon login errors and surface localised dialogs
The daemon returns gRPC errors whose message is a wrapped mgm + JWT
stack (e.g. "invalid jwt token, err: token could not be parsed: ...").
Showing that in a native dialog is unreadable. Connection now maps the
substrings it recognises to a ClientError{code, short, long} so the UI
can render a localised summary plus a Details: block carrying the raw
daemon text. formatErrorMessage on the TS side reads the structured
payload from Wails' Error.cause (or the JSON-stringified Error.message)
and falls back to plain Error.message for callers not yet migrated.

Also bumps Wails to v3.0.0-alpha.95.
2026-05-20 19:13:13 +02:00

53 lines
2.3 KiB
TypeScript

// Shared error formatter for native dialog bodies.
//
// The Go service layer (client/ui/services/connection.go classifyDaemonError)
// wraps daemon errors in a ClientError struct exposed to the TS side as
// {code, short, long}. Short is already localised (Go reads the current
// preferences.Store language and resolves "error.<code>" via i18n.Bundle).
// Long always carries the unwrapped raw daemon message so the operator can
// see the JWT / mgm stack when the short text is too generic.
//
// Wails wraps Go-returned errors as Error({message, cause, kind}) where
// .message holds the JSON-stringified payload and the structured object
// lives on .cause — Object.keys(err) is empty in that case. We therefore
// probe .cause first, then fall back to parsing .message as JSON, then
// to plain .message text for callers that still hand us a raw Error.
const extractClientError = (e: unknown): { short?: string; long?: string } | null => {
if (!e || typeof e !== "object") return null;
const withCause = e as { cause?: unknown; message?: unknown };
if (withCause.cause && typeof withCause.cause === "object") {
return withCause.cause as { short?: string; long?: string };
}
if (typeof withCause.message === "string") {
const m = withCause.message.trim();
if (m.startsWith("{") && m.endsWith("}")) {
try {
const parsed = JSON.parse(m);
if (parsed && typeof parsed === "object") {
if ("cause" in parsed && parsed.cause && typeof parsed.cause === "object") {
return parsed.cause as { short?: string; long?: string };
}
return parsed as { short?: string; long?: string };
}
} catch {
// not JSON — fall through to plain-message handling
}
}
}
return null;
};
export const formatErrorMessage = (e: unknown): string => {
const ce = extractClientError(e);
if (ce) {
const short = typeof ce.short === "string" ? ce.short : "";
const long = typeof ce.long === "string" ? ce.long : "";
if (short && long && long !== short) {
return `${short}\n\nDetails: ${long}`;
}
if (short) return short;
}
if (e instanceof Error) return e.message;
return String(e);
};