2025-12-16 19:40:16 +00:00
|
|
|
import { AxiosError } from 'axios';
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
|
|
// Si le backend retourne le format standardisé { success: false, error: {...} }
|
|
|
|
|
if (
|
|
|
|
|
axiosError.response?.data &&
|
|
|
|
|
typeof axiosError.response.data === 'object' &&
|
|
|
|
|
'success' in axiosError.response.data &&
|
|
|
|
|
axiosError.response.data.success === false &&
|
|
|
|
|
'error' in axiosError.response.data
|
|
|
|
|
) {
|
|
|
|
|
const backendError = (axiosError.response.data as { error: any }).error;
|
|
|
|
|
return normalizeApiError(backendError);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 21:00:50 +00:00
|
|
|
// Si le backend retourne le format { error: {...} } sans success property (Middleware Gin actuel)
|
|
|
|
|
if (
|
|
|
|
|
axiosError.response?.data &&
|
|
|
|
|
typeof axiosError.response.data === 'object' &&
|
|
|
|
|
'error' in axiosError.response.data &&
|
|
|
|
|
typeof (axiosError.response.data as any).error === 'object'
|
|
|
|
|
) {
|
|
|
|
|
const backendError = (axiosError.response.data as { error: any }).error;
|
|
|
|
|
// Vérifier si l'objet error contient au moins code ou message pour éviter les faux positifs
|
|
|
|
|
if (backendError && ('code' in backendError || 'message' in backendError)) {
|
|
|
|
|
return normalizeApiError(backendError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si le backend retourne directement un objet error (Legacy ou autre middleware)
|
2025-12-16 19:40:16 +00:00
|
|
|
if (
|
|
|
|
|
axiosError.response?.data &&
|
|
|
|
|
typeof axiosError.response.data === 'object' &&
|
|
|
|
|
'code' in axiosError.response.data &&
|
|
|
|
|
'message' in axiosError.response.data
|
|
|
|
|
) {
|
|
|
|
|
return normalizeApiError(axiosError.response.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Erreur réseau (pas de réponse)
|
|
|
|
|
if (axiosError.request && !axiosError.response) {
|
|
|
|
|
return {
|
|
|
|
|
code: 0,
|
|
|
|
|
message: 'Network error: Unable to connect to server',
|
|
|
|
|
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
|
|
|
|
|
const retryAfter = axiosError.response?.headers?.['retry-after'];
|
|
|
|
|
const retryAfterSeconds = retryAfter ? parseInt(String(retryAfter), 10) : undefined;
|
|
|
|
|
return {
|
|
|
|
|
code: 429,
|
|
|
|
|
message:
|
|
|
|
|
(axiosError.response?.data as any)?.message ||
|
|
|
|
|
'Trop de requêtes. Veuillez patienter avant de réessayer.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: retryAfterSeconds
|
|
|
|
|
? [{ field: 'retry_after', message: `Réessayez dans ${retryAfterSeconds} secondes` }]
|
|
|
|
|
: undefined,
|
|
|
|
|
retry_after: retryAfterSeconds,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 503) {
|
|
|
|
|
// Service Unavailable - ClamAV ou autre service externe
|
|
|
|
|
return {
|
|
|
|
|
code: 503,
|
|
|
|
|
message:
|
|
|
|
|
(axiosError.response?.data as any)?.message ||
|
|
|
|
|
'Service temporairement indisponible. Veuillez réessayer dans quelques instants.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: (axiosError.response?.data as any)?.details,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 502) {
|
|
|
|
|
// Bad Gateway - Problème de communication avec un service externe
|
|
|
|
|
return {
|
|
|
|
|
code: 502,
|
|
|
|
|
message:
|
|
|
|
|
(axiosError.response?.data as any)?.message ||
|
|
|
|
|
'Erreur de communication avec le serveur. Veuillez réessayer plus tard.',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: (axiosError.response?.data as any)?.details,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 19:40:16 +00:00
|
|
|
// Erreur HTTP sans format standardisé
|
|
|
|
|
return {
|
2025-12-22 21:00:50 +00:00
|
|
|
code: status || 0,
|
2025-12-16 19:40:16 +00:00
|
|
|
message:
|
|
|
|
|
(axiosError.response?.data as any)?.message ||
|
|
|
|
|
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é
|
|
|
|
|
*/
|
|
|
|
|
function normalizeApiError(error: any): ApiError {
|
|
|
|
|
return {
|
|
|
|
|
code: typeof error.code === 'number' ? error.code : parseInt(String(error.code || 0), 10),
|
|
|
|
|
message: error.message || 'An error occurred',
|
|
|
|
|
details: error.details || (Array.isArray(error.details) ? error.details : undefined),
|
|
|
|
|
request_id: error.request_id,
|
|
|
|
|
timestamp: error.timestamp || new Date().toISOString(),
|
|
|
|
|
context: error.context,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|