diff --git a/src/server/static-react/src/api/core/errors.ts b/src/server/static-react/src/api/core/errors.ts index 7df0a01d..ea2605a4 100644 --- a/src/server/static-react/src/api/core/errors.ts +++ b/src/server/static-react/src/api/core/errors.ts @@ -68,11 +68,21 @@ export class ApiError extends Error { if (this.isNetworkError) { return ERROR_MESSAGES.NETWORK_ERROR; } - + if (this.isTimeoutError) { return ERROR_MESSAGES.TIMEOUT_ERROR; } + // Prefer the server-supplied descriptive message over the generic constant + // for HTTP errors. ErrorFactory.fromResponse fills `this.message` with + // `errorData.message` / `errorData.error` (e.g. structured 400 payloads + // like "Field 'title' not on schema 'X'", or a 404 body's "schema not + // found") — that text is what the user can actually act on. The synthetic + // `HTTP ` fallback is filtered out so we don't surface "HTTP 404". + if (this.message && this.status > 0 && !/^HTTP \d+$/.test(this.message)) { + return this.message; + } + switch (this.status) { case HTTP_STATUS_CODES.UNAUTHORIZED: return ERROR_MESSAGES.AUTHENTICATION_ERROR; diff --git a/src/server/static-react/src/components/StatusSection.tsx b/src/server/static-react/src/components/StatusSection.tsx index 59110e7c..86dea759 100644 --- a/src/server/static-react/src/components/StatusSection.tsx +++ b/src/server/static-react/src/components/StatusSection.tsx @@ -4,6 +4,8 @@ import { systemClient } from '../api/clients/systemClient' import { ingestionClient } from '../api/clients' import type { IngestionProgress } from '../api/clients/ingestionClient' import { usePolling } from '../hooks/usePolling' +import { isNetworkError } from '../api/core/errors' +import { toErrorMessage } from '../utils/schemaUtils' type ProgressJob = IngestionProgress & { job_type?: string } @@ -76,8 +78,11 @@ function StatusSection() { setIsResetting(false) } } catch (error) { - const msg = error instanceof Error ? error.message : String(error) - setResetResult({ type: 'error', message: `Network error: ${msg}` }) + const msg = toErrorMessage(error) + setResetResult({ + type: 'error', + message: isNetworkError(error) ? `Network error: ${msg}` : msg, + }) setShowConfirmDialog(false) setIsResetting(false) } diff --git a/src/server/static-react/src/components/tabs/DataBrowserTab.tsx b/src/server/static-react/src/components/tabs/DataBrowserTab.tsx index aa13bbab..bc5c7df2 100644 --- a/src/server/static-react/src/components/tabs/DataBrowserTab.tsx +++ b/src/server/static-react/src/components/tabs/DataBrowserTab.tsx @@ -15,7 +15,7 @@ import { FieldsTable } from '../StructuredResults' import SchemaName from '../shared/SchemaName' import ShareRecordModal from '../ShareRecordModal' import { ArrowUpTrayIcon } from '@heroicons/react/24/outline' -import { getFieldNames, toggleSetItem } from '../../utils/schemaUtils' +import { getFieldNames, toErrorMessage, toggleSetItem } from '../../utils/schemaUtils' import { keyId, keyLabel, @@ -186,7 +186,7 @@ export default function DataBrowserTab() { setSchemaErrors((p) => ({ ...p, [name]: res.error || 'Failed to fetch keys' })) } } catch (e) { - setSchemaErrors((p) => ({ ...p, [name]: (e instanceof Error ? e.message : String(e)) || 'Network error' })) + setSchemaErrors((p) => ({ ...p, [name]: toErrorMessage(e) || 'Failed to fetch keys' })) } finally { setSchemaLoading((p) => ({ ...p, [name]: false })) } diff --git a/src/server/static-react/src/components/tabs/DiscoveryBrowseTab.tsx b/src/server/static-react/src/components/tabs/DiscoveryBrowseTab.tsx index c498cc33..36a55efd 100644 --- a/src/server/static-react/src/components/tabs/DiscoveryBrowseTab.tsx +++ b/src/server/static-react/src/components/tabs/DiscoveryBrowseTab.tsx @@ -47,7 +47,7 @@ function ProfileCard({ result, onConnect }: ProfileCardProps) { onConnect({ error: res.error || 'Connect failed' }) } } catch (e) { - onConnect({ error: toErrorMessage(e) || 'Network error' }) + onConnect({ error: toErrorMessage(e) || 'Connect failed' }) } finally { setSending(false) } @@ -235,7 +235,7 @@ export default function DiscoveryBrowseTab({ onResult }: DiscoveryBrowseTabProps setSearchError(res.error || 'Search failed') } } catch (e) { - setSearchError(toErrorMessage(e) || 'Network error') + setSearchError(toErrorMessage(e) || 'Search failed') } finally { setSearching(false) } diff --git a/src/server/static-react/src/components/tabs/FeedTab.tsx b/src/server/static-react/src/components/tabs/FeedTab.tsx index 08a07c72..a124a2c2 100644 --- a/src/server/static-react/src/components/tabs/FeedTab.tsx +++ b/src/server/static-react/src/components/tabs/FeedTab.tsx @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react' import { useAppSelector } from '../../store/hooks' import { createApiClient } from '../../api/core/client' import { API_ENDPOINTS } from '../../api/endpoints' +import { toErrorMessage } from '../../utils/schemaUtils' const apiClient = createApiClient() @@ -78,7 +79,7 @@ export default function FeedTab() { setTotal(data?.total || 0) setFetched(true) } catch (err) { - setError((err instanceof Error ? err.message : null) || 'Network error') + setError(toErrorMessage(err) || 'Failed to fetch feed') } finally { setLoading(false) } diff --git a/src/server/static-react/src/components/tabs/MyProfileTab.tsx b/src/server/static-react/src/components/tabs/MyProfileTab.tsx index 248aa4ba..1538cf17 100644 --- a/src/server/static-react/src/components/tabs/MyProfileTab.tsx +++ b/src/server/static-react/src/components/tabs/MyProfileTab.tsx @@ -342,7 +342,7 @@ export default function MyProfileTab({ onResult }: MyProfileTabProps) { onResult?.({ error: res.error || 'Detection failed' }) } } catch (e) { - onResult?.({ error: toErrorMessage(e) || 'Network error' }) + onResult?.({ error: toErrorMessage(e) || 'Detection failed' }) } finally { setDetecting(false) } @@ -358,7 +358,7 @@ export default function MyProfileTab({ onResult }: MyProfileTabProps) { onResult?.({ error: res.error || 'Toggle failed' }) } } catch (e) { - onResult?.({ error: toErrorMessage(e) || 'Network error' }) + onResult?.({ error: toErrorMessage(e) || 'Toggle failed' }) } finally { setToggling(false) } diff --git a/src/server/static-react/src/components/tabs/NativeIndexTab.tsx b/src/server/static-react/src/components/tabs/NativeIndexTab.tsx index 7275de73..7ec45cb1 100644 --- a/src/server/static-react/src/components/tabs/NativeIndexTab.tsx +++ b/src/server/static-react/src/components/tabs/NativeIndexTab.tsx @@ -154,9 +154,9 @@ export default function NativeIndexTab({ onResult }: NativeIndexTabProps) { onResult({ error: res.error || 'Search failed', status: res.status }) } } catch (e) { - const msg = toErrorMessage(e) - setError(msg || 'Network error') - onResult({ error: msg || 'Network error' }) + const msg = toErrorMessage(e) || 'Search failed' + setError(msg) + onResult({ error: msg }) } finally { setIsSearching(false) } diff --git a/src/server/static-react/src/components/tabs/WordGraphTab.tsx b/src/server/static-react/src/components/tabs/WordGraphTab.tsx index d6a78111..2fb5da12 100644 --- a/src/server/static-react/src/components/tabs/WordGraphTab.tsx +++ b/src/server/static-react/src/components/tabs/WordGraphTab.tsx @@ -154,7 +154,7 @@ export default function WordGraphTab() { setError(res.error || 'Search failed') } } catch (e) { - setError(toErrorMessage(e) || 'Network error') + setError(toErrorMessage(e) || 'Search failed') } finally { setIsSearching(false) } diff --git a/src/server/static-react/src/components/tabs/discovery/ConnectionRequestsPanel.tsx b/src/server/static-react/src/components/tabs/discovery/ConnectionRequestsPanel.tsx index f088247b..48147776 100644 --- a/src/server/static-react/src/components/tabs/discovery/ConnectionRequestsPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/ConnectionRequestsPanel.tsx @@ -67,7 +67,7 @@ export default function ConnectionRequestsPanel({ onResult }: ConnectionRequests onResult({ error: res.error || `Failed to ${action}` }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Failed to respond' }) } finally { setResponding(null) } diff --git a/src/server/static-react/src/components/tabs/discovery/FaceSearchPanel.tsx b/src/server/static-react/src/components/tabs/discovery/FaceSearchPanel.tsx index 21f3b627..ae90aa97 100644 --- a/src/server/static-react/src/components/tabs/discovery/FaceSearchPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/FaceSearchPanel.tsx @@ -117,9 +117,9 @@ export default function FaceSearchPanel({ onResult }: FaceSearchPanelProps) { onResult({ error: res.error || 'Face search failed' }) } } catch (e) { - const msg = toErrorMessage(e) - setError(msg || 'Network error') - onResult({ error: msg || 'Network error' }) + const msg = toErrorMessage(e) || 'Face search failed' + setError(msg) + onResult({ error: msg }) } finally { setSearching(false) } @@ -138,7 +138,7 @@ export default function FaceSearchPanel({ onResult }: FaceSearchPanelProps) { onResult({ error: res.error || 'Connect failed' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Connect failed' }) } } diff --git a/src/server/static-react/src/components/tabs/discovery/InterestsPanel.tsx b/src/server/static-react/src/components/tabs/discovery/InterestsPanel.tsx index 6cead256..40e85d69 100644 --- a/src/server/static-react/src/components/tabs/discovery/InterestsPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/InterestsPanel.tsx @@ -25,7 +25,7 @@ export default function InterestsPanel({ onResult }: InterestsPanelProps) { setLoadError(res.error || 'Failed to load interests') } } catch (e) { - setLoadError(toErrorMessage(e) || 'Network error') + setLoadError(toErrorMessage(e) || 'Failed to load interests') } finally { setLoading(false) } @@ -42,7 +42,7 @@ export default function InterestsPanel({ onResult }: InterestsPanelProps) { onResult({ error: res.error || 'Toggle failed' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Toggle failed' }) } } @@ -57,7 +57,7 @@ export default function InterestsPanel({ onResult }: InterestsPanelProps) { onResult({ error: res.error || 'Detection failed' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Detection failed' }) } finally { setDetecting(false) } diff --git a/src/server/static-react/src/components/tabs/discovery/PeopleLikeYouPanel.tsx b/src/server/static-react/src/components/tabs/discovery/PeopleLikeYouPanel.tsx index a71d7b2c..b765e874 100644 --- a/src/server/static-react/src/components/tabs/discovery/PeopleLikeYouPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/PeopleLikeYouPanel.tsx @@ -40,7 +40,7 @@ export default function PeopleLikeYouPanel({ onResult }: PeopleLikeYouPanelProps } } } catch (e) { - const msg = toErrorMessage(e) || 'Network error' + const msg = toErrorMessage(e) || 'Failed to load similar profiles' setError(msg) if (isLocalModeError(msg)) { localModeRef.current = true @@ -74,7 +74,7 @@ export default function PeopleLikeYouPanel({ onResult }: PeopleLikeYouPanelProps onResult({ error: res.error || 'Connect failed' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Connect failed' }) } } diff --git a/src/server/static-react/src/components/tabs/discovery/SearchPanel.tsx b/src/server/static-react/src/components/tabs/discovery/SearchPanel.tsx index 4ed767c7..d4954896 100644 --- a/src/server/static-react/src/components/tabs/discovery/SearchPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/SearchPanel.tsx @@ -55,9 +55,9 @@ export default function SearchPanel({ onResult }: SearchPanelProps) { onResult({ error: res.error || 'Search failed' }) } } catch (e) { - const msg = toErrorMessage(e) - setError(msg || 'Network error') - onResult({ error: msg || 'Network error' }) + const msg = toErrorMessage(e) || 'Search failed' + setError(msg) + onResult({ error: msg }) } finally { setSearching(false) } @@ -76,7 +76,7 @@ export default function SearchPanel({ onResult }: SearchPanelProps) { onResult({ error: res.error || 'Connect failed' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Connect failed' }) } } diff --git a/src/server/static-react/src/components/tabs/discovery/SharedEventsPanel.tsx b/src/server/static-react/src/components/tabs/discovery/SharedEventsPanel.tsx index f1902839..89fe3927 100644 --- a/src/server/static-react/src/components/tabs/discovery/SharedEventsPanel.tsx +++ b/src/server/static-react/src/components/tabs/discovery/SharedEventsPanel.tsx @@ -36,7 +36,7 @@ export default function SharedEventsPanel({ onResult }: SharedEventsPanelProps) setError(statusRes.error || 'Failed to load calendar sharing status') } } catch (e) { - setError(toErrorMessage(e) || 'Network error') + setError(toErrorMessage(e) || 'Failed to load calendar sharing status') } finally { setLoading(false) } @@ -65,7 +65,7 @@ export default function SharedEventsPanel({ onResult }: SharedEventsPanelProps) onResult({ error: res.error || 'Failed to toggle calendar sharing' }) } } catch (e) { - onResult({ error: toErrorMessage(e) || 'Network error' }) + onResult({ error: toErrorMessage(e) || 'Failed to toggle calendar sharing' }) } finally { setToggling(false) } diff --git a/src/server/static-react/src/components/tabs/personas/ImportedIdentitiesPanel.tsx b/src/server/static-react/src/components/tabs/personas/ImportedIdentitiesPanel.tsx index 11e95019..cee9e1da 100644 --- a/src/server/static-react/src/components/tabs/personas/ImportedIdentitiesPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/ImportedIdentitiesPanel.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import { listIdentities, type IdentityAuditRow } from '../../../api/clients/fingerprintsClient' +import { toErrorMessage } from '../../../utils/schemaUtils' /** * "Identities" audit panel — surfaces every Identity record on this @@ -29,7 +30,7 @@ export default function ImportedIdentitiesPanel() { setError(res.error ?? 'Failed to load identities') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load identities') } finally { setLoading(false) } diff --git a/src/server/static-react/src/components/tabs/personas/IngestionErrorsPanel.tsx b/src/server/static-react/src/components/tabs/personas/IngestionErrorsPanel.tsx index 4ae4803f..38c58567 100644 --- a/src/server/static-react/src/components/tabs/personas/IngestionErrorsPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/IngestionErrorsPanel.tsx @@ -4,6 +4,7 @@ import { resolveIngestionError, type IngestionErrorView, } from '../../../api/clients/fingerprintsClient' +import { toErrorMessage } from '../../../utils/schemaUtils' /** * Failed records panel. Renders unresolved IngestionError rows so the @@ -33,7 +34,7 @@ export default function IngestionErrorsPanel() { setError(res.error ?? 'Failed to load ingestion errors') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load ingestion errors') } finally { setLoading(false) } @@ -63,7 +64,7 @@ export default function IngestionErrorsPanel() { setError(res.error ?? `Failed to ${resolved ? 'dismiss' : 'restore'}`) } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to update ingestion error') } finally { setBusyId(null) } diff --git a/src/server/static-react/src/components/tabs/personas/MyIdentityCardPanel.tsx b/src/server/static-react/src/components/tabs/personas/MyIdentityCardPanel.tsx index 8bab91b7..8aef0b47 100644 --- a/src/server/static-react/src/components/tabs/personas/MyIdentityCardPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/MyIdentityCardPanel.tsx @@ -8,6 +8,7 @@ import { type MyIdentityCardResponse, } from '../../../api/clients/fingerprintsClient' import { listContacts, type Contact } from '../../../api/clients/trustClient' +import { toErrorMessage } from '../../../utils/schemaUtils' type AttachStage = 'idle' | 'camera' | 'detecting' | 'saving' @@ -93,7 +94,7 @@ export default function MyIdentityCardPanel() { setError(res.error ?? 'Failed to load identity card') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load identity card') } finally { setLoading(false) } @@ -134,7 +135,7 @@ export default function MyIdentityCardPanel() { setSaveError(res.error ?? 'Failed to reissue identity card') } } catch (e) { - setSaveError((e as Error)?.message ?? 'Network error') + setSaveError(toErrorMessage(e) || 'Failed to reissue identity card') } finally { setSaving(false) } diff --git a/src/server/static-react/src/components/tabs/personas/PersonasPanel.tsx b/src/server/static-react/src/components/tabs/personas/PersonasPanel.tsx index 71d80ff0..5c017623 100644 --- a/src/server/static-react/src/components/tabs/personas/PersonasPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/PersonasPanel.tsx @@ -16,6 +16,7 @@ import { type ResolveDiagnostics, } from '../../../api/clients/fingerprintsClient' import { ErrorMessage } from '../../shared/ErrorMessage' +import { toErrorMessage } from '../../../utils/schemaUtils' type SortMode = 'recent' | 'name_asc' | 'mentions_desc' | 'trust_tier_desc' @@ -160,7 +161,7 @@ export default function PersonasPanel() { setError(res.error ?? 'Failed to load personas') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load personas') } finally { setLoading(false) } @@ -190,7 +191,7 @@ export default function PersonasPanel() { }) .catch((e: unknown) => { if (!cancelled) { - setDetailError((e as Error)?.message ?? 'Network error') + setDetailError(toErrorMessage(e) || 'Failed to load persona') setDetail(null) } }) diff --git a/src/server/static-react/src/components/tabs/personas/ReceivedCardsPanel.tsx b/src/server/static-react/src/components/tabs/personas/ReceivedCardsPanel.tsx index baadeffa..b1e1db03 100644 --- a/src/server/static-react/src/components/tabs/personas/ReceivedCardsPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/ReceivedCardsPanel.tsx @@ -5,6 +5,7 @@ import { listReceivedCards, type ReceivedCardView, } from '../../../api/clients/fingerprintsClient' +import { toErrorMessage } from '../../../utils/schemaUtils' type ActionFlag = 'accept' | 'dismiss' | null @@ -52,7 +53,7 @@ export default function ReceivedCardsPanel() { setError(res.error ?? 'Failed to load received cards') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load received cards') } finally { setLoading(false) } diff --git a/src/server/static-react/src/components/tabs/personas/SuggestedPersonasPanel.tsx b/src/server/static-react/src/components/tabs/personas/SuggestedPersonasPanel.tsx index 5a7e7a0a..81ec188d 100644 --- a/src/server/static-react/src/components/tabs/personas/SuggestedPersonasPanel.tsx +++ b/src/server/static-react/src/components/tabs/personas/SuggestedPersonasPanel.tsx @@ -5,6 +5,7 @@ import { RELATIONSHIP_OPTIONS, type SuggestedPersonaView, } from '../../../api/clients/fingerprintsClient' +import { toErrorMessage } from '../../../utils/schemaUtils' /** * localStorage key used to persist the set of dismissed suggestion @@ -78,7 +79,7 @@ export default function SuggestedPersonasPanel() { setError(res.error ?? 'Failed to load suggestions') } } catch (e) { - setError((e as Error)?.message ?? 'Network error') + setError(toErrorMessage(e) || 'Failed to load suggestions') } finally { setLoading(false) } diff --git a/src/server/static-react/src/components/tabs/sharing/CrossUserSharingPanel.tsx b/src/server/static-react/src/components/tabs/sharing/CrossUserSharingPanel.tsx index a6352ecb..9e10c5db 100644 --- a/src/server/static-react/src/components/tabs/sharing/CrossUserSharingPanel.tsx +++ b/src/server/static-react/src/components/tabs/sharing/CrossUserSharingPanel.tsx @@ -13,6 +13,7 @@ import { } from '../../../api/clients/sharingClient' import { listContacts, type Contact } from '../../../api/clients/trustClient' import { getAllSchemasWithState } from '../../../api/clients/schemaClient' +import { toErrorMessage } from '../../../utils/schemaUtils' type ScopeKind = 'AllSchemas' | 'Schema' | 'SchemaField' @@ -81,7 +82,7 @@ export default function CrossUserSharingPanel() { setRulesError(res.error ?? 'Failed to load share rules') } } catch (e) { - setRulesError((e as Error)?.message ?? 'Network error') + setRulesError(toErrorMessage(e) || 'Failed to load share rules') } finally { setRulesLoading(false) } @@ -98,7 +99,7 @@ export default function CrossUserSharingPanel() { setPendingError(res.error ?? 'Failed to load pending invites') } } catch (e) { - setPendingError((e as Error)?.message ?? 'Network error') + setPendingError(toErrorMessage(e) || 'Failed to load pending invites') } finally { setPendingLoading(false) } @@ -181,7 +182,7 @@ export default function CrossUserSharingPanel() { setToast(res.error ?? 'Failed to generate invite') } } catch (e) { - setToast((e as Error)?.message ?? 'Network error') + setToast(toErrorMessage(e) || 'Failed to generate invite') } }} onDeactivate={async (rule: ShareRule) => { @@ -197,7 +198,7 @@ export default function CrossUserSharingPanel() { setToast(res.error ?? 'Failed to deactivate') } } catch (e) { - setToast((e as Error)?.message ?? 'Network error') + setToast(toErrorMessage(e) || 'Failed to deactivate') } }} /> @@ -219,7 +220,7 @@ export default function CrossUserSharingPanel() { setToast(res.error ?? 'Failed to accept invite') } } catch (e) { - setToast((e as Error)?.message ?? 'Network error') + setToast(toErrorMessage(e) || 'Failed to accept invite') } }} /> @@ -289,7 +290,7 @@ function CreateRuleForm({ contacts, schemas, onCreated }: CreateRuleFormProps) { setError(res.error ?? 'Failed to create share rule') } } catch (err) { - setError((err as Error)?.message ?? 'Network error') + setError(toErrorMessage(err) || 'Failed to create share rule') } finally { setSubmitting(false) } diff --git a/src/server/static-react/src/utils/schemaUtils.ts b/src/server/static-react/src/utils/schemaUtils.ts index bf7f2296..4f3725f1 100644 --- a/src/server/static-react/src/utils/schemaUtils.ts +++ b/src/server/static-react/src/utils/schemaUtils.ts @@ -9,6 +9,8 @@ * - Query-by-key record fetching */ +import { isApiError } from '../api/core/errors' + export interface SchemaLike { name?: string; descriptive_name?: string; @@ -81,8 +83,13 @@ export function toggleSetItem(set: Iterable, item: T): Set { return next } -/** Convert an unknown error value to a string message. */ +/** + * Convert an unknown error value to a user-friendly message. + * For ApiError, defers to toUserMessage() so HTTP 4xx/5xx responses surface + * the server-supplied message instead of a generic "Network error" fallback. + */ export function toErrorMessage(error: unknown): string { + if (isApiError(error)) return error.toUserMessage() if (error instanceof Error) return error.message return String(error) }