veza/apps/web/src/utils/errorMessages.ts

275 lines
9.3 KiB
TypeScript
Raw Normal View History

/**
* 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.",
TIMEOUT: "La requête a expiré. Veuillez réessayer.",
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);
}