diff --git a/tools/ui/src/lib/constants/error.ts b/tools/ui/src/lib/constants/error.ts new file mode 100644 index 0000000000..4339bd25d5 --- /dev/null +++ b/tools/ui/src/lib/constants/error.ts @@ -0,0 +1,23 @@ +export const ERROR_MESSAGES = { + NETWORK: { + GENERIC: 'Failed to connect to server', + NXDOMAIN: 'Server not found - check server address', + REFUSED: 'Connection refused - server may be offline', + TIMEOUT: 'Request timed out', + UNREACHABLE: 'Server is not running or unreachable' + }, + HTTP: { + GENERIC: 'Request failed', + ACCESS_DENIED: 'Access denied', + INTERNAL_ERROR: 'Server error - check server logs', + NOT_FOUND: 'Not found', + TEMPORARILY_UNAVAILABLE: 'Server temporarily unavailable' + } +}; + +export const HTTP_CODE_TO_STRING: Record = { + 401: ERROR_MESSAGES.HTTP.ACCESS_DENIED, + 403: ERROR_MESSAGES.HTTP.ACCESS_DENIED, + 500: ERROR_MESSAGES.HTTP.INTERNAL_ERROR, + 503: ERROR_MESSAGES.HTTP.TEMPORARILY_UNAVAILABLE +}; diff --git a/tools/ui/src/lib/stores/server.svelte.ts b/tools/ui/src/lib/stores/server.svelte.ts index dfcb9b2bb4..d9a9f855a9 100644 --- a/tools/ui/src/lib/stores/server.svelte.ts +++ b/tools/ui/src/lib/stores/server.svelte.ts @@ -82,8 +82,8 @@ class ServerStore { this.props = props; this.error = null; this.detectRole(props); - } catch (error) { - this.error = this.getErrorMessage(error); + } catch (error: unknown) { + this.error = error instanceof Error ? error.message : String(error); console.error('Error fetching server properties:', error); } finally { this.loading = false; @@ -95,32 +95,6 @@ class ServerStore { await fetchPromise; } - private getErrorMessage(error: unknown): string { - if (error instanceof Error) { - const message = error.message || ''; - - if (error.name === 'TypeError' && message.includes('fetch')) { - return 'Server is not running or unreachable'; - } else if (message.includes('ECONNREFUSED')) { - return 'Connection refused - server may be offline'; - } else if (message.includes('ENOTFOUND')) { - return 'Server not found - check server address'; - } else if (message.includes('ETIMEDOUT')) { - return 'Request timed out'; - } else if (message.includes('503')) { - return 'Server temporarily unavailable'; - } else if (message.includes('500')) { - return 'Server error - check server logs'; - } else if (message.includes('404')) { - return 'Server endpoint not found'; - } else if (message.includes('403') || message.includes('401')) { - return 'Access denied'; - } - } - - return 'Failed to connect to server'; - } - clear(): void { this.props = null; this.error = null; diff --git a/tools/ui/src/lib/utils/api-fetch.ts b/tools/ui/src/lib/utils/api-fetch.ts index 80781b98e9..82a9383ddf 100644 --- a/tools/ui/src/lib/utils/api-fetch.ts +++ b/tools/ui/src/lib/utils/api-fetch.ts @@ -1,6 +1,7 @@ import { base } from '$app/paths'; import { getJsonHeaders, getAuthHeaders } from './api-headers'; import { UrlProtocol } from '$lib/enums'; +import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants/error'; /** * API Fetch Utilities @@ -54,10 +55,15 @@ export async function apiFetch(path: string, options: ApiFetchOptions = {}): ? path : `${base}${path}`; - const response = await fetch(url, { - ...fetchOptions, - headers - }); + let response; + try { + response = await fetch(url, { + ...fetchOptions, + headers + }); + } catch (e) { + throw new Error(beautifyNetworkError(e)); + } if (!response.ok) { const errorMessage = await parseErrorMessage(response); @@ -101,10 +107,15 @@ export async function apiFetchWithParams( const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders(); const headers = { ...baseHeaders, ...customHeaders }; - const response = await fetch(url.toString(), { - ...fetchOptions, - headers - }); + let response; + try { + response = await fetch(url.toString(), { + ...fetchOptions, + headers + }); + } catch (e) { + throw new Error(beautifyNetworkError(e)); + } if (!response.ok) { const errorMessage = await parseErrorMessage(response); @@ -154,5 +165,37 @@ async function parseErrorMessage(response: Response): Promise { // JSON parsing failed, use status text } - return `Request failed: ${response.status} ${response.statusText}`; + const httpErrorStr = HTTP_CODE_TO_STRING[response.status]; + if (httpErrorStr) { + return httpErrorStr; + } + + return `${ERROR_MESSAGES.HTTP.GENERIC}: ${response.status} ${response.statusText}`; +} + +/** + * Converts a network issue into a human-readable message. + * @param throwable - The throwable raised during fetch operation + * @returns Error in an human-readable format + */ +function beautifyNetworkError(throwable: unknown): string { + let message; + if (throwable instanceof Error) { + message = throwable.message; + if (throwable.name === 'TypeError' && message.includes('fetch')) { + return ERROR_MESSAGES.NETWORK.UNREACHABLE; + } + } else { + message = String(throwable); + } + + if (message.includes('ECONNREFUSED')) { + return ERROR_MESSAGES.NETWORK.REFUSED; + } else if (message.includes('ENOTFOUND')) { + return ERROR_MESSAGES.NETWORK.NXDOMAIN; + } else if (message.includes('ETIMEDOUT')) { + return ERROR_MESSAGES.NETWORK.TIMEOUT; + } + + return `${ERROR_MESSAGES.NETWORK.GENERIC} (${message})`; }