export interface ApiError { message: string code?: string statusCode?: number originalError?: Error } const GENERIC_ERROR_MESSAGES = new Set(["error", "unknown", "unknownerror"]) const normalizeErrorText = (value: unknown): string | null => { if (typeof value !== "string") { return null } const trimmed = value.trim() return trimmed ? trimmed : null } const isSpecificErrorText = (value: string | null): value is string => { return !!value && !GENERIC_ERROR_MESSAGES.has(value.toLowerCase()) } const getXmlTagText = (xml: string, tagName: string): string | null => { const match = xml.match(new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)`, "i")) return normalizeErrorText(match?.[1]) } export const getXmlErrorCode = (xml: string): string | null => { const trimmed = xml.trim() if (!trimmed.startsWith("<")) return null if (typeof DOMParser !== "undefined") { try { const doc = new DOMParser().parseFromString(trimmed, "text/xml") if (!doc.querySelector("parsererror")) { return normalizeErrorText(doc.querySelector("Code")?.textContent) } } catch { // Fall through to regex-based parsing. } } return getXmlTagText(trimmed, "Code") } export const getXmlErrorMessage = (xml: string): string | null => { const trimmed = xml.trim() if (!trimmed.startsWith("<")) { return null } // Use DOMParser for proper XML parsing with automatic entity decoding if (typeof DOMParser !== "undefined") { try { const parser = new DOMParser() const doc = parser.parseFromString(trimmed, "text/xml") // Check for parse errors const parseError = doc.querySelector("parsererror") if (parseError) { return null } // Try to extract Message first (most specific) const message = doc.querySelector("Message")?.textContent?.trim() if (message && !GENERIC_ERROR_MESSAGES.has(message.toLowerCase())) { return message } // Fall back to Code const code = doc.querySelector("Code")?.textContent?.trim() if (code && !GENERIC_ERROR_MESSAGES.has(code.toLowerCase())) { return code } // Fall back to Error element text const error = doc.querySelector("Error")?.textContent?.trim() if (error && !GENERIC_ERROR_MESSAGES.has(error.toLowerCase())) { return error } return message || code || error || null } catch { // Fall through to regex-based parsing } } // Fallback: regex-based parsing (for SSR or if DOMParser fails) const message = getXmlTagText(trimmed, "Message") if (isSpecificErrorText(message)) { return message } const code = getXmlTagText(trimmed, "Code") if (isSpecificErrorText(code)) { return code } const error = getXmlTagText(trimmed, "Error") if (isSpecificErrorText(error)) { return error } return message ?? code ?? error } export const getServiceErrorMessage = (error: unknown): string | null => { if (error instanceof Error) { const directMessage = normalizeErrorText(error.message) if (isSpecificErrorText(directMessage)) { return directMessage } } if (!error || typeof error !== "object") { return null } const err = error as { Code?: unknown Message?: unknown name?: unknown message?: unknown Error?: { Code?: unknown Message?: unknown message?: unknown } } const codeCandidates = [ normalizeErrorText(err.Code), normalizeErrorText(err.Error?.Code), normalizeErrorText(err.name), ] const messageCandidates = [ normalizeErrorText(err.Message), normalizeErrorText(err.Error?.Message), normalizeErrorText(err.Error?.message), normalizeErrorText(err.message), ] for (const candidate of codeCandidates) { if (isSpecificErrorText(candidate)) { return candidate } } for (const candidate of messageCandidates) { if (isSpecificErrorText(candidate)) { return candidate } } return codeCandidates.find(Boolean) ?? messageCandidates.find(Boolean) ?? null } export class ConfigLoadError extends Error { code: "INVALID_URL" | "STORAGE_ERROR" | "NETWORK_ERROR" | "UNKNOWN_ERROR" originalError?: Error constructor(message: string, code: ConfigLoadError["code"] = "UNKNOWN_ERROR", originalError?: Error) { super(message) this.name = "ConfigLoadError" this.code = code this.originalError = originalError } } export const parseApiError = async (response: Response): Promise => { try { const errorData = await response.clone().json() return (errorData as { message?: string }).message || JSON.stringify(errorData) || response.statusText } catch { try { const text = await response.clone().text() if (text) { if (text.trim().startsWith("<")) { return getXmlErrorMessage(text) ?? text } return text } } catch { // keep statusText } } return response.statusText } export const handleConfigError = (error: unknown, context: string): ConfigLoadError => { if (error instanceof ConfigLoadError) { return error } if (error instanceof Error) { if (error.message.includes("Invalid URL")) { return new ConfigLoadError(`Invalid URL in ${context}: ${error.message}`, "INVALID_URL", error) } if (error.message.includes("localStorage") || error.message.includes("storage")) { return new ConfigLoadError(`Storage error in ${context}: ${error.message}`, "STORAGE_ERROR", error) } if (error.message.includes("fetch") || error.message.includes("network")) { return new ConfigLoadError(`Network error in ${context}: ${error.message}`, "NETWORK_ERROR", error) } } return new ConfigLoadError( `Unknown error in ${context}: ${error}`, "UNKNOWN_ERROR", error instanceof Error ? error : new Error(String(error)), ) }