diff --git a/VEZA_MVP_STABILITY_TODOLIST.json b/VEZA_MVP_STABILITY_TODOLIST.json index 0666fc40c..94abfc19e 100644 --- a/VEZA_MVP_STABILITY_TODOLIST.json +++ b/VEZA_MVP_STABILITY_TODOLIST.json @@ -737,7 +737,7 @@ "description": "Transient errors cause immediate failure. Add retry with exponential backoff.", "owner": "frontend", "estimated_hours": 3, - "status": "todo", + "status": "completed", "priority": 12, "dependencies": [], "files_to_modify": [ @@ -900,12 +900,12 @@ ] }, "progress_tracking": { - "completed": 11, + "completed": 12, "in_progress": 0, - "todo": 4, + "todo": 3, "blocked": 0, - "last_updated": "2025-01-28T00:00:00Z", - "completion_percentage": 73 + "last_updated": "2025-01-28T01:00:00Z", + "completion_percentage": 80 }, "validation_checklist": { "description": "Run these checks after all tasks complete to verify MVP stability", diff --git a/VEZA_MVP_TODOLIST_TRACKING.md b/VEZA_MVP_TODOLIST_TRACKING.md index 029356133..021152345 100644 --- a/VEZA_MVP_TODOLIST_TRACKING.md +++ b/VEZA_MVP_TODOLIST_TRACKING.md @@ -10,10 +10,10 @@ | Métrique | Valeur | |----------|--------| -| **Tâches complétées** | 11 / 15 | +| **Tâches complétées** | 12 / 15 | | **Phase actuelle** | PHASE-3 (Reliability & Polish) | -| **Progression globale** | ███████████░ 73% | -| **Dernière mise à jour** | 2025-01-28 00:00 | +| **Progression globale** | ████████████░ 80% | +| **Dernière mise à jour** | 2025-01-28 01:00 | ### Progression par Phase @@ -21,7 +21,7 @@ |-------|--------|-------------| | PHASE-1 — Bloquants Critiques | ✅ Terminé | 5/5 | | PHASE-2 — Alignement API | ✅ Terminé | 5/5 | -| PHASE-3 — Fiabilité & Polish | 🔄 En cours | 1/5 | +| PHASE-3 — Fiabilité & Polish | 🔄 En cours | 2/5 | | PHASE-3 — Fiabilité | ⚪ En attente | 0/5 | --- @@ -609,32 +609,33 @@ code: z.number() | **Source** | INT-000012 | | **Owner** | Frontend | | **Effort** | ~3h | -| **Statut** | ⬜ À faire | +| **Statut** | ✅ Terminé | **Problème** : Erreurs transitoires causent un échec immédiat. -**Fichier** : -- [ ] `apps/web/src/services/api/client.ts` +**Fichier modifié** : +- [x] `apps/web/src/services/api/client.ts` → Ajouté retry logic avec exponential backoff pour 502/503 -**Implémentation** : -```typescript -async function retryWithBackoff( - fn: () => Promise, - maxRetries: number = 3, - baseDelay: number = 1000 -): Promise { - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - if (attempt === maxRetries - 1) throw error; - if (!isRetryableError(error)) throw error; - await sleep(baseDelay * Math.pow(2, attempt)); - } - } - throw new Error('Max retries exceeded'); -} -``` +**Changements effectués** : +- Créé fonction utilitaire `sleep()` pour les délais +- Créé fonction `getRetryDelay()` qui : + - Respecte le header `Retry-After` si présent + - Utilise exponential backoff sinon (baseDelay * 2^attempt) +- Modifié l'interceptor de réponse pour retry automatiquement les erreurs 502/503 : + - Maximum 3 retries + - Utilise `_retry502503Count` pour éviter les boucles infinies + - Délai calculé avec `getRetryDelay()` (respecte Retry-After ou exponential backoff) + +**Validation** : +- `npx tsc --noEmit` → ✅ Aucune erreur TypeScript +- Retry logic appliqué uniquement aux erreurs 502/503 +- Header Retry-After respecté si présent +- Exponential backoff : 1s, 2s, 4s entre les retries + +**Critères d'acceptation** : +- [x] Erreurs 502/503 retentées jusqu'à 3 fois +- [x] Exponential backoff entre les retries +- [x] Header Retry-After respecté si présent --- @@ -1055,10 +1056,42 @@ Frontend : **Temps passé** : 1h -**Prochaine tâche** : MVP-012 (Add Retry Logic for 503/502 Errors) +**Prochaine tâche** : MVP-013 (Add Error Correlation with Request IDs) **Notes** : Le code de refresh token est maintenant beaucoup plus simple et maintenable. Il n'y a plus de logique de fallback complexe, seulement le format documenté du backend. Les erreurs sont claires si le format change. +---- + +## 2025-01-28 (suite) + +**Tâches travaillées** : MVP-012 +**Statut** : +- MVP-012 : ✅ Terminé + +**Changements effectués** : +- Modifié `apps/web/src/services/api/client.ts` : + - Créé fonction utilitaire `sleep(ms)` pour les délais + - Créé fonction `getRetryDelay(error, attempt, baseDelay)` : + - Vérifie le header `Retry-After` (case-insensitive) + - Utilise exponential backoff sinon : `baseDelay * 2^attempt` + - Modifié l'interceptor de réponse pour retry automatiquement les erreurs 502/503 : + - Maximum 3 retries par requête + - Utilise `_retry502503Count` pour éviter les boucles infinies + - Délai calculé avec `getRetryDelay()` avant chaque retry + - Si tous les retries échouent, parse et rejette l'erreur + +**Validation** : +- `npx tsc --noEmit` → ✅ Aucune erreur TypeScript +- Retry logic appliqué uniquement aux erreurs 502/503 (transient errors) +- Header Retry-After respecté si présent dans la réponse +- Exponential backoff : 1s, 2s, 4s entre les retries (si pas de Retry-After) + +**Temps passé** : 2h + +**Prochaine tâche** : MVP-013 (Add Error Correlation with Request IDs) + +**Notes** : Les erreurs transitoires (502/503) sont maintenant automatiquement retentées avec exponential backoff, améliorant la robustesse de l'application face aux problèmes temporaires de réseau ou de services externes. Le header Retry-After est respecté si présent, permettant au backend de contrôler le timing des retries. + --- ## 📚 Commandes Utiles diff --git a/apps/web/src/services/api/client.ts b/apps/web/src/services/api/client.ts index 9d14d50f7..2b8522bd5 100644 --- a/apps/web/src/services/api/client.ts +++ b/apps/web/src/services/api/client.ts @@ -28,6 +28,37 @@ let failedQueue: Array<{ reject: (error?: any) => void; }> = []; +/** + * Sleep utility function + */ +const sleep = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + + +/** + * Get retry delay from Retry-After header or use exponential backoff + */ +const getRetryDelay = ( + error: AxiosError, + attempt: number, + baseDelay: number = 1000, +): number => { + // Check for Retry-After header (case-insensitive) + const retryAfterHeader = + error.response?.headers['retry-after'] || + error.response?.headers['Retry-After']; + if (retryAfterHeader) { + const delay = parseInt(String(retryAfterHeader), 10); + if (!isNaN(delay) && delay > 0) { + return delay * 1000; // Convert to milliseconds + } + } + + // Exponential backoff: baseDelay * 2^attempt + return baseDelay * Math.pow(2, attempt); +}; + // T0177: Fonction pour traiter la queue de requêtes en attente const processQueue = (error: Error | null, token: string | null = null) => { failedQueue.forEach((prom) => { @@ -194,17 +225,29 @@ apiClient.interceptors.response.use( return Promise.reject(apiError); } - if (status === 503) { - // Service Unavailable - ClamAV ou autre service externe - const apiError = parseApiError(error); - // Message déjà formaté dans parseApiError avec message spécifique pour 503 - return Promise.reject(apiError); - } + // Retry logic for 502/503 errors (transient errors) + if (status === 502 || status === 503) { + // Service Unavailable (503) or Bad Gateway (502) - Retry with exponential backoff + // Check if this request has already been retried (to avoid infinite loops) + const retryCount = (originalRequest as any)._retry502503Count || 0; + const maxRetries = 3; - if (status === 502) { - // Bad Gateway - Problème de communication avec un service externe + if (originalRequest && retryCount < maxRetries) { + // Mark that we're retrying this request + (originalRequest as any)._retry502503Count = retryCount + 1; + + // Calculate delay (respect Retry-After header if present, otherwise exponential backoff) + const delay = getRetryDelay(error, retryCount, 1000); + + // Wait before retrying + return sleep(delay).then(() => { + // Retry the request + return apiClient(originalRequest); + }); + } + + // If already retried maxRetries times, reject immediately const apiError = parseApiError(error); - // Message déjà formaté dans parseApiError avec message spécifique pour 502 return Promise.reject(apiError); }