fix(MVP-012): Add retry logic with exponential backoff for 502/503 errors

This commit is contained in:
senke 2025-12-22 23:10:52 +01:00
parent 44509e9b2e
commit 872f11d264
3 changed files with 117 additions and 41 deletions

View file

@ -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",

View file

@ -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<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
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

View file

@ -28,6 +28,37 @@ let failedQueue: Array<{
reject: (error?: any) => void;
}> = [];
/**
* Sleep utility function
*/
const sleep = (ms: number): Promise<void> => {
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);
}