2025-12-25 12:20:07 +00:00
|
|
|
/**
|
|
|
|
|
* Error Messages Utility
|
|
|
|
|
* FE-API-013: Centralized user-friendly error messages
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2025-12-25 12:20:07 +00:00
|
|
|
* Provides consistent, user-friendly error messages across the application
|
|
|
|
|
* with support for internationalization and context-aware messages.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-15 16:03:35 +00:00
|
|
|
import type { ApiError } from '@/schemas/apiSchemas';
|
2025-12-25 12:20:07 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* User-friendly error messages mapped by HTTP status codes
|
|
|
|
|
*/
|
|
|
|
|
export const ERROR_MESSAGES = {
|
|
|
|
|
// Client errors (4xx)
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
400: 'Invalid request. Please check the information provided.',
|
|
|
|
|
401: 'You must be logged in to perform this action.',
|
|
|
|
|
403: 'You do not have permission to perform this action.',
|
|
|
|
|
404: 'The requested resource was not found.',
|
|
|
|
|
409: 'A conflict occurred. This resource already exists or has been modified.',
|
|
|
|
|
422: 'The data provided is not valid.',
|
|
|
|
|
429: 'Too many requests. Please wait a moment before trying again.',
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Server errors (5xx)
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
500: 'A server error occurred. Please try again later.',
|
|
|
|
|
502: 'Server communication error. Please try again later.',
|
|
|
|
|
503: 'Service temporarily unavailable. Please try again in a moment.',
|
|
|
|
|
504: 'The server is taking too long to respond. Please try again later.',
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Network errors
|
2026-01-13 18:47:57 +00:00
|
|
|
NETWORK:
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
'Connection error. Check your internet connection and try again. If the problem persists, the server may be temporarily unavailable.',
|
2026-01-13 18:47:57 +00:00
|
|
|
TIMEOUT:
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
'The request timed out. Check your internet connection and try again.',
|
|
|
|
|
UNKNOWN: 'An unexpected error occurred. Please try again.',
|
2025-12-25 12:20:07 +00:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Context-specific error messages
|
|
|
|
|
*/
|
|
|
|
|
export const CONTEXT_ERROR_MESSAGES = {
|
|
|
|
|
auth: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
login: 'Login failed. Check your credentials.',
|
|
|
|
|
logout: 'Error during logout.',
|
|
|
|
|
register: 'Error during registration. Please try again.',
|
|
|
|
|
tokenExpired: 'Your session has expired. Please log in again.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
upload: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
fileTooLarge: 'The file is too large.',
|
|
|
|
|
invalidFormat: 'The file format is not supported.',
|
|
|
|
|
uploadFailed: 'Upload failed. Please try again.',
|
|
|
|
|
networkError: 'Network error during upload. Check your connection.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
playlist: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
notFound: 'Playlist not found.',
|
|
|
|
|
accessDenied: 'You do not have access to this playlist.',
|
|
|
|
|
createFailed: 'Error creating the playlist.',
|
|
|
|
|
updateFailed: 'Error updating the playlist.',
|
|
|
|
|
deleteFailed: 'Error deleting the playlist.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
track: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
notFound: 'Track not found.',
|
|
|
|
|
playFailed: 'Unable to play track. Check your connection.',
|
|
|
|
|
uploadFailed: 'Error uploading the track.',
|
|
|
|
|
deleteFailed: 'Error deleting the track.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
conversation: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
notFound: 'Conversation not found.',
|
|
|
|
|
accessDenied: 'You do not have access to this conversation.',
|
|
|
|
|
createFailed: 'Error creating the conversation.',
|
|
|
|
|
sendMessageFailed: 'Error sending the message.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
search: {
|
feat: UI components, services, utils, i18n, and routing
Update shared components (ComingSoon, SelectTrigger, AnnouncementBanner,
modals, social cards). Add usePatina hook. Refine API services, error
handling, query invalidation, state management. Update i18n strings
(en/fr/es). Update routing and app configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:42 +00:00
|
|
|
failed: 'Search failed. Please try again.',
|
|
|
|
|
timeout: 'Search took too long. Please try again.',
|
|
|
|
|
invalidQuery: 'Invalid search query.',
|
2025-12-25 12:20:07 +00:00
|
|
|
},
|
|
|
|
|
} 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
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
error &&
|
|
|
|
|
typeof error === 'object' &&
|
|
|
|
|
'code' in error &&
|
|
|
|
|
'message' in error
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
const apiError = error as ApiError;
|
|
|
|
|
const status = typeof apiError.code === 'number' ? apiError.code : 0;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Use status-based message
|
|
|
|
|
if (status > 0) {
|
|
|
|
|
const statusMessage = getErrorMessageByStatus(status, apiError.message);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Add validation details if requested
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
includeDetails &&
|
|
|
|
|
apiError.details &&
|
|
|
|
|
Array.isArray(apiError.details)
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
const details = apiError.details
|
2026-02-22 16:44:49 +00:00
|
|
|
.map((d: { message?: string; field?: string }) => d.message || d.field)
|
2025-12-25 12:20:07 +00:00
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(', ');
|
|
|
|
|
if (details) {
|
|
|
|
|
return `${statusMessage} (${details})`;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
return statusMessage;
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Fallback to error message
|
|
|
|
|
return apiError.message || ERROR_MESSAGES.UNKNOWN;
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Handle standard Error
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
return error.message || ERROR_MESSAGES.UNKNOWN;
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
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();
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
if (lowerMessage.includes('login') || lowerMessage.includes('connexion')) {
|
|
|
|
|
return 'login';
|
|
|
|
|
}
|
|
|
|
|
if (lowerMessage.includes('logout') || lowerMessage.includes('déconnexion')) {
|
|
|
|
|
return 'logout';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
lowerMessage.includes('register') ||
|
|
|
|
|
lowerMessage.includes('inscription')
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
return 'register';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
lowerMessage.includes('upload') ||
|
|
|
|
|
lowerMessage.includes('téléchargement')
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
if (lowerMessage.includes('large') || lowerMessage.includes('volumineux')) {
|
|
|
|
|
return 'fileTooLarge';
|
|
|
|
|
}
|
|
|
|
|
if (lowerMessage.includes('format') || lowerMessage.includes('type')) {
|
|
|
|
|
return 'invalidFormat';
|
|
|
|
|
}
|
|
|
|
|
return 'uploadFailed';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
lowerMessage.includes('not found') ||
|
|
|
|
|
lowerMessage.includes('introuvable')
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
return 'notFound';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
lowerMessage.includes('access denied') ||
|
|
|
|
|
lowerMessage.includes('permission')
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
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';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
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;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Retryable status codes: 429, 500, 502, 503, 504
|
|
|
|
|
if ([429, 500, 502, 503, 504].includes(status)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Network errors are retryable
|
|
|
|
|
const code = (error as any).code;
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
code === 'ECONNABORTED' ||
|
|
|
|
|
code === 'ETIMEDOUT' ||
|
|
|
|
|
code === 'ERR_NETWORK'
|
|
|
|
|
) {
|
2025-12-25 12:20:07 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
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;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:20:07 +00:00
|
|
|
// Exponential backoff: 1s, 2s, 4s, 8s, max 30s
|
|
|
|
|
return Math.min(1000 * Math.pow(2, attempt), 30000);
|
|
|
|
|
}
|