2025-12-25 12:20:07 +00:00
/ * *
* 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
2026-01-11 15:30:43 +00:00
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." ,
2025-12-25 12:20:07 +00:00
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 ) ;
}