/** * Error Messages Utility * FE-API-013: Centralized user-friendly error messages * * Provides consistent, user-friendly error messages across the application * with support for internationalization and context-aware messages. */ import type { ApiError } from '@/types/api'; /** * User-friendly error messages mapped by HTTP status codes */ export const ERROR_MESSAGES = { // Client errors (4xx) 400: 'La requête est invalide. Veuillez vérifier les informations fournies.', 401: 'Vous devez être connecté pour effectuer cette action.', 403: "Vous n'avez pas les permissions nécessaires pour effectuer cette action.", 404: 'La ressource demandée est introuvable.', 409: 'Un conflit est survenu. Cette ressource existe déjà ou a été modifiée.', 422: 'Les données fournies ne sont pas valides.', 429: 'Trop de requêtes. Veuillez patienter quelques instants avant de réessayer.', // Server errors (5xx) 500: "Une erreur serveur s'est produite. Veuillez réessayer plus tard.", 502: 'Erreur de communication avec le serveur. Veuillez réessayer plus tard.', 503: 'Service temporairement indisponible. Veuillez réessayer dans quelques instants.', 504: 'Le serveur met trop de temps à répondre. Veuillez réessayer plus tard.', // Network errors NETWORK: 'Erreur de connexion. Vérifiez votre connexion internet et réessayez. Si le problème persiste, le serveur pourrait être temporairement indisponible.', TIMEOUT: 'La requête a expiré. Vérifiez votre connexion internet et réessayez.', UNKNOWN: "Une erreur inattendue s'est produite. Veuillez réessayer.", } as const; /** * Context-specific error messages */ export const CONTEXT_ERROR_MESSAGES = { auth: { login: 'Échec de la connexion. Vérifiez vos identifiants.', logout: 'Erreur lors de la déconnexion.', register: "Erreur lors de l'inscription. Veuillez réessayer.", tokenExpired: 'Votre session a expiré. Veuillez vous reconnecter.', }, upload: { fileTooLarge: 'Le fichier est trop volumineux.', invalidFormat: "Le format de fichier n'est pas supporté.", uploadFailed: "L'upload a échoué. Veuillez réessayer.", networkError: "Erreur réseau lors de l'upload. Vérifiez votre connexion.", }, playlist: { notFound: 'La playlist est introuvable.', accessDenied: "Vous n'avez pas accès à cette playlist.", createFailed: 'Erreur lors de la création de la playlist.', updateFailed: 'Erreur lors de la mise à jour de la playlist.', deleteFailed: 'Erreur lors de la suppression de la playlist.', }, track: { notFound: 'Le morceau est introuvable.', playFailed: 'Impossible de lire le morceau. Vérifiez votre connexion.', uploadFailed: "Erreur lors de l'upload du morceau.", deleteFailed: 'Erreur lors de la suppression du morceau.', }, conversation: { notFound: 'La conversation est introuvable.', accessDenied: "Vous n'avez pas accès à cette conversation.", createFailed: 'Erreur lors de la création de la conversation.', sendMessageFailed: "Erreur lors de l'envoi du message.", }, search: { failed: 'La recherche a échoué. Veuillez réessayer.', timeout: 'La recherche a pris trop de temps. Veuillez réessayer.', invalidQuery: 'La requête de recherche est invalide.', }, } as const; /** * Gets a user-friendly error message based on status code * @param status HTTP status code * @param defaultMessage Optional default message if status not found * @returns User-friendly error message */ export function getErrorMessageByStatus( status: number, defaultMessage?: string, ): string { if (status in ERROR_MESSAGES) { return ERROR_MESSAGES[status as keyof typeof ERROR_MESSAGES]; } return defaultMessage || ERROR_MESSAGES.UNKNOWN; } /** * Gets a context-specific error message * @param context Context key (e.g., 'auth', 'upload') * @param action Action key (e.g., 'login', 'uploadFailed') * @param defaultMessage Optional default message if context/action not found * @returns Context-specific error message */ export function getContextErrorMessage( context: keyof typeof CONTEXT_ERROR_MESSAGES, action: string, defaultMessage?: string, ): string { const contextMessages = CONTEXT_ERROR_MESSAGES[context]; if (contextMessages && action in contextMessages) { return (contextMessages as any)[action]; } return defaultMessage || ERROR_MESSAGES.UNKNOWN; } /** * Formats an ApiError into a user-friendly message * @param error ApiError object * @param context Optional context for context-specific messages * @param includeDetails Whether to include validation details in the message * @returns Formatted user-friendly error message */ export function formatUserFriendlyError( error: ApiError | Error | unknown, context?: keyof typeof CONTEXT_ERROR_MESSAGES, includeDetails: boolean = false, ): string { // Handle ApiError if ( error && typeof error === 'object' && 'code' in error && 'message' in error ) { const apiError = error as ApiError; const status = typeof apiError.code === 'number' ? apiError.code : 0; // Try context-specific message first if (context && status >= 400 && status < 500) { // Try to extract action from error message or use status const action = extractActionFromMessage(apiError.message); const contextMessage = getContextErrorMessage(context, action, undefined); if (contextMessage !== ERROR_MESSAGES.UNKNOWN) { return contextMessage; } } // Use status-based message if (status > 0) { const statusMessage = getErrorMessageByStatus(status, apiError.message); // Add validation details if requested if ( includeDetails && apiError.details && Array.isArray(apiError.details) ) { const details = apiError.details .map((d: any) => d.message || d.field) .filter(Boolean) .join(', '); if (details) { return `${statusMessage} (${details})`; } } return statusMessage; } // Fallback to error message return apiError.message || ERROR_MESSAGES.UNKNOWN; } // Handle standard Error if (error instanceof Error) { return error.message || ERROR_MESSAGES.UNKNOWN; } // Handle network errors if (error && typeof error === 'object' && 'code' in error) { const code = (error as any).code; if (code === 'ECONNABORTED' || code === 'ETIMEDOUT') { return ERROR_MESSAGES.TIMEOUT; } if (code === 'ERR_NETWORK' || !(error as any).response) { return ERROR_MESSAGES.NETWORK; } } return ERROR_MESSAGES.UNKNOWN; } /** * Extracts action from error message for context matching * @param message Error message * @returns Extracted action key */ function extractActionFromMessage(message: string): string { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('login') || lowerMessage.includes('connexion')) { return 'login'; } if (lowerMessage.includes('logout') || lowerMessage.includes('déconnexion')) { return 'logout'; } if ( lowerMessage.includes('register') || lowerMessage.includes('inscription') ) { return 'register'; } if ( lowerMessage.includes('upload') || lowerMessage.includes('téléchargement') ) { if (lowerMessage.includes('large') || lowerMessage.includes('volumineux')) { return 'fileTooLarge'; } if (lowerMessage.includes('format') || lowerMessage.includes('type')) { return 'invalidFormat'; } return 'uploadFailed'; } if ( lowerMessage.includes('not found') || lowerMessage.includes('introuvable') ) { return 'notFound'; } if ( lowerMessage.includes('access denied') || lowerMessage.includes('permission') ) { return 'accessDenied'; } if (lowerMessage.includes('create') || lowerMessage.includes('créer')) { return 'createFailed'; } if (lowerMessage.includes('update') || lowerMessage.includes('mise à jour')) { return 'updateFailed'; } if (lowerMessage.includes('delete') || lowerMessage.includes('suppression')) { return 'deleteFailed'; } return ''; } /** * Checks if an error is retryable * @param error Error object * @returns True if the error is retryable */ export function isRetryableError(error: unknown): boolean { if (error && typeof error === 'object' && 'code' in error) { const apiError = error as ApiError; const status = typeof apiError.code === 'number' ? apiError.code : 0; // Retryable status codes: 429, 500, 502, 503, 504 if ([429, 500, 502, 503, 504].includes(status)) { return true; } // Network errors are retryable const code = (error as any).code; if ( code === 'ECONNABORTED' || code === 'ETIMEDOUT' || code === 'ERR_NETWORK' ) { return true; } } return false; } /** * Gets retry delay in milliseconds based on error * @param error Error object * @param attempt Current retry attempt (0-indexed) * @returns Delay in milliseconds */ export function getRetryDelay(error: unknown, attempt: number = 0): number { if (error && typeof error === 'object' && 'code' in error) { const apiError = error as ApiError; const status = typeof apiError.code === 'number' ? apiError.code : 0; // Rate limit: use retry_after if available if (status === 429 && 'retry_after' in apiError) { const retryAfter = (apiError as any).retry_after; if (typeof retryAfter === 'number') { return retryAfter * 1000; } } } // Exponential backoff: 1s, 2s, 4s, 8s, max 30s return Math.min(1000 * Math.pow(2, attempt), 30000); }