fix(MVP-012): Add retry logic with exponential backoff for 502/503 errors
This commit is contained in:
parent
44509e9b2e
commit
872f11d264
3 changed files with 117 additions and 41 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue