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

235 lines
7 KiB
TypeScript
Raw Normal View History

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)
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,
};
}
// Erreur HTTP sans format standardisé
return {
2025-12-22 21:00:50 +00:00
code: status || 0,
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
* @param includeRequestId - Si true, inclut le request_id dans le message (pour debugging)
* @returns Message formaté pour l'utilisateur
*/
export function formatErrorMessage(
error: ApiError,
includeRequestId: boolean = false,
): string {
let message = error.message;
// 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(', ');
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}]`;
}
}
return message;
}
/**
* 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
);
}