Gestion des Erreurs
Guide complet pour gérer les erreurs de l'API MONCRENEAU de manière robuste.
1. Codes d'Erreur HTTP
L'API utilise les codes HTTP standards:
| Code | Signification | Action |
|---|---|---|
| 2xx | Succès | Continuer |
| 4xx | Erreur client | Corriger la requête |
| 5xx | Erreur serveur | Retry avec backoff |
Erreurs 4xx (Erreurs Client)
try {
const response = await api.post('/appointments', data);
} catch (error) {
if (error.response?.status >= 400 && error.response?.status < 500) {
// Erreur client - NE PAS RETRY
const errorData = error.response.data;
switch (errorData.error.code) {
case 'VALIDATION_ERROR':
console.error('Données invalides:', errorData.error.details);
// Afficher les erreurs à l'utilisateur
break;
case 'INSUFFICIENT_CREDITS':
console.error('Crédits insuffisants');
// Rediriger vers page de recharge
break;
case 'SLOT_NOT_AVAILABLE':
console.error('Créneau déjà réservé');
// Proposer d'autres créneaux
break;
case 'UNAUTHORIZED':
console.error('Clé API invalide');
// Vérifier la configuration
break;
default:
console.error('Erreur client:', errorData);
}
}
}
Erreurs 5xx (Erreurs Serveur)
async function callApiWithRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const status = error.response?.status;
if (status >= 500 || !status) {
// Erreur serveur ou réseau - RETRY
if (attempt < maxAttempts) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
}
// Dernière tentative ou erreur non-retriable
throw error;
}
}
}
// Usage
const appointment = await callApiWithRetry(() =>
api.post('/appointments', data)
);
2. Structure des Erreurs
Toutes les erreurs suivent ce format:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Crédits insuffisants pour créer le rendez-vous",
"details": {
"required": 1,
"available": 0
}
},
"timestamp": "2024-02-01T10:30:00Z",
"path": "/v1/appointments"
}
Parser les Erreurs
function parseApiError(error) {
if (!error.response) {
return {
code: 'NETWORK_ERROR',
message: 'Impossible de contacter le serveur',
retryable: true
};
}
const status = error.response.status;
const data = error.response.data;
return {
code: data?.error?.code || 'UNKNOWN_ERROR',
message: data?.error?.message || error.message,
details: data?.error?.details,
status: status,
retryable: status >= 500 || status === 429
};
}
// Usage
try {
await api.post('/appointments', data);
} catch (error) {
const parsedError = parseApiError(error);
if (parsedError.retryable) {
console.log('Error is retryable');
}
console.error(`[${parsedError.code}] ${parsedError.message}`);
}
3. Patterns de Retry
Exponential Backoff
async function exponentialBackoff(fn, options = {}) {
const {
maxAttempts = 3,
baseDelay = 1000,
maxDelay = 30000,
factor = 2
} = options;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const shouldRetry =
attempt < maxAttempts &&
isRetryableError(error);
if (!shouldRetry) throw error;
const delay = Math.min(
baseDelay * Math.pow(factor, attempt - 1),
maxDelay
);
console.log(`Retry ${attempt}/${maxAttempts} after ${delay}ms`);
await sleep(delay);
}
}
}
function isRetryableError(error) {
const status = error.response?.status;
return (
!status || // Network error
status >= 500 || // Server error
status === 429 || // Rate limit
status === 408 // Timeout
);
}
// Usage
const result = await exponentialBackoff(
() => api.post('/appointments', data),
{ maxAttempts: 5, baseDelay: 2000 }
);
Circuit Breaker
Arrête temporairement les requêtes après plusieurs échecs.
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
this.failures = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
console.error(`Circuit breaker OPEN for ${this.resetTimeout}ms`);
}
}
}
// Usage
const breaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 60000
});
try {
const result = await breaker.execute(() =>
api.post('/appointments', data)
);
} catch (error) {
console.error('Request failed or circuit open:', error);
}
4. Rate Limiting (429)
Gérer la limite de 100 requêtes/minute.
const pThrottle = require('p-throttle');
// Limiter à 80 req/min (marge de sécurité)
const throttle = pThrottle({
limit: 80,
interval: 60000,
strict: true
});
const createAppointment = throttle(async (data) => {
return api.post('/appointments', data);
});
// Gestion de 429 avec Retry-After
async function handleRateLimit(fn) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 60000;
console.log(`Rate limited. Retrying in ${delay}ms`);
await sleep(delay);
return await fn();
}
throw error;
}
}
// Usage
const appointment = await handleRateLimit(() =>
createAppointment(data)
);
5. Timeouts
Toujours définir un timeout.
[object Object]
6. Logs Structurés
Logger toutes les erreurs avec contexte.
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
async function createAppointment(data) {
const requestId = generateRequestId();
try {
logger.info('Creating appointment', {
requestId,
organizationId: data.organizationId,
departmentId: data.departmentId
});
const response = await api.post('/appointments', data);
logger.info('Appointment created', {
requestId,
appointmentId: response.data.id,
status: response.data.status
});
return response.data;
} catch (error) {
const parsedError = parseApiError(error);
logger.error('Failed to create appointment', {
requestId,
errorCode: parsedError.code,
errorMessage: parsedError.message,
status: parsedError.status,
data: data,
stack: error.stack
});
throw error;
}
}
7. Alertes
Configurer des alertes pour les erreurs critiques.
const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);
async function sendAlert(error, context) {
if (error.response?.status >= 500) {
await slack.chat.postMessage({
channel: '#tech-alerts',
text: `API Error`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*API Error*\n*Code:* ${error.code}\n*Message:* ${error.message}\n*Context:* ${JSON.stringify(context)}`
}
}
]
});
}
}
// Usage
try {
await api.post('/appointments', data);
} catch (error) {
await sendAlert(error, {
action: 'create_appointment',
organizationId: data.organizationId
});
throw error;
}
8. Fallback Strategies
Stratégie de Dégradation
async function createAppointmentWithFallback(data) {
try {
// Tentative normale
return await api.post('/appointments', data);
} catch (error) {
if (error.response?.status >= 500) {
// Fallback: sauvegarder en local
console.warn('API unavailable, saving to local queue');
await saveToLocalQueue(data);
return {
id: 'pending_' + Date.now(),
status: 'queued',
message: 'Rendez-vous en attente de synchronisation'
};
}
throw error;
}
}
// Worker pour synchroniser la queue
setInterval(async () => {
const pendingAppointments = await getLocalQueue();
for (const appointment of pendingAppointments) {
try {
const created = await api.post('/appointments', appointment);
await removeFromLocalQueue(appointment.id);
console.log(`Synced appointment ${created.data.id}`);
} catch (error) {
console.error(`Failed to sync ${appointment.id}`);
}
}
}, 60000); // Chaque minute
9. Dead Letter Queue
Pour les événements qui échouent définitivement.
const Bull = require('bull');
const appointmentQueue = new Bull('appointments');
const deadLetterQueue = new Bull('appointments-dlq');
appointmentQueue.process(async (job) => {
try {
return await api.post('/appointments', job.data);
} catch (error) {
if (job.attemptsMade >= 5) {
// Échec définitif après 5 tentatives
await deadLetterQueue.add({
originalJob: job.data,
error: error.message,
attempts: job.attemptsMade,
failedAt: new Date()
});
// Alerter l'équipe
await sendAlert(error, { jobId: job.id });
}
throw error;
}
});
// Monitoring de la DLQ
setInterval(async () => {
const dlqCount = await deadLetterQueue.count();
if (dlqCount > 10) {
console.error(`⚠️ ${dlqCount} items in dead letter queue`);
}
}, 300000); // Toutes les 5 minutes
Checklist Gestion d'Erreurs
- Tous les appels API dans try/catch
- Retry avec exponential backoff pour 5xx
- Pas de retry pour 4xx (sauf 429)
- Circuit breaker configuré
- Timeouts définis (30s max)
- Rate limiting côté client (< 100/min)
- Logs structurés avec contexte
- Alertes sur erreurs critiques
- Dead letter queue pour échecs
- Stratégie de fallback définie