2025-12-16 19:40:16 +00:00
|
|
|
import { AxiosError } from 'axios';
|
2025-12-25 12:22:15 +00:00
|
|
|
import { isTimeoutError, TIMEOUT_MESSAGES } from './timeoutHandler';
|
2026-01-11 16:42:04 +00:00
|
|
|
import { isOffline } from './offlineDetection';
|
2025-12-16 19:40:16 +00:00
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
/**
|
|
|
|
|
* Parse une erreur Axios en ApiError standardisé
|
|
|
|
|
* @param error - Erreur Axios ou Error
|
|
|
|
|
* @returns ApiError formaté selon le contrat backend
|
|
|
|
|
*/
|
|
|
|
|
// Structure interne de l'erreur backend
|
|
|
|
|
interface BackendErrorDetail {
|
|
|
|
|
code?: number | string;
|
|
|
|
|
message?: string;
|
|
|
|
|
details?: unknown;
|
|
|
|
|
retry_after?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Structure de réponse standardisée { success: false, error: ... }
|
|
|
|
|
interface StandardErrorResponse {
|
|
|
|
|
success: boolean;
|
|
|
|
|
error: BackendErrorDetail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Structure { error: ... } (Middleware différent)
|
|
|
|
|
interface NestedErrorResponse {
|
|
|
|
|
error: BackendErrorDetail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Structure directe { code: ..., message: ... }
|
|
|
|
|
interface DirectErrorResponse {
|
|
|
|
|
code: number | string;
|
|
|
|
|
message: string;
|
|
|
|
|
details?: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 19:40:16 +00:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-01-07 18:39:21 +00:00
|
|
|
const responseData = axiosError.response?.data;
|
2025-12-16 19:40:16 +00:00
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
// Type guard helpers locaux
|
|
|
|
|
const isStandardError = (data: unknown): data is StandardErrorResponse => {
|
|
|
|
|
return (
|
|
|
|
|
typeof data === 'object' &&
|
|
|
|
|
data !== null &&
|
|
|
|
|
'success' in data &&
|
|
|
|
|
(data as any).success === false &&
|
|
|
|
|
'error' in data
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-12-16 19:40:16 +00:00
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
const isNestedError = (data: unknown): data is NestedErrorResponse => {
|
|
|
|
|
return (
|
|
|
|
|
typeof data === 'object' &&
|
|
|
|
|
data !== null &&
|
|
|
|
|
'error' in data &&
|
|
|
|
|
typeof (data as any).error === 'object'
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isDirectError = (data: unknown): data is DirectErrorResponse => {
|
|
|
|
|
return (
|
|
|
|
|
typeof data === 'object' &&
|
|
|
|
|
data !== null &&
|
|
|
|
|
'code' in data &&
|
|
|
|
|
'message' in data
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (responseData) {
|
|
|
|
|
// 1. Format standardisé { success: false, error: {...} }
|
|
|
|
|
if (isStandardError(responseData)) {
|
|
|
|
|
return normalizeApiError(responseData.error);
|
2025-12-22 21:00:50 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
// 2. Format { error: {...} } sans success property
|
|
|
|
|
if (isNestedError(responseData)) {
|
|
|
|
|
const backendError = responseData.error;
|
|
|
|
|
if (backendError && ('code' in backendError || 'message' in backendError)) {
|
|
|
|
|
return normalizeApiError(backendError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Format direct { code: ..., message: ... }
|
|
|
|
|
if (isDirectError(responseData)) {
|
|
|
|
|
return normalizeApiError(responseData);
|
|
|
|
|
}
|
2025-12-16 19:40:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Erreur réseau (pas de réponse)
|
2026-01-11 16:41:08 +00:00
|
|
|
// Action 3.5.1.1: Enhance network error detection - distinguish timeout vs connection refused vs offline
|
2025-12-16 19:40:16 +00:00
|
|
|
if (axiosError.request && !axiosError.response) {
|
2025-12-25 12:22:15 +00:00
|
|
|
// Check if it's a timeout error
|
|
|
|
|
if (isTimeoutError(axiosError)) {
|
|
|
|
|
return {
|
|
|
|
|
code: 0,
|
|
|
|
|
message: TIMEOUT_MESSAGES.timeout,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-11 16:41:08 +00:00
|
|
|
|
|
|
|
|
// Check for connection refused (server is down or unreachable)
|
|
|
|
|
if (axiosError.code === 'ECONNREFUSED' || axiosError.code === 'ERR_CONNECTION_REFUSED') {
|
|
|
|
|
return {
|
|
|
|
|
code: 0,
|
|
|
|
|
message: 'Connection refused: The server is not responding. Please try again later.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 16:42:04 +00:00
|
|
|
// Action 3.5.1.3: Integrate offline detection with error handler
|
2026-01-11 16:41:08 +00:00
|
|
|
// Check for network unreachable or no internet connection
|
|
|
|
|
if (
|
|
|
|
|
axiosError.code === 'ENETUNREACH' ||
|
|
|
|
|
axiosError.code === 'ERR_NETWORK' ||
|
|
|
|
|
axiosError.code === 'ERR_INTERNET_DISCONNECTED' ||
|
2026-01-11 16:42:04 +00:00
|
|
|
isOffline()
|
2026-01-11 16:41:08 +00:00
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
code: 0,
|
|
|
|
|
message: 'No internet connection: Please check your network settings and try again.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic network error fallback
|
2025-12-16 19:40:16 +00:00
|
|
|
return {
|
|
|
|
|
code: 0,
|
2026-01-11 16:41:08 +00:00
|
|
|
message: 'Network error: Unable to connect to server. Please check your connection and try again.',
|
2025-12-16 19:40:16 +00:00
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 21:00:50 +00:00
|
|
|
// Gestion spécifique des codes HTTP d'erreur
|
|
|
|
|
const status = axiosError.response?.status;
|
|
|
|
|
|
|
|
|
|
if (status === 429) {
|
|
|
|
|
// Too Many Requests - Rate limiting
|
2025-12-25 14:29:31 +00:00
|
|
|
const headers = axiosError.response?.headers || {};
|
2026-01-07 18:39:21 +00:00
|
|
|
const data = responseData as { error?: { message?: string; retry_after?: number } } | null;
|
|
|
|
|
|
|
|
|
|
const rateLimitLimit = headers['x-ratelimit-limit']
|
|
|
|
|
? parseInt(String(headers['x-ratelimit-limit']), 10)
|
2025-12-25 14:29:31 +00:00
|
|
|
: undefined;
|
2026-01-07 18:39:21 +00:00
|
|
|
const rateLimitRemaining = headers['x-ratelimit-remaining']
|
|
|
|
|
? parseInt(String(headers['x-ratelimit-remaining']), 10)
|
2025-12-25 14:29:31 +00:00
|
|
|
: undefined;
|
2026-01-07 18:39:21 +00:00
|
|
|
const rateLimitReset = headers['x-ratelimit-reset']
|
|
|
|
|
? parseInt(String(headers['x-ratelimit-reset']), 10)
|
2025-12-25 14:29:31 +00:00
|
|
|
: undefined;
|
2026-01-07 18:39:21 +00:00
|
|
|
const retryAfter = headers['retry-after']
|
|
|
|
|
? parseInt(String(headers['retry-after']), 10)
|
2025-12-25 14:29:31 +00:00
|
|
|
: (data?.error?.retry_after || 60);
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2025-12-25 14:29:31 +00:00
|
|
|
const resetTime = rateLimitReset ? new Date(rateLimitReset * 1000) : undefined;
|
2026-01-07 18:39:21 +00:00
|
|
|
const secondsUntilReset = resetTime
|
2025-12-25 14:29:31 +00:00
|
|
|
? Math.max(0, Math.ceil((resetTime.getTime() - Date.now()) / 1000))
|
|
|
|
|
: retryAfter;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2025-12-22 21:00:50 +00:00
|
|
|
return {
|
|
|
|
|
code: 429,
|
|
|
|
|
message:
|
2025-12-25 14:29:31 +00:00
|
|
|
data?.error?.message ||
|
2025-12-22 21:00:50 +00:00
|
|
|
'Trop de requêtes. Veuillez patienter avant de réessayer.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
2025-12-25 14:29:31 +00:00
|
|
|
details: [
|
|
|
|
|
{
|
|
|
|
|
field: 'rate_limit',
|
|
|
|
|
message: `Limite de ${rateLimitLimit || 'N/A'} requêtes atteinte. Réessayez dans ${secondsUntilReset} seconde${secondsUntilReset > 1 ? 's' : ''}.`,
|
|
|
|
|
},
|
|
|
|
|
...(rateLimitRemaining !== undefined
|
|
|
|
|
? [
|
2026-01-07 18:39:21 +00:00
|
|
|
{
|
|
|
|
|
field: 'remaining',
|
|
|
|
|
message: `${rateLimitRemaining} requête${rateLimitRemaining > 1 ? 's' : ''} restante${rateLimitRemaining > 1 ? 's' : ''}`,
|
|
|
|
|
},
|
|
|
|
|
]
|
2025-12-25 14:29:31 +00:00
|
|
|
: []),
|
|
|
|
|
],
|
|
|
|
|
retry_after: secondsUntilReset,
|
2025-12-22 21:00:50 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 503) {
|
2026-01-07 18:39:21 +00:00
|
|
|
const data = responseData as { message?: string; details?: unknown } | null;
|
2025-12-22 21:00:50 +00:00
|
|
|
return {
|
|
|
|
|
code: 503,
|
|
|
|
|
message:
|
2026-01-07 18:39:21 +00:00
|
|
|
data?.message ||
|
2025-12-22 21:00:50 +00:00
|
|
|
'Service temporairement indisponible. Veuillez réessayer dans quelques instants.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
2026-01-07 18:39:21 +00:00
|
|
|
details: normalizeDetails(data?.details),
|
2025-12-22 21:00:50 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 502) {
|
2026-01-07 18:39:21 +00:00
|
|
|
const data = responseData as { message?: string; details?: unknown } | null;
|
2025-12-22 21:00:50 +00:00
|
|
|
return {
|
|
|
|
|
code: 502,
|
|
|
|
|
message:
|
2026-01-07 18:39:21 +00:00
|
|
|
data?.message ||
|
2025-12-22 21:00:50 +00:00
|
|
|
'Erreur de communication avec le serveur. Veuillez réessayer plus tard.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
2026-01-07 18:39:21 +00:00
|
|
|
details: normalizeDetails(data?.details),
|
2025-12-22 21:00:50 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 19:40:16 +00:00
|
|
|
// Erreur HTTP sans format standardisé
|
2026-01-07 18:39:21 +00:00
|
|
|
const data = responseData as { message?: string } | null;
|
2025-12-16 19:40:16 +00:00
|
|
|
return {
|
2025-12-22 21:00:50 +00:00
|
|
|
code: status || 0,
|
2025-12-16 19:40:16 +00:00
|
|
|
message:
|
2026-01-07 18:39:21 +00:00
|
|
|
data?.message ||
|
2025-12-16 19:40:16 +00:00
|
|
|
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é
|
|
|
|
|
*/
|
2026-01-07 18:39:21 +00:00
|
|
|
// Interface interne pour normalizeApiError dont on ne connait pas la structure exacte avant runtime
|
|
|
|
|
interface RawBackendError {
|
|
|
|
|
code?: unknown;
|
|
|
|
|
message?: unknown;
|
|
|
|
|
details?: unknown;
|
|
|
|
|
request_id?: unknown;
|
|
|
|
|
timestamp?: unknown;
|
|
|
|
|
context?: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ErrorDetail {
|
|
|
|
|
field: string;
|
|
|
|
|
message: string;
|
|
|
|
|
value?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalise les détails d'une erreur
|
|
|
|
|
*/
|
|
|
|
|
function normalizeDetails(details: unknown): ErrorDetail[] | undefined {
|
|
|
|
|
if (!Array.isArray(details)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtrer pour ne garder que les objets valides qui ressemblent à des ErrorDetail
|
|
|
|
|
const validDetails = details.filter((item): item is ErrorDetail => {
|
|
|
|
|
return (
|
|
|
|
|
typeof item === 'object' &&
|
|
|
|
|
item !== null &&
|
|
|
|
|
'field' in item &&
|
|
|
|
|
'message' in item &&
|
|
|
|
|
typeof (item as any).field === 'string' &&
|
|
|
|
|
typeof (item as any).message === 'string'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return validDetails.length > 0 ? validDetails : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalise le contexte d'une erreur
|
|
|
|
|
*/
|
|
|
|
|
function normalizeContext(context: unknown): Record<string, any> | undefined {
|
|
|
|
|
if (typeof context === 'object' && context !== null && !Array.isArray(context)) {
|
|
|
|
|
return context as Record<string, any>;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalise un objet d'erreur backend en ApiError standardisé
|
|
|
|
|
*/
|
|
|
|
|
function normalizeApiError(error: unknown): ApiError {
|
|
|
|
|
const err = error as RawBackendError;
|
2025-12-16 19:40:16 +00:00
|
|
|
return {
|
2026-01-07 18:39:21 +00:00
|
|
|
code: typeof err.code === 'number' ? err.code : parseInt(String(err.code || 0), 10),
|
|
|
|
|
message: typeof err.message === 'string' ? err.message : 'An error occurred',
|
|
|
|
|
details: normalizeDetails(err.details),
|
|
|
|
|
request_id: typeof err.request_id === 'string' ? err.request_id : undefined,
|
|
|
|
|
timestamp: typeof err.timestamp === 'string' ? err.timestamp : new Date().toISOString(),
|
|
|
|
|
context: normalizeContext(err.context),
|
2025-12-16 19:40:16 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formate un message d'erreur pour l'affichage dans l'UI
|
|
|
|
|
* @param error - ApiError
|
2025-12-22 22:13:49 +00:00
|
|
|
* @param includeRequestId - Si true, inclut le request_id dans le message (pour debugging)
|
2025-12-16 19:40:16 +00:00
|
|
|
* @returns Message formaté pour l'utilisateur
|
|
|
|
|
*/
|
2025-12-22 22:13:49 +00:00
|
|
|
export function formatErrorMessage(
|
|
|
|
|
error: ApiError,
|
|
|
|
|
includeRequestId: boolean = false,
|
|
|
|
|
): string {
|
|
|
|
|
let message = error.message;
|
|
|
|
|
|
2025-12-16 19:40:16 +00:00
|
|
|
// 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(', ');
|
2025-12-22 22:13:49 +00:00
|
|
|
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}]`;
|
|
|
|
|
}
|
2025-12-16 19:40:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-22 22:13:49 +00:00
|
|
|
return message;
|
2025-12-16 19:40:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-11 16:14:49 +00:00
|
|
|
/**
|
|
|
|
|
* Error category types for error handling strategy
|
|
|
|
|
*/
|
|
|
|
|
export type ErrorCategory =
|
|
|
|
|
| 'network'
|
|
|
|
|
| 'validation'
|
|
|
|
|
| 'authentication'
|
|
|
|
|
| 'authorization'
|
|
|
|
|
| 'not_found'
|
|
|
|
|
| 'rate_limit'
|
|
|
|
|
| 'server_error'
|
|
|
|
|
| 'timeout'
|
|
|
|
|
| 'unknown';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Categorizes an ApiError into a specific error category
|
|
|
|
|
* Used for determining appropriate error handling strategy (display, retry, redirect, etc.)
|
|
|
|
|
*
|
|
|
|
|
* @param error - ApiError to categorize
|
|
|
|
|
* @returns ErrorCategory indicating the type of error
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* const category = getErrorCategory(apiError);
|
|
|
|
|
* if (category === 'network') {
|
|
|
|
|
* // Show offline indicator
|
|
|
|
|
* } else if (category === 'authentication') {
|
|
|
|
|
* // Redirect to login
|
|
|
|
|
* }
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export function getErrorCategory(error: ApiError | Error | unknown): ErrorCategory {
|
|
|
|
|
// Check for AxiosError patterns first (before ApiError)
|
|
|
|
|
if (error && typeof error === 'object' && 'isAxiosError' in error) {
|
|
|
|
|
const axiosError = error as AxiosError;
|
|
|
|
|
// Network error (no response)
|
|
|
|
|
if (!axiosError.response && axiosError.request) {
|
|
|
|
|
return 'network';
|
|
|
|
|
}
|
|
|
|
|
// Timeout
|
|
|
|
|
if (axiosError.code === 'ECONNABORTED' || axiosError.code === 'ETIMEDOUT') {
|
|
|
|
|
return 'timeout';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle ApiError
|
|
|
|
|
if (error && typeof error === 'object' && 'code' in error) {
|
|
|
|
|
const apiError = error as ApiError;
|
|
|
|
|
const code = typeof apiError.code === 'number' ? apiError.code : parseInt(String(apiError.code || 0), 10);
|
|
|
|
|
|
|
|
|
|
// Network errors (code 0 typically indicates network issues)
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
return 'network';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HTTP status code based categorization
|
|
|
|
|
if (code >= 400 && code < 500) {
|
|
|
|
|
// Client errors
|
|
|
|
|
if (code === 401) {
|
|
|
|
|
return 'authentication';
|
|
|
|
|
}
|
|
|
|
|
if (code === 403) {
|
|
|
|
|
return 'authorization';
|
|
|
|
|
}
|
|
|
|
|
if (code === 404) {
|
|
|
|
|
return 'not_found';
|
|
|
|
|
}
|
|
|
|
|
if (code === 422) {
|
|
|
|
|
// Validation errors typically have details array
|
|
|
|
|
if (apiError.details && Array.isArray(apiError.details) && apiError.details.length > 0) {
|
|
|
|
|
return 'validation';
|
|
|
|
|
}
|
|
|
|
|
return 'validation';
|
|
|
|
|
}
|
|
|
|
|
if (code === 429) {
|
|
|
|
|
return 'rate_limit';
|
|
|
|
|
}
|
|
|
|
|
// Other 4xx errors
|
|
|
|
|
return 'validation';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (code >= 500 && code < 600) {
|
|
|
|
|
// Server errors
|
|
|
|
|
if (code === 504 || code === 408) {
|
|
|
|
|
return 'timeout';
|
|
|
|
|
}
|
|
|
|
|
return 'server_error';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle standard Error objects
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
const message = error.message.toLowerCase();
|
|
|
|
|
const name = error.name.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Network-related errors
|
|
|
|
|
if (
|
|
|
|
|
message.includes('network') ||
|
|
|
|
|
message.includes('fetch') ||
|
|
|
|
|
message.includes('connection') ||
|
|
|
|
|
message.includes('offline') ||
|
|
|
|
|
name === 'networkerror' ||
|
|
|
|
|
name === 'typeerror'
|
|
|
|
|
) {
|
|
|
|
|
return 'network';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Timeout errors
|
|
|
|
|
if (message.includes('timeout') || name === 'timeouterror') {
|
|
|
|
|
return 'timeout';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Abort errors
|
|
|
|
|
if (message.includes('abort') || name === 'aborterror') {
|
|
|
|
|
return 'network';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'unknown';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 19:40:16 +00:00
|
|
|
/**
|
|
|
|
|
* 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<string, string> {
|
|
|
|
|
if (!error.details || !Array.isArray(error.details)) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errors: Record<string, string> = {};
|
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|