DEV Community

Cover image for Webhooks vs Polling: Quelle méthode d'intégration API est la meilleure ?
Antoine Laurent
Antoine Laurent

Posted on • Originally published at apidog.com

Webhooks vs Polling: Quelle méthode d'intégration API est la meilleure ?

En bref : Le sondage (polling) vérifie les mises à jour périodiquement (simple mais inefficace). Les webhooks poussent les mises à jour en temps réel (efficace mais complexe). Utilisez le sondage pour les vérifications peu fréquentes, les webhooks pour les mises à jour en temps réel. L'API Petstore moderne prend en charge les deux modèles avec une livraison fiable des webhooks.

Essayez Apidog dès aujourd'hui

Comprendre la différence

Sondage (Polling) : Le client demande « Y a-t-il des mises à jour ? » de manière répétée.
Webhooks : Le serveur dit « Voici une mise à jour ! » quand quelque chose se produit.

  • Sondage = Vérifier votre boîte aux lettres toutes les heures
  • Webhooks = Le facteur sonne à la porte quand le courrier arrive

Sondage (Polling) : Comment ça fonctionne

Le client effectue des requêtes périodiques pour vérifier les changements.

// Sonde toutes les 30 secondes
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);
Enter fullscreen mode Exit fullscreen mode

Modèles de sondage :

Sondage simple :

GET /api/v1/orders/123
# Retourne l'état actuel de la commande
Enter fullscreen mode Exit fullscreen mode

Sondage conditionnel (ETag) :

GET /api/v1/orders/123
If-None-Match: "abc123"

# Retourne 304 Not Modified si inchangé
# Retourne 200 avec de nouvelles données si modifié
Enter fullscreen mode Exit fullscreen mode

Sondage basé sur une date (since-based) :

GET /api/v1/orders/123/events?since=1710331200
# Retourne les événements depuis l'horodatage
Enter fullscreen mode Exit fullscreen mode

Webhooks : Comment ça fonctionne

Le serveur envoie une requête HTTP POST à votre endpoint lorsque des événements se produisent.

Flux de configuration :

// 1. Enregistrer l'endpoint du webhook
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. Le serveur envoie le webhook lorsqu'un événement se produit
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Vérifier et traiter le webhook
// Répondre avec 200 OK
Enter fullscreen mode Exit fullscreen mode

Quand utiliser le sondage (polling)

Bon pour :

  • Vérifications peu fréquentes (une fois par heure)
  • Petit nombre de ressources
  • Implémentations simples
  • Lorsque vous contrôlez le client
  • Tests et débogage

Exemples :

  • Vérifier le statut d'un rapport quotidien
  • Synchroniser les contacts toutes les quelques minutes
  • Surveiller l'état du serveur
  • Vérifier le statut de paiement (peu fréquent)

Le sondage est approprié lorsque :

  • Les mises à jour sont rares
  • Un léger délai est acceptable
  • Vous souhaitez une implémentation simple
  • La ressource est petite

Quand utiliser les webhooks

Bon pour :

  • Mises à jour en temps réel
  • Nombreuses ressources à surveiller
  • Événements sensibles au temps
  • Intégrations tierces
  • Mises à jour à haute fréquence

Exemples :

  • Confirmations de paiement
  • Messages de chat
  • Alertes de prix boursiers
  • Changements de statut de commande
  • Notifications CI/CD

Les webhooks sont meilleurs lorsque :

  • Les mises à jour doivent être immédiates
  • Le sondage serait inefficace
  • De nombreux clients surveillent la même ressource
  • Vous souhaitez réduire la charge du serveur

Tableau comparatif

Facteur Sondage (Polling) Webhooks
Latence Jusqu'à l'intervalle de sondage Temps réel
Charge serveur Élevée (nombreuses requêtes vides) Faible (seulement les événements réels)
Complexité Simple Complexe
Fiabilité Élevée (le client contrôle la nouvelle tentative) Moyenne (nécessite une logique de nouvelle tentative)
Configuration Aucune Enregistrement de l'endpoint
Problèmes de pare-feu Aucun (sortant uniquement) Peut nécessiter une liste blanche
Coût Plus élevé (plus de requêtes) Moins élevé (moins de requêtes)
Idéal pour Vérifications peu fréquentes Mises à jour en temps réel

