import { AxiosError } from 'axios'; import type { ApiError } from '@/types/api'; /** * Helper de gestion d'erreurs API * Transforme les erreurs Axios brutes en objets ApiError standardisés * selon le format défini dans FRONTEND_INTEGRATION.md */ /** * Parse une erreur Axios en ApiError standardisé * @param error - Erreur Axios ou Error * @returns ApiError formaté selon le contrat backend */ export function parseApiError(error: unknown): ApiError { // Si c'est déjà une ApiError, la retourner telle quelle if (isApiError(error)) { return error; } // Si c'est une erreur Axios if (isAxiosError(error)) { const axiosError = error as AxiosError; // Si le backend retourne le format standardisé { success: false, error: {...} } if ( axiosError.response?.data && typeof axiosError.response.data === 'object' && 'success' in axiosError.response.data && axiosError.response.data.success === false && 'error' in axiosError.response.data ) { const backendError = (axiosError.response.data as { error: any }).error; return normalizeApiError(backendError); } // Si le backend retourne le format { error: {...} } sans success property (Middleware Gin actuel) if ( axiosError.response?.data && typeof axiosError.response.data === 'object' && 'error' in axiosError.response.data && typeof (axiosError.response.data as any).error === 'object' ) { const backendError = (axiosError.response.data as { error: any }).error; // Vérifier si l'objet error contient au moins code ou message pour éviter les faux positifs if (backendError && ('code' in backendError || 'message' in backendError)) { return normalizeApiError(backendError); } } // Si le backend retourne directement un objet error (Legacy ou autre middleware) if ( axiosError.response?.data && typeof axiosError.response.data === 'object' && 'code' in axiosError.response.data && 'message' in axiosError.response.data ) { return normalizeApiError(axiosError.response.data); } // Erreur réseau (pas de réponse) if (axiosError.request && !axiosError.response) { return { code: 0, message: 'Network error: Unable to connect to server', timestamp: new Date().toISOString(), }; } // Gestion spécifique des codes HTTP d'erreur const status = axiosError.response?.status; if (status === 429) { // Too Many Requests - Rate limiting const retryAfter = axiosError.response?.headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(String(retryAfter), 10) : undefined; return { code: 429, message: (axiosError.response?.data as any)?.message || 'Trop de requêtes. Veuillez patienter avant de réessayer.', timestamp: new Date().toISOString(), details: retryAfterSeconds ? [{ field: 'retry_after', message: `Réessayez dans ${retryAfterSeconds} secondes` }] : undefined, retry_after: retryAfterSeconds, }; } if (status === 503) { // Service Unavailable - ClamAV ou autre service externe return { code: 503, message: (axiosError.response?.data as any)?.message || 'Service temporairement indisponible. Veuillez réessayer dans quelques instants.', timestamp: new Date().toISOString(), details: (axiosError.response?.data as any)?.details, }; } if (status === 502) { // Bad Gateway - Problème de communication avec un service externe return { code: 502, message: (axiosError.response?.data as any)?.message || 'Erreur de communication avec le serveur. Veuillez réessayer plus tard.', timestamp: new Date().toISOString(), details: (axiosError.response?.data as any)?.details, }; } // Erreur HTTP sans format standardisé return { code: status || 0, message: (axiosError.response?.data as any)?.message || axiosError.message || 'An unexpected error occurred', timestamp: new Date().toISOString(), }; } // Erreur JavaScript standard if (error instanceof Error) { return { code: 0, message: error.message || 'An unexpected error occurred', timestamp: new Date().toISOString(), }; } // Erreur inconnue return { code: 0, message: 'An unexpected error occurred', timestamp: new Date().toISOString(), }; } /** * Normalise un objet d'erreur backend en ApiError standardisé */ function normalizeApiError(error: any): ApiError { return { code: typeof error.code === 'number' ? error.code : parseInt(String(error.code || 0), 10), message: error.message || 'An error occurred', details: error.details || (Array.isArray(error.details) ? error.details : undefined), request_id: error.request_id, timestamp: error.timestamp || new Date().toISOString(), context: error.context, }; } /** * Formate un message d'erreur pour l'affichage dans l'UI * @param error - ApiError * @param includeRequestId - Si true, inclut le request_id dans le message (pour debugging) * @returns Message formaté pour l'utilisateur */ export function formatErrorMessage( error: ApiError, includeRequestId: boolean = false, ): string { let message = error.message; // Si l'erreur a des détails de validation, les inclure if (error.details && Array.isArray(error.details) && error.details.length > 0) { const detailsMessages = error.details .map((detail) => `${detail.field}: ${detail.message}`) .join(', '); message = `${error.message} (${detailsMessages})`; } // Optionnellement inclure le request_id pour le debugging (en mode développement) if (includeRequestId && error.request_id) { const isDev = import.meta.env.DEV; if (isDev) { message = `${message} [Request ID: ${error.request_id}]`; } } return message; } /** * Extrait les erreurs de validation par champ * @param error - ApiError * @returns Record avec les erreurs par champ (field -> message) */ export function getValidationErrors( error: ApiError, ): Record { if (!error.details || !Array.isArray(error.details)) { return {}; } const errors: Record = {}; for (const detail of error.details) { if (detail.field && detail.message) { errors[detail.field] = detail.message; } } return errors; } /** * Vérifie si une erreur est une ApiError */ function isApiError(error: unknown): error is ApiError { return ( typeof error === 'object' && error !== null && 'code' in error && 'message' in error && typeof (error as any).code === 'number' && typeof (error as any).message === 'string' ); } /** * Vérifie si une erreur est une AxiosError */ function isAxiosError(error: unknown): error is AxiosError { return ( typeof error === 'object' && error !== null && 'isAxiosError' in error && (error as any).isAxiosError === true ); }