Implémenter le sondage (polling)

Sondage de base

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Rappeler seulement si le statut a changé
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Arrêter le sondage si l'état est terminal
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Continuer le sondage
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Recommencer après une erreur
    }
  };

  poll();
}

// Utilisation
pollOrderStatus('order-123', (order) => {
  console.log(`Order status: ${order.status}`);
});
Enter fullscreen mode Exit fullscreen mode

Sondage intelligent (retrait exponentiel)

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Rappeler si les données ont changé
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Arrêter si la condition est remplie
      if (stopCondition(data)) {
        return;
      }

      // Réinitialiser l'intervalle en cas de requête réussie
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Max retries exceeded');
      }
    }

    // Planifier le prochain sondage avec un retrait exponentiel
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Utilisation : Sonde la commande jusqu'à ce qu'elle soit complétée
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Order:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);
Enter fullscreen mode Exit fullscreen mode

Sondage avec ETag

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // Non modifié, continuer le sondage
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}
Enter fullscreen mode Exit fullscreen mode

Implémenter les webhooks

Enregistrer les webhooks

// Enregistrer l'endpoint du webhook
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Enter fullscreen mode Exit fullscreen mode

Recevoir les webhooks

const express = require('express');
const crypto = require('crypto');
const app = express();

// Parseur de corps brut pour la vérification de signature
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Vérifier la signature
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body.toString());

  // Traiter l'événement
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Accuser réception
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
Enter fullscreen mode Exit fullscreen mode

Tester les webhooks localement

# Utiliser ngrok pour exposer l'endpoint local
ngrok http 3000

# Enregistrer l'URL ngrok comme endpoint de webhook
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'
Enter fullscreen mode Exit fullscreen mode

Livraison fiable des webhooks

Les webhooks peuvent échouer. Implémentez une logique de nouvelle tentative.

Côté expéditeur (serveur)

// Mettre en file d'attente les webhooks pour la livraison
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Supprimer de la file d'attente en cas de succès
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Planifier une nouvelle tentative avec un retrait exponentiel
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Marquer comme échoué après 5 tentatives
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1min, 5min, 15min, 1h, 4h
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Côté récepteur (client)

// Gestion idempotente des webhooks
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Ignorer si déjà traité (idempotence)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Nettoyer les anciens ID d'événements (garder les 1000 derniers)
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Retourner 5xx pour déclencher une nouvelle tentative
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function processEvent(event) {
  // Traiter l'événement
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... gérer les autres événements
  }
}
Enter fullscreen mode Exit fullscreen mode

Approche hybride

Utilisez à la fois le sondage et les webhooks pour les mises à jour critiques.

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Commencer par le sondage pour un feedback immédiat
    this.startPolling();

    // Enregistrer le webhook pour une mise à jour en temps réel
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Suppression automatique après la première livraison
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

FAQ

Q : À quelle fréquence dois-je sonder ? Dépend de l'urgence. 30 secondes pour du quasi temps réel. 5 minutes pour les moins urgents. Équilibrez la fraîcheur des données et la charge du serveur.

Q : Que se passe-t-il si mon endpoint de webhook est indisponible ? Les bons fournisseurs de webhooks réessayent avec un retrait exponentiel. Implémentez l'idempotence pour gérer les livraisons en double.

Q : Comment sécuriser les webhooks ? Vérifiez les signatures à l'aide de secrets partagés. Utilisez HTTPS uniquement. Validez les données d'événement.

Q : Puis-je utiliser les webhooks pour les données historiques ? Non. Les webhooks sont uniquement pour les nouveaux événements. Utilisez le sondage ou les API de traitement par lots pour les données historiques.

Q : Dois-je utiliser le sondage ou les webhooks pour les applications mobiles ? Le sondage est plus simple pour le mobile. Les webhooks nécessitent des notifications push comme intermédiaire.

Q : Comment déboguer les problèmes de webhook ? Utilisez des outils comme webhook.site pour les tests. Enregistrez toutes les livraisons de webhooks. Fournissez un historique des événements de webhook dans votre API.

L'API Petstore moderne prend en charge le sondage et les webhooks. Consultez le guide des webhooks pour les détails d'implémentation. Testez les intégrations de webhooks avec Apidog.

Top comments (0